feat: basic song ui + settings
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
|
||||
export default ts.config(
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
);
|
||||
15
package.json
15
package.json
@@ -8,19 +8,17 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^1.0.0-next.64",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"lucide-svelte": "^0.460.1",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
@@ -31,7 +29,12 @@
|
||||
"tailwindcss": "^3.4.9",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobuf-ts/grpcweb-transport": "^2.9.4",
|
||||
"@protobuf-ts/plugin": "^2.9.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"mode-watcher": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
20
protos/library.proto
Normal file
20
protos/library.proto
Normal file
@@ -0,0 +1,20 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import 'google/protobuf/empty.proto';
|
||||
|
||||
package library;
|
||||
|
||||
service Library {
|
||||
rpc ListTracks(google.protobuf.Empty) returns (TrackList);
|
||||
}
|
||||
|
||||
message TrackList {
|
||||
repeated Track tracks = 1;
|
||||
}
|
||||
|
||||
message Track {
|
||||
string hash = 1;
|
||||
string name = 2;
|
||||
string artist_name = 3;
|
||||
uint64 artist_id = 4;
|
||||
}
|
||||
18
protos/player.proto
Normal file
18
protos/player.proto
Normal file
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import 'google/protobuf/empty.proto';
|
||||
|
||||
package player;
|
||||
|
||||
service Player {
|
||||
rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse);
|
||||
rpc ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
}
|
||||
|
||||
message PlayTrackRequest {
|
||||
string hash = 1;
|
||||
}
|
||||
|
||||
message PlayTrackResponse {
|
||||
}
|
||||
43
protos/settings.proto
Normal file
43
protos/settings.proto
Normal file
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import 'google/protobuf/empty.proto';
|
||||
|
||||
package settings;
|
||||
|
||||
service Settings {
|
||||
rpc ListPaths(google.protobuf.Empty) returns (SettingsData);
|
||||
rpc AddPath(AddPathRequest) returns (AddPathResponse);
|
||||
rpc DeletePath(DeletePathRequest) returns (DeletePathResponse);
|
||||
rpc RefreshPath(RefreshPathRequest) returns (RefreshPathResponse);
|
||||
}
|
||||
|
||||
message SettingsData {
|
||||
repeated LibraryPath library_paths = 1;
|
||||
}
|
||||
|
||||
message LibraryPath {
|
||||
uint64 id = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message AddPathRequest {
|
||||
string path = 1;
|
||||
}
|
||||
|
||||
message AddPathResponse {
|
||||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message DeletePathRequest {
|
||||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message DeletePathResponse {
|
||||
}
|
||||
|
||||
message RefreshPathRequest {
|
||||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message RefreshPathResponse {
|
||||
}
|
||||
50
src/app.css
50
src/app.css
@@ -6,24 +6,24 @@
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--destructive: 0 100% 67%;
|
||||
--destructive-foreground: 0 100% 5%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
@@ -32,28 +32,30 @@
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--primary: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--destructive: 0 100% 67%;
|
||||
--destructive-foreground: 0 100% 5%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
@@ -73,3 +75,7 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-user-drag: none !important;
|
||||
}
|
||||
|
||||
5
src/hooks.server.ts
Normal file
5
src/hooks.server.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
|
||||
|
||||
export const protoTransport = new GrpcWebFetchTransport({
|
||||
baseUrl: 'http://[::1]:39993'
|
||||
});
|
||||
73
src/lib/components/groove/AppSidebar.svelte
Normal file
73
src/lib/components/groove/AppSidebar.svelte
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||
import { Home, Library, ListMusic, Music2, Settings } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<Sidebar.Root collapsible="icon">
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/'} class="transition-all">
|
||||
{#snippet child({ props })}
|
||||
<a href="/" {...props}>
|
||||
<Home />
|
||||
<span>Home</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
<Sidebar.Content>
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>Library</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/songs'} class="transition-all">
|
||||
{#snippet child({ props })}
|
||||
<a href="/songs" {...props}>
|
||||
<Music2 />
|
||||
<span>Songs</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/albums'} class="transition-all">
|
||||
{#snippet child({ props })}
|
||||
<a href="/albums" {...props}>
|
||||
<Library />
|
||||
<span>Albums</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/playlists'} class="transition-all">
|
||||
{#snippet child({ props })}
|
||||
<a href="/playlists" {...props}>
|
||||
<ListMusic />
|
||||
<span>Playlists</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Footer>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/settings'} class="transition-all">
|
||||
{#snippet child({ props })}
|
||||
<a href="/settings" {...props}>
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Footer>
|
||||
</Sidebar.Root>
|
||||
160
src/lib/components/groove/Footer.svelte
Normal file
160
src/lib/components/groove/Footer.svelte
Normal file
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
VolumeX,
|
||||
Volume1,
|
||||
Volume2,
|
||||
List
|
||||
} from 'lucide-svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Progress } from '$lib/components/ui/progress';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { currentlyPlaying, volume } from '$lib/stores/player';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
let progressValue = $state(0);
|
||||
|
||||
let isPlaying = $state(false);
|
||||
|
||||
/* async function seekProgressValue(e: MouseEvent) {
|
||||
if (!$currentlyPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.currentTarget! as HTMLElement;
|
||||
const _targetProgress = Math.max(0.0, Math.min(1.0, e.offsetX / target.offsetWidth));
|
||||
} */
|
||||
|
||||
async function skipToQueueIndex(index: number) {
|
||||
console.log(`SKIP TO QUEUE INDEX ${index}`);
|
||||
}
|
||||
|
||||
let volumeValue = {
|
||||
get value() {
|
||||
return [$volume];
|
||||
},
|
||||
set value(v: number[]) {
|
||||
$volume = v[0];
|
||||
// TODO: Send message to player
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<footer class="border border-x-0 border-t border-border/40 bg-background/95 px-4">
|
||||
<nav class="my-2 grid w-full grid-cols-5 items-center">
|
||||
<div class="col-span-1 flex items-end gap-2">
|
||||
{#if $currentlyPlaying}
|
||||
<img
|
||||
class="w-16 rounded-md shadow-2xl shadow-primary/50"
|
||||
src="https://i.scdn.co/image/ab67616d0000b2732c0ead8ce0dd1c6e2fca817f"
|
||||
alt={$currentlyPlaying}
|
||||
/>
|
||||
<div class="space-y-1 text-sm">
|
||||
<h3 class="font-medium leading-none">Song name</h3>
|
||||
<p class="text-xs text-muted-foreground">Artist name</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-16 w-16 rounded-md bg-muted shadow-2xl shadow-primary/50"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<form class="col-span-3 flex justify-center gap-1" method="POST" use:enhance>
|
||||
<Button variant="outline" size="icon">
|
||||
<SkipBack />
|
||||
</Button>
|
||||
<Button type="submit" formaction="/player?/pause" variant="outline" size="icon">
|
||||
{#if isPlaying}
|
||||
<Pause />
|
||||
{:else}
|
||||
<Play />
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<SkipForward />
|
||||
</Button>
|
||||
</form>
|
||||
<div class="col-span-1 flex items-center">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger class="mr-8 flex items-center text-sm">
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="sm">
|
||||
<List class="mr-2 size-4" />
|
||||
<span>15</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="flex max-h-[max(50vh,300px)] w-64 flex-col overflow-y-hidden">
|
||||
<p class="font-semibold">Next up</p>
|
||||
|
||||
<Separator class="my-1" />
|
||||
|
||||
<div class="flex h-full flex-col gap-1 overflow-y-auto pr-3">
|
||||
{#each [] as _song, i}
|
||||
<button
|
||||
class="flex flex-row items-center gap-2 rounded-lg px-3 py-2 transition-all hover:bg-secondary"
|
||||
onclick={() => skipToQueueIndex(i)}
|
||||
>
|
||||
<div class="min-w-8 overflow-hidden rounded-md">
|
||||
<img
|
||||
src="https://i.scdn.co/image/ab67616d0000b2732c0ead8ce0dd1c6e2fca817f"
|
||||
class="aspect-square size-8"
|
||||
alt="Cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<p
|
||||
class="w-full self-start overflow-hidden text-ellipsis text-nowrap text-left text-sm text-foreground/80"
|
||||
>
|
||||
Song name
|
||||
</p>
|
||||
<p
|
||||
class="w-full self-start overflow-hidden text-left text-xs text-muted-foreground"
|
||||
>
|
||||
Song artist
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
<div class="flex grow flex-row items-center gap-4">
|
||||
<div class="min-w-6 text-muted-foreground">
|
||||
{#if $volume <= 0}
|
||||
<VolumeX class="size-full" />
|
||||
{:else if $volume < 50.0}
|
||||
<Volume1 class="size-full" />
|
||||
{:else}
|
||||
<Volume2 class="size-full" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<Slider class="w-full" min={0} max={1} step={0.01} bind:value={volumeValue.value} />
|
||||
</div>
|
||||
<div class="min-w-10 self-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{Math.round($volume * 100.0)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="mb-2 flex flex-row items-center gap-2">
|
||||
<span class="w-12 text-left text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
|
||||
<button class="w-full">
|
||||
{#key progressValue}
|
||||
<Progress class="pointer-events-none w-full" value={progressValue} max={100} />
|
||||
{/key}
|
||||
</button>
|
||||
<span class="w-12 text-right text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
|
||||
</div>
|
||||
</footer>
|
||||
41
src/lib/components/groove/SongListing.svelte
Normal file
41
src/lib/components/groove/SongListing.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { getCoverUrl } from '$lib/covers';
|
||||
|
||||
// import { AudioLines } from 'lucide-svelte';
|
||||
import type { Song } from '$lib/song';
|
||||
|
||||
interface Props {
|
||||
song: Song;
|
||||
}
|
||||
|
||||
let { song }: Props = $props();
|
||||
|
||||
async function playSong(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
// TODO: Play song
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="flex flex-col gap-3" method="POST" action="/songs/{song.hash}?/play" use:enhance>
|
||||
<div class="relative">
|
||||
<button type="submit" class="relative overflow-hidden rounded-lg" oncontextmenu={playSong}>
|
||||
<img
|
||||
class="aspect-square w-auto transition-all duration-150 hover:scale-105 hover:saturate-150"
|
||||
src={getCoverUrl(song.hash)}
|
||||
alt={song.name}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- <div
|
||||
class="absolute bottom-6 left-2 size-4 animate-pulse"
|
||||
class:hidden={$currentlyPlaying?.entry.hash !== song.entry.hash}
|
||||
>
|
||||
<AudioLines class="text-primary" />
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="relative space-y-1 text-sm">
|
||||
<h3 class="font-medium leading-none">{song.name}</h3>
|
||||
<p class="text-xs text-muted-foreground">{song.artistName}</p>
|
||||
</div>
|
||||
</form>
|
||||
13
src/lib/components/groove/ThemeSwitcher.svelte
Normal file
13
src/lib/components/groove/ThemeSwitcher.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Moon, Sun } from 'lucide-svelte';
|
||||
import { toggleMode, mode } from 'mode-watcher';
|
||||
</script>
|
||||
|
||||
<Button variant="outline" size="icon" onclick={toggleMode}>
|
||||
{#if $mode === 'light'}
|
||||
<Sun />
|
||||
{:else}
|
||||
<Moon />
|
||||
{/if}
|
||||
</Button>
|
||||
69
src/lib/components/ui/button/button.svelte
Normal file
69
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" module>
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||
outline:
|
||||
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a bind:this={ref} class={cn(buttonVariants({ variant, size, className }))} {href} {...restProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants
|
||||
} from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import Check from 'lucide-svelte/icons/check';
|
||||
import Minus from 'lucide-svelte/icons/minus';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
bind:ref
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if indeterminate}
|
||||
<Minus class="size-4" />
|
||||
{:else}
|
||||
<Check class={cn('size-4', !checked && 'text-transparent')} />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
{/snippet}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ContentProps & {
|
||||
portalProps?: ContextMenuPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Portal {...portalProps}>
|
||||
<ContextMenuPrimitive.Content
|
||||
class={cn(
|
||||
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.GroupHeadingProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold text-foreground', inset && 'pl-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
23
src/lib/components/ui/context-menu/context-menu-item.svelte
Normal file
23
src/lib/components/ui/context-menu/context-menu-item.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Item
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import Circle from 'lucide-svelte/icons/circle';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
class={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||
{#if checked}
|
||||
<Circle class="size-2 fill-current" />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.({ checked })}
|
||||
{/snippet}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Separator
|
||||
bind:ref
|
||||
class={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
class={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SubContentProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
class={cn(
|
||||
'z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
inset,
|
||||
...restProps
|
||||
}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {
|
||||
inset?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
class={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRight class="ml-auto size-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
49
src/lib/components/ui/context-menu/index.ts
Normal file
49
src/lib/components/ui/context-menu/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
|
||||
|
||||
import Item from './context-menu-item.svelte';
|
||||
import GroupHeading from './context-menu-group-heading.svelte';
|
||||
import Content from './context-menu-content.svelte';
|
||||
import Shortcut from './context-menu-shortcut.svelte';
|
||||
import RadioItem from './context-menu-radio-item.svelte';
|
||||
import Separator from './context-menu-separator.svelte';
|
||||
import SubContent from './context-menu-sub-content.svelte';
|
||||
import SubTrigger from './context-menu-sub-trigger.svelte';
|
||||
import CheckboxItem from './context-menu-checkbox-item.svelte';
|
||||
|
||||
const Sub: typeof ContextMenuPrimitive.Sub = ContextMenuPrimitive.Sub;
|
||||
const Root: typeof ContextMenuPrimitive.Root = ContextMenuPrimitive.Root;
|
||||
const Trigger: typeof ContextMenuPrimitive.Trigger = ContextMenuPrimitive.Trigger;
|
||||
const Group: typeof ContextMenuPrimitive.Group = ContextMenuPrimitive.Group;
|
||||
const RadioGroup: typeof ContextMenuPrimitive.RadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
export {
|
||||
Sub,
|
||||
Root,
|
||||
Item,
|
||||
Group,
|
||||
Trigger,
|
||||
Content,
|
||||
Shortcut,
|
||||
Separator,
|
||||
RadioItem,
|
||||
GroupHeading,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
RadioGroup,
|
||||
CheckboxItem,
|
||||
//
|
||||
Root as ContextMenu,
|
||||
Sub as ContextMenuSub,
|
||||
Item as ContextMenuItem,
|
||||
Group as ContextMenuGroup,
|
||||
Content as ContextMenuContent,
|
||||
Trigger as ContextMenuTrigger,
|
||||
Shortcut as ContextMenuShortcut,
|
||||
RadioItem as ContextMenuRadioItem,
|
||||
Separator as ContextMenuSeparator,
|
||||
GroupHeading as ContextMenuGroupHeading,
|
||||
RadioGroup as ContextMenuRadioGroup,
|
||||
SubContent as ContextMenuSubContent,
|
||||
SubTrigger as ContextMenuSubTrigger,
|
||||
CheckboxItem as ContextMenuCheckboxItem
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
};
|
||||
22
src/lib/components/ui/input/input.svelte
Normal file
22
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLInputAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/popover/index.ts
Normal file
17
src/lib/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import Content from './popover-content.svelte';
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Trigger = PopoverPrimitive.Trigger;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose
|
||||
};
|
||||
28
src/lib/components/ui/popover/popover-content.svelte
Normal file
28
src/lib/components/ui/popover/popover-content.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
{align}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
7
src/lib/components/ui/progress/index.ts
Normal file
7
src/lib/components/ui/progress/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './progress.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Progress
|
||||
};
|
||||
24
src/lib/components/ui/progress/progress.svelte
Normal file
24
src/lib/components/ui/progress/progress.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Progress as ProgressPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
max = 100,
|
||||
value,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<ProgressPrimitive.Root
|
||||
bind:ref
|
||||
{value}
|
||||
class={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
class="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={`transform: translateX(-${100 - (100 * (value ?? 0)) / (max ?? 1)}%)`}
|
||||
></div>
|
||||
</ProgressPrimitive.Root>
|
||||
10
src/lib/components/ui/scroll-area/index.ts
Normal file
10
src/lib/components/ui/scroll-area/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Scrollbar from './scroll-area-scrollbar.svelte';
|
||||
import Root from './scroll-area.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Scrollbar,
|
||||
//,
|
||||
Root as ScrollArea,
|
||||
Scrollbar as ScrollAreaScrollbar
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { ScrollArea as ScrollAreaPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'vertical',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props();
|
||||
</script>
|
||||
|
||||
<ScrollAreaPrimitive.Scrollbar
|
||||
bind:ref
|
||||
{orientation}
|
||||
class={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal' && 'h-2.5 w-full border-t border-t-transparent p-px',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ScrollAreaPrimitive.Thumb
|
||||
class={cn('relative rounded-full bg-border', orientation === 'vertical' && 'flex-1')}
|
||||
/>
|
||||
</ScrollAreaPrimitive.Scrollbar>
|
||||
32
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
32
src/lib/components/ui/scroll-area/scroll-area.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { ScrollArea as ScrollAreaPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import { Scrollbar } from './index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'vertical',
|
||||
scrollbarXClasses = '',
|
||||
scrollbarYClasses = '',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||
orientation?: 'vertical' | 'horizontal' | 'both' | undefined;
|
||||
scrollbarXClasses?: string | undefined;
|
||||
scrollbarYClasses?: string | undefined;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ScrollAreaPrimitive.Root bind:ref {...restProps} class={cn('relative overflow-hidden', className)}>
|
||||
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
|
||||
{@render children?.()}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
{#if orientation === 'vertical' || orientation === 'both'}
|
||||
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||
{/if}
|
||||
{#if orientation === 'horizontal' || orientation === 'both'}
|
||||
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||
{/if}
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
7
src/lib/components/ui/separator/index.ts
Normal file
7
src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator
|
||||
};
|
||||
22
src/lib/components/ui/separator/separator.svelte
Normal file
22
src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'horizontal',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'min-h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
{...restProps}
|
||||
/>
|
||||
37
src/lib/components/ui/sheet/index.ts
Normal file
37
src/lib/components/ui/sheet/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
|
||||
import Overlay from './sheet-overlay.svelte';
|
||||
import Content from './sheet-content.svelte';
|
||||
import Header from './sheet-header.svelte';
|
||||
import Footer from './sheet-footer.svelte';
|
||||
import Title from './sheet-title.svelte';
|
||||
import Description from './sheet-description.svelte';
|
||||
|
||||
const Root = SheetPrimitive.Root;
|
||||
const Close = SheetPrimitive.Close;
|
||||
const Trigger = SheetPrimitive.Trigger;
|
||||
const Portal = SheetPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Close,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Header,
|
||||
Footer,
|
||||
Title,
|
||||
Description,
|
||||
//
|
||||
Root as Sheet,
|
||||
Close as SheetClose,
|
||||
Trigger as SheetTrigger,
|
||||
Portal as SheetPortal,
|
||||
Overlay as SheetOverlay,
|
||||
Content as SheetContent,
|
||||
Header as SheetHeader,
|
||||
Footer as SheetFooter,
|
||||
Title as SheetTitle,
|
||||
Description as SheetDescription
|
||||
};
|
||||
56
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
56
src/lib/components/ui/sheet/sheet-content.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const sheetVariants = tv({
|
||||
base: 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
variants: {
|
||||
side: {
|
||||
top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b',
|
||||
bottom:
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t',
|
||||
left: 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
right:
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right'
|
||||
}
|
||||
});
|
||||
|
||||
export type Side = VariantProps<typeof sheetVariants>['side'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import X from 'lucide-svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SheetOverlay from './sheet-overlay.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
side = 'right',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||
portalProps?: SheetPrimitive.PortalProps;
|
||||
side?: Side;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Portal {...portalProps}>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
<SheetPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPrimitive.Portal>
|
||||
16
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
16
src/lib/components/ui/sheet/sheet-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
class={cn('text-sm text-muted-foreground', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
20
src/lib/components/ui/sheet/sheet-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col space-y-2 text-center sm:text-left', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
19
src/lib/components/ui/sheet/sheet-overlay.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
bind:ref
|
||||
class={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
16
src/lib/components/ui/sheet/sheet-title.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
class={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
6
src/lib/components/ui/sidebar/constants.ts
Normal file
6
src/lib/components/ui/sidebar/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
79
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
79
src/lib/components/ui/sidebar/context.svelte.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
export type SidebarStateProps = {
|
||||
/**
|
||||
* A getter function that returns the current open state of the sidebar.
|
||||
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
|
||||
* component.
|
||||
*/
|
||||
open: Getter<boolean>;
|
||||
|
||||
/**
|
||||
* A function that sets the open state of the sidebar. To support `bind:open`, we need
|
||||
* a source of truth for changing the open state to ensure it will be synced throughout
|
||||
* the sub-components and any `bind:` references.
|
||||
*/
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
class SidebarState {
|
||||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
setOpen: SidebarStateProps['setOpen'];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
||||
|
||||
constructor(props: SidebarStateProps) {
|
||||
this.setOpen = props.setOpen;
|
||||
this.#isMobile = new IsMobile();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
// Convenience getter for checking if the sidebar is mobile
|
||||
// without this, we would need to use `sidebar.isMobile.current` everywhere
|
||||
get isMobile() {
|
||||
return this.#isMobile.current;
|
||||
}
|
||||
|
||||
// Event handler to apply to the `<svelte:window>`
|
||||
handleShortcutKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
setOpenMobile = (value: boolean) => {
|
||||
this.openMobile = value;
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open);
|
||||
};
|
||||
}
|
||||
|
||||
const SYMBOL_KEY = 'scn-sidebar';
|
||||
|
||||
/**
|
||||
* Instantiates a new `SidebarState` instance and sets it in the context.
|
||||
*
|
||||
* @param props The constructor props for the `SidebarState` class.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function setSidebar(props: SidebarStateProps): SidebarState {
|
||||
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the `SidebarState` instance from the context. This is a class instance,
|
||||
* so you cannot destructure it.
|
||||
* @returns The `SidebarState` instance.
|
||||
*/
|
||||
export function useSidebar(): SidebarState {
|
||||
return getContext(Symbol.for(SYMBOL_KEY));
|
||||
}
|
||||
75
src/lib/components/ui/sidebar/index.ts
Normal file
75
src/lib/components/ui/sidebar/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
import Content from './sidebar-content.svelte';
|
||||
import Footer from './sidebar-footer.svelte';
|
||||
import GroupAction from './sidebar-group-action.svelte';
|
||||
import GroupContent from './sidebar-group-content.svelte';
|
||||
import GroupLabel from './sidebar-group-label.svelte';
|
||||
import Group from './sidebar-group.svelte';
|
||||
import Header from './sidebar-header.svelte';
|
||||
import Input from './sidebar-input.svelte';
|
||||
import Inset from './sidebar-inset.svelte';
|
||||
import MenuAction from './sidebar-menu-action.svelte';
|
||||
import MenuBadge from './sidebar-menu-badge.svelte';
|
||||
import MenuButton from './sidebar-menu-button.svelte';
|
||||
import MenuItem from './sidebar-menu-item.svelte';
|
||||
import MenuSkeleton from './sidebar-menu-skeleton.svelte';
|
||||
import MenuSubButton from './sidebar-menu-sub-button.svelte';
|
||||
import MenuSubItem from './sidebar-menu-sub-item.svelte';
|
||||
import MenuSub from './sidebar-menu-sub.svelte';
|
||||
import Menu from './sidebar-menu.svelte';
|
||||
import Provider from './sidebar-provider.svelte';
|
||||
import Rail from './sidebar-rail.svelte';
|
||||
import Separator from './sidebar-separator.svelte';
|
||||
import Trigger from './sidebar-trigger.svelte';
|
||||
import Root from './sidebar.svelte';
|
||||
|
||||
export {
|
||||
Content,
|
||||
Footer,
|
||||
Group,
|
||||
GroupAction,
|
||||
GroupContent,
|
||||
GroupLabel,
|
||||
Header,
|
||||
Input,
|
||||
Inset,
|
||||
Menu,
|
||||
MenuAction,
|
||||
MenuBadge,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuSkeleton,
|
||||
MenuSub,
|
||||
MenuSubButton,
|
||||
MenuSubItem,
|
||||
Provider,
|
||||
Rail,
|
||||
Root,
|
||||
Separator,
|
||||
//
|
||||
Root as Sidebar,
|
||||
Content as SidebarContent,
|
||||
Footer as SidebarFooter,
|
||||
Group as SidebarGroup,
|
||||
GroupAction as SidebarGroupAction,
|
||||
GroupContent as SidebarGroupContent,
|
||||
GroupLabel as SidebarGroupLabel,
|
||||
Header as SidebarHeader,
|
||||
Input as SidebarInput,
|
||||
Inset as SidebarInset,
|
||||
Menu as SidebarMenu,
|
||||
MenuAction as SidebarMenuAction,
|
||||
MenuBadge as SidebarMenuBadge,
|
||||
MenuButton as SidebarMenuButton,
|
||||
MenuItem as SidebarMenuItem,
|
||||
MenuSkeleton as SidebarMenuSkeleton,
|
||||
MenuSub as SidebarMenuSub,
|
||||
MenuSubButton as SidebarMenuSubButton,
|
||||
MenuSubItem as SidebarMenuSubItem,
|
||||
Provider as SidebarProvider,
|
||||
Rail as SidebarRail,
|
||||
Separator as SidebarSeparator,
|
||||
Trigger as SidebarTrigger,
|
||||
Trigger,
|
||||
useSidebar
|
||||
};
|
||||
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-content.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="content"
|
||||
class={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-footer.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="footer"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-group-action.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const propObj = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 after:md:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className
|
||||
),
|
||||
'data-sidebar': 'group-action',
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: propObj })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...propObj}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group-content.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="group-content"
|
||||
class={cn('w-full text-sm', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-group-label.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
||||
className
|
||||
),
|
||||
'data-sidebar': 'group-label',
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-group.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="group"
|
||||
class={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-header.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="header"
|
||||
class={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
23
src/lib/components/ui/sidebar/sidebar-input.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Input> = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
bind:ref
|
||||
bind:value
|
||||
data-sidebar="input"
|
||||
class={cn(
|
||||
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
24
src/lib/components/ui/sidebar/sidebar-inset.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<main
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'relative flex min-h-svh flex-1 flex-col bg-background',
|
||||
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-action.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showOnHover = false,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLButtonAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
showOnHover?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 after:md:hidden',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
showOnHover &&
|
||||
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
|
||||
className
|
||||
),
|
||||
'data-sidebar': 'menu-action',
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
29
src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-badge"
|
||||
class={cn(
|
||||
'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
95
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
95
src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const sidebarMenuButtonVariants = tv({
|
||||
base: 'peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]'
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-sm',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type SidebarMenuButtonVariant = VariantProps<typeof sidebarMenuButtonVariants>['variant'];
|
||||
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>['size'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import type { ComponentProps, Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
child,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
isActive = false,
|
||||
tooltipContent,
|
||||
tooltipContentProps,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
|
||||
isActive?: boolean;
|
||||
variant?: SidebarMenuButtonVariant;
|
||||
size?: SidebarMenuButtonSize;
|
||||
tooltipContent?: Snippet;
|
||||
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
const buttonProps = $derived({
|
||||
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
||||
'data-sidebar': 'menu-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet Button({ props }: { props?: Record<string, unknown> })}
|
||||
{@const mergedProps = mergeProps(buttonProps, props)}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if !tooltipContent}
|
||||
{@render Button({})}
|
||||
{:else}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
{@render Button({ props })}
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={sidebar.state !== 'collapsed' || sidebar.isMobile}
|
||||
children={tooltipContent}
|
||||
{...tooltipContentProps}
|
||||
/>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu-item.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-item"
|
||||
class={cn('group/menu-item relative', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
showIcon = false,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
||||
showIcon?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Random width between 50% and 90%
|
||||
const width = `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
class={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if showIcon}
|
||||
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
||||
{/if}
|
||||
<Skeleton
|
||||
class="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style="--skeleton-width: {width};"
|
||||
/>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
43
src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
class: className,
|
||||
size = 'md',
|
||||
isActive,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
} = $props();
|
||||
|
||||
const mergedProps = $derived({
|
||||
class: cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||
size === 'sm' && 'text-xs',
|
||||
size === 'md' && 'text-sm',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className
|
||||
),
|
||||
'data-sidebar': 'menu-sub-button',
|
||||
'data-size': size,
|
||||
'data-active': isActive,
|
||||
...restProps
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<a bind:this={ref} {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
14
src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-sidebar="menu-sub-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
25
src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-sidebar="menu-sub"
|
||||
class={cn(
|
||||
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
21
src/lib/components/ui/sidebar/sidebar-menu.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-sidebar="menu"
|
||||
class={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
59
src/lib/components/ui/sidebar/sidebar-provider.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import {
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON
|
||||
} from './constants.js';
|
||||
import { setSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
open = $bindable(true),
|
||||
onOpenChange = () => {},
|
||||
controlledOpen = false,
|
||||
class: className,
|
||||
style,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
controlledOpen?: boolean;
|
||||
} = $props();
|
||||
|
||||
const sidebar = setSidebar({
|
||||
open: () => open,
|
||||
setOpen: (value: boolean) => {
|
||||
if (controlledOpen) {
|
||||
onOpenChange(value);
|
||||
} else {
|
||||
open = value;
|
||||
onOpenChange(value);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
||||
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<div
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||
class={cn(
|
||||
'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
|
||||
className
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
36
src/lib/components/ui/sidebar/sidebar-rail.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onclick={() => sidebar.toggle()}
|
||||
title="Toggle Sidebar"
|
||||
class={cn(
|
||||
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
18
src/lib/components/ui/sidebar/sidebar-separator.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Separator> = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-sidebar="separator"
|
||||
class={cn('mx-2 w-auto bg-sidebar-border', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
34
src/lib/components/ui/sidebar/sidebar-trigger.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import PanelLeft from 'lucide-svelte/icons/panel-left';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
onclick,
|
||||
...restProps
|
||||
}: ComponentProps<typeof Button> & {
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
onclick?.(e);
|
||||
sidebar.toggle();
|
||||
}}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('h-7 w-7', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span class="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
98
src/lib/components/ui/sidebar/sidebar.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { SIDEBAR_WIDTH_MOBILE } from './constants.js';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
</script>
|
||||
|
||||
{#if collapsible === 'none'}
|
||||
<div
|
||||
class={cn(
|
||||
'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
|
||||
className
|
||||
)}
|
||||
bind:this={ref}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else if sidebar.isMobile}
|
||||
<Sheet.Root
|
||||
controlledOpen
|
||||
open={sidebar.openMobile}
|
||||
onOpenChange={sidebar.setOpenMobile}
|
||||
{...restProps}
|
||||
>
|
||||
<Sheet.Content
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
|
||||
{side}
|
||||
>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={sidebar.state}
|
||||
data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
class={cn(
|
||||
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
class={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
class="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
7
src/lib/components/ui/skeleton/index.ts
Normal file
7
src/lib/components/ui/skeleton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './skeleton.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Skeleton
|
||||
};
|
||||
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
17
src/lib/components/ui/skeleton/skeleton.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef, WithoutChildren } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('animate-pulse rounded-md bg-primary/10', className)}
|
||||
{...restProps}
|
||||
></div>
|
||||
7
src/lib/components/ui/slider/index.ts
Normal file
7
src/lib/components/ui/slider/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './slider.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Slider
|
||||
};
|
||||
32
src/lib/components/ui/slider/slider.svelte
Normal file
32
src/lib/components/ui/slider/slider.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Slider as SliderPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable([0]),
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SliderPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
class={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ thumbs })}
|
||||
<span class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range class="absolute h-full bg-primary" />
|
||||
</span>
|
||||
{#each thumbs as thumb}
|
||||
<SliderPrimitive.Thumb
|
||||
index={thumb}
|
||||
class="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SliderPrimitive.Root>
|
||||
18
src/lib/components/ui/tooltip/index.ts
Normal file
18
src/lib/components/ui/tooltip/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import Content from './tooltip-content.svelte';
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Trigger = TooltipPrimitive.Trigger;
|
||||
const Provider = TooltipPrimitive.Provider;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Trigger,
|
||||
Content,
|
||||
Provider,
|
||||
//
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider
|
||||
};
|
||||
21
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal file
21
src/lib/components/ui/tooltip/tooltip-content.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
...restProps
|
||||
}: TooltipPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
3
src/lib/covers.ts
Normal file
3
src/lib/covers.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getCoverUrl(songHash: string) {
|
||||
return `http://localhost:39994/${songHash}.webp`;
|
||||
}
|
||||
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
27
src/lib/hooks/is-mobile.svelte.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export class IsMobile {
|
||||
#current = $state<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
$effect(() => {
|
||||
return untrack(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
|
||||
};
|
||||
mql.addEventListener('change', onChange);
|
||||
onChange();
|
||||
return () => {
|
||||
mql.removeEventListener('change', onChange);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
}
|
||||
87
src/lib/proto/google/protobuf/empty.ts
Normal file
87
src/lib/proto/google/protobuf/empty.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "google/protobuf/empty.proto" (package "google.protobuf", syntax proto3)
|
||||
// tslint:disable
|
||||
//
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
import type { BinaryWriteOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryWriter } from '@protobuf-ts/runtime';
|
||||
import { UnknownFieldHandler } from '@protobuf-ts/runtime';
|
||||
import type { BinaryReadOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryReader } from '@protobuf-ts/runtime';
|
||||
import type { PartialMessage } from '@protobuf-ts/runtime';
|
||||
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
||||
import { MessageType } from '@protobuf-ts/runtime';
|
||||
/**
|
||||
* A generic empty message that you can re-use to avoid defining duplicated
|
||||
* empty messages in your APIs. A typical example is to use it as the request
|
||||
* or the response type of an API method. For instance:
|
||||
*
|
||||
* service Foo {
|
||||
* rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @generated from protobuf message google.protobuf.Empty
|
||||
*/
|
||||
export interface Empty {}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class Empty$Type extends MessageType<Empty> {
|
||||
constructor() {
|
||||
super('google.protobuf.Empty', []);
|
||||
}
|
||||
create(value?: PartialMessage<Empty>): Empty {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
if (value !== undefined) reflectionMergePartial<Empty>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: Empty
|
||||
): Empty {
|
||||
return target ?? this.create();
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: Empty,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message google.protobuf.Empty
|
||||
*/
|
||||
export const Empty = new Empty$Type();
|
||||
37
src/lib/proto/library.client.ts
Normal file
37
src/lib/proto/library.client.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "library.proto" (package "library", syntax proto3)
|
||||
// tslint:disable
|
||||
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
||||
import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
|
||||
import { Library } from './library';
|
||||
import { stackIntercept } from '@protobuf-ts/runtime-rpc';
|
||||
import type { TrackList } from './library';
|
||||
import type { Empty } from './google/protobuf/empty';
|
||||
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
|
||||
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
||||
/**
|
||||
* @generated from protobuf service library.Library
|
||||
*/
|
||||
export interface ILibraryClient {
|
||||
/**
|
||||
* @generated from protobuf rpc: ListTracks(google.protobuf.Empty) returns (library.TrackList);
|
||||
*/
|
||||
listTracks(input: Empty, options?: RpcOptions): UnaryCall<Empty, TrackList>;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf service library.Library
|
||||
*/
|
||||
export class LibraryClient implements ILibraryClient, ServiceInfo {
|
||||
typeName = Library.typeName;
|
||||
methods = Library.methods;
|
||||
options = Library.options;
|
||||
constructor(private readonly _transport: RpcTransport) {}
|
||||
/**
|
||||
* @generated from protobuf rpc: ListTracks(google.protobuf.Empty) returns (library.TrackList);
|
||||
*/
|
||||
listTracks(input: Empty, options?: RpcOptions): UnaryCall<Empty, TrackList> {
|
||||
const method = this.methods[0],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<Empty, TrackList>('unary', this._transport, method, opt, input);
|
||||
}
|
||||
}
|
||||
207
src/lib/proto/library.ts
Normal file
207
src/lib/proto/library.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "library.proto" (package "library", syntax proto3)
|
||||
// tslint:disable
|
||||
import { Empty } from './google/protobuf/empty';
|
||||
import { ServiceType } from '@protobuf-ts/runtime-rpc';
|
||||
import type { BinaryWriteOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryWriter } from '@protobuf-ts/runtime';
|
||||
import { WireType } from '@protobuf-ts/runtime';
|
||||
import type { BinaryReadOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryReader } from '@protobuf-ts/runtime';
|
||||
import { UnknownFieldHandler } from '@protobuf-ts/runtime';
|
||||
import type { PartialMessage } from '@protobuf-ts/runtime';
|
||||
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
||||
import { MessageType } from '@protobuf-ts/runtime';
|
||||
/**
|
||||
* @generated from protobuf message library.TrackList
|
||||
*/
|
||||
export interface TrackList {
|
||||
/**
|
||||
* @generated from protobuf field: repeated library.Track tracks = 1;
|
||||
*/
|
||||
tracks: Track[];
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message library.Track
|
||||
*/
|
||||
export interface Track {
|
||||
/**
|
||||
* @generated from protobuf field: string hash = 1;
|
||||
*/
|
||||
hash: string;
|
||||
/**
|
||||
* @generated from protobuf field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @generated from protobuf field: string artist_name = 3;
|
||||
*/
|
||||
artistName: string;
|
||||
/**
|
||||
* @generated from protobuf field: uint64 artist_id = 4;
|
||||
*/
|
||||
artistId: bigint;
|
||||
}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class TrackList$Type extends MessageType<TrackList> {
|
||||
constructor() {
|
||||
super('library.TrackList', [
|
||||
{ no: 1, name: 'tracks', kind: 'message', repeat: 1 /*RepeatType.PACKED*/, T: () => Track }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<TrackList>): TrackList {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.tracks = [];
|
||||
if (value !== undefined) reflectionMergePartial<TrackList>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: TrackList
|
||||
): TrackList {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* repeated library.Track tracks */ 1:
|
||||
message.tracks.push(Track.internalBinaryRead(reader, reader.uint32(), options));
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: TrackList,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* repeated library.Track tracks = 1; */
|
||||
for (let i = 0; i < message.tracks.length; i++)
|
||||
Track.internalBinaryWrite(
|
||||
message.tracks[i],
|
||||
writer.tag(1, WireType.LengthDelimited).fork(),
|
||||
options
|
||||
).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message library.TrackList
|
||||
*/
|
||||
export const TrackList = new TrackList$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class Track$Type extends MessageType<Track> {
|
||||
constructor() {
|
||||
super('library.Track', [
|
||||
{ no: 1, name: 'hash', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 2, name: 'name', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 3, name: 'artist_name', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
||||
{
|
||||
no: 4,
|
||||
name: 'artist_id',
|
||||
kind: 'scalar',
|
||||
T: 4 /*ScalarType.UINT64*/,
|
||||
L: 0 /*LongType.BIGINT*/
|
||||
}
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<Track>): Track {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.hash = '';
|
||||
message.name = '';
|
||||
message.artistName = '';
|
||||
message.artistId = 0n;
|
||||
if (value !== undefined) reflectionMergePartial<Track>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: Track
|
||||
): Track {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string hash */ 1:
|
||||
message.hash = reader.string();
|
||||
break;
|
||||
case /* string name */ 2:
|
||||
message.name = reader.string();
|
||||
break;
|
||||
case /* string artist_name */ 3:
|
||||
message.artistName = reader.string();
|
||||
break;
|
||||
case /* uint64 artist_id */ 4:
|
||||
message.artistId = reader.uint64().toBigInt();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: Track,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* string hash = 1; */
|
||||
if (message.hash !== '') writer.tag(1, WireType.LengthDelimited).string(message.hash);
|
||||
/* string name = 2; */
|
||||
if (message.name !== '') writer.tag(2, WireType.LengthDelimited).string(message.name);
|
||||
/* string artist_name = 3; */
|
||||
if (message.artistName !== '')
|
||||
writer.tag(3, WireType.LengthDelimited).string(message.artistName);
|
||||
/* uint64 artist_id = 4; */
|
||||
if (message.artistId !== 0n) writer.tag(4, WireType.Varint).uint64(message.artistId);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message library.Track
|
||||
*/
|
||||
export const Track = new Track$Type();
|
||||
/**
|
||||
* @generated ServiceType for protobuf service library.Library
|
||||
*/
|
||||
export const Library = new ServiceType('library.Library', [
|
||||
{ name: 'ListTracks', options: {}, I: Empty, O: TrackList }
|
||||
]);
|
||||
74
src/lib/proto/player.client.ts
Normal file
74
src/lib/proto/player.client.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "player.proto" (package "player", syntax proto3)
|
||||
// tslint:disable
|
||||
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
||||
import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
|
||||
import { Player } from './player';
|
||||
import type { Empty } from './google/protobuf/empty';
|
||||
import { stackIntercept } from '@protobuf-ts/runtime-rpc';
|
||||
import type { PlayTrackResponse } from './player';
|
||||
import type { PlayTrackRequest } from './player';
|
||||
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
|
||||
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
||||
/**
|
||||
* @generated from protobuf service player.Player
|
||||
*/
|
||||
export interface IPlayerClient {
|
||||
/**
|
||||
* @generated from protobuf rpc: PlayTrack(player.PlayTrackRequest) returns (player.PlayTrackResponse);
|
||||
*/
|
||||
playTrack(
|
||||
input: PlayTrackRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<PlayTrackRequest, PlayTrackResponse>;
|
||||
/**
|
||||
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
*/
|
||||
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty>;
|
||||
/**
|
||||
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
*/
|
||||
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty>;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf service player.Player
|
||||
*/
|
||||
export class PlayerClient implements IPlayerClient, ServiceInfo {
|
||||
typeName = Player.typeName;
|
||||
methods = Player.methods;
|
||||
options = Player.options;
|
||||
constructor(private readonly _transport: RpcTransport) {}
|
||||
/**
|
||||
* @generated from protobuf rpc: PlayTrack(player.PlayTrackRequest) returns (player.PlayTrackResponse);
|
||||
*/
|
||||
playTrack(
|
||||
input: PlayTrackRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<PlayTrackRequest, PlayTrackResponse> {
|
||||
const method = this.methods[0],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<PlayTrackRequest, PlayTrackResponse>(
|
||||
'unary',
|
||||
this._transport,
|
||||
method,
|
||||
opt,
|
||||
input
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
*/
|
||||
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty> {
|
||||
const method = this.methods[1],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<Empty, Empty>('unary', this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
*/
|
||||
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty> {
|
||||
const method = this.methods[2],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<Empty, Empty>('unary', this._transport, method, opt, input);
|
||||
}
|
||||
}
|
||||
129
src/lib/proto/player.ts
Normal file
129
src/lib/proto/player.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "player.proto" (package "player", syntax proto3)
|
||||
// tslint:disable
|
||||
import { Empty } from './google/protobuf/empty';
|
||||
import { ServiceType } from '@protobuf-ts/runtime-rpc';
|
||||
import type { BinaryWriteOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryWriter } from '@protobuf-ts/runtime';
|
||||
import { WireType } from '@protobuf-ts/runtime';
|
||||
import type { BinaryReadOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryReader } from '@protobuf-ts/runtime';
|
||||
import { UnknownFieldHandler } from '@protobuf-ts/runtime';
|
||||
import type { PartialMessage } from '@protobuf-ts/runtime';
|
||||
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
||||
import { MessageType } from '@protobuf-ts/runtime';
|
||||
/**
|
||||
* @generated from protobuf message player.PlayTrackRequest
|
||||
*/
|
||||
export interface PlayTrackRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string hash = 1;
|
||||
*/
|
||||
hash: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message player.PlayTrackResponse
|
||||
*/
|
||||
export interface PlayTrackResponse {}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class PlayTrackRequest$Type extends MessageType<PlayTrackRequest> {
|
||||
constructor() {
|
||||
super('player.PlayTrackRequest', [
|
||||
{ no: 1, name: 'hash', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<PlayTrackRequest>): PlayTrackRequest {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.hash = '';
|
||||
if (value !== undefined) reflectionMergePartial<PlayTrackRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: PlayTrackRequest
|
||||
): PlayTrackRequest {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string hash */ 1:
|
||||
message.hash = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: PlayTrackRequest,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* string hash = 1; */
|
||||
if (message.hash !== '') writer.tag(1, WireType.LengthDelimited).string(message.hash);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message player.PlayTrackRequest
|
||||
*/
|
||||
export const PlayTrackRequest = new PlayTrackRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class PlayTrackResponse$Type extends MessageType<PlayTrackResponse> {
|
||||
constructor() {
|
||||
super('player.PlayTrackResponse', []);
|
||||
}
|
||||
create(value?: PartialMessage<PlayTrackResponse>): PlayTrackResponse {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
if (value !== undefined) reflectionMergePartial<PlayTrackResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: PlayTrackResponse
|
||||
): PlayTrackResponse {
|
||||
return target ?? this.create();
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: PlayTrackResponse,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message player.PlayTrackResponse
|
||||
*/
|
||||
export const PlayTrackResponse = new PlayTrackResponse$Type();
|
||||
/**
|
||||
* @generated ServiceType for protobuf service player.Player
|
||||
*/
|
||||
export const Player = new ServiceType('player.Player', [
|
||||
{ name: 'PlayTrack', options: {}, I: PlayTrackRequest, O: PlayTrackResponse },
|
||||
{ name: 'ResumeTrack', options: {}, I: Empty, O: Empty },
|
||||
{ name: 'PauseTrack', options: {}, I: Empty, O: Empty }
|
||||
]);
|
||||
109
src/lib/proto/settings.client.ts
Normal file
109
src/lib/proto/settings.client.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "settings.proto" (package "settings", syntax proto3)
|
||||
// tslint:disable
|
||||
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
||||
import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
|
||||
import { Settings } from './settings';
|
||||
import type { RefreshPathResponse } from './settings';
|
||||
import type { RefreshPathRequest } from './settings';
|
||||
import type { DeletePathResponse } from './settings';
|
||||
import type { DeletePathRequest } from './settings';
|
||||
import type { AddPathResponse } from './settings';
|
||||
import type { AddPathRequest } from './settings';
|
||||
import { stackIntercept } from '@protobuf-ts/runtime-rpc';
|
||||
import type { SettingsData } from './settings';
|
||||
import type { Empty } from './google/protobuf/empty';
|
||||
import type { UnaryCall } from '@protobuf-ts/runtime-rpc';
|
||||
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
||||
/**
|
||||
* @generated from protobuf service settings.Settings
|
||||
*/
|
||||
export interface ISettingsClient {
|
||||
/**
|
||||
* @generated from protobuf rpc: ListPaths(google.protobuf.Empty) returns (settings.SettingsData);
|
||||
*/
|
||||
listPaths(input: Empty, options?: RpcOptions): UnaryCall<Empty, SettingsData>;
|
||||
/**
|
||||
* @generated from protobuf rpc: AddPath(settings.AddPathRequest) returns (settings.AddPathResponse);
|
||||
*/
|
||||
addPath(input: AddPathRequest, options?: RpcOptions): UnaryCall<AddPathRequest, AddPathResponse>;
|
||||
/**
|
||||
* @generated from protobuf rpc: DeletePath(settings.DeletePathRequest) returns (settings.DeletePathResponse);
|
||||
*/
|
||||
deletePath(
|
||||
input: DeletePathRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<DeletePathRequest, DeletePathResponse>;
|
||||
/**
|
||||
* @generated from protobuf rpc: RefreshPath(settings.RefreshPathRequest) returns (settings.RefreshPathResponse);
|
||||
*/
|
||||
refreshPath(
|
||||
input: RefreshPathRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<RefreshPathRequest, RefreshPathResponse>;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf service settings.Settings
|
||||
*/
|
||||
export class SettingsClient implements ISettingsClient, ServiceInfo {
|
||||
typeName = Settings.typeName;
|
||||
methods = Settings.methods;
|
||||
options = Settings.options;
|
||||
constructor(private readonly _transport: RpcTransport) {}
|
||||
/**
|
||||
* @generated from protobuf rpc: ListPaths(google.protobuf.Empty) returns (settings.SettingsData);
|
||||
*/
|
||||
listPaths(input: Empty, options?: RpcOptions): UnaryCall<Empty, SettingsData> {
|
||||
const method = this.methods[0],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<Empty, SettingsData>('unary', this._transport, method, opt, input);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: AddPath(settings.AddPathRequest) returns (settings.AddPathResponse);
|
||||
*/
|
||||
addPath(input: AddPathRequest, options?: RpcOptions): UnaryCall<AddPathRequest, AddPathResponse> {
|
||||
const method = this.methods[1],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<AddPathRequest, AddPathResponse>(
|
||||
'unary',
|
||||
this._transport,
|
||||
method,
|
||||
opt,
|
||||
input
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: DeletePath(settings.DeletePathRequest) returns (settings.DeletePathResponse);
|
||||
*/
|
||||
deletePath(
|
||||
input: DeletePathRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<DeletePathRequest, DeletePathResponse> {
|
||||
const method = this.methods[2],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<DeletePathRequest, DeletePathResponse>(
|
||||
'unary',
|
||||
this._transport,
|
||||
method,
|
||||
opt,
|
||||
input
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf rpc: RefreshPath(settings.RefreshPathRequest) returns (settings.RefreshPathResponse);
|
||||
*/
|
||||
refreshPath(
|
||||
input: RefreshPathRequest,
|
||||
options?: RpcOptions
|
||||
): UnaryCall<RefreshPathRequest, RefreshPathResponse> {
|
||||
const method = this.methods[3],
|
||||
opt = this._transport.mergeOptions(options);
|
||||
return stackIntercept<RefreshPathRequest, RefreshPathResponse>(
|
||||
'unary',
|
||||
this._transport,
|
||||
method,
|
||||
opt,
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
545
src/lib/proto/settings.ts
Normal file
545
src/lib/proto/settings.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
// @generated by protobuf-ts 2.9.4
|
||||
// @generated from protobuf file "settings.proto" (package "settings", syntax proto3)
|
||||
// tslint:disable
|
||||
import { Empty } from './google/protobuf/empty';
|
||||
import { ServiceType } from '@protobuf-ts/runtime-rpc';
|
||||
import type { BinaryWriteOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryWriter } from '@protobuf-ts/runtime';
|
||||
import { WireType } from '@protobuf-ts/runtime';
|
||||
import type { BinaryReadOptions } from '@protobuf-ts/runtime';
|
||||
import type { IBinaryReader } from '@protobuf-ts/runtime';
|
||||
import { UnknownFieldHandler } from '@protobuf-ts/runtime';
|
||||
import type { PartialMessage } from '@protobuf-ts/runtime';
|
||||
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
||||
import { MessageType } from '@protobuf-ts/runtime';
|
||||
/**
|
||||
* @generated from protobuf message settings.SettingsData
|
||||
*/
|
||||
export interface SettingsData {
|
||||
/**
|
||||
* @generated from protobuf field: repeated settings.LibraryPath library_paths = 1;
|
||||
*/
|
||||
libraryPaths: LibraryPath[];
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.LibraryPath
|
||||
*/
|
||||
export interface LibraryPath {
|
||||
/**
|
||||
* @generated from protobuf field: uint64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
/**
|
||||
* @generated from protobuf field: string path = 2;
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.AddPathRequest
|
||||
*/
|
||||
export interface AddPathRequest {
|
||||
/**
|
||||
* @generated from protobuf field: string path = 1;
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.AddPathResponse
|
||||
*/
|
||||
export interface AddPathResponse {
|
||||
/**
|
||||
* @generated from protobuf field: uint64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.DeletePathRequest
|
||||
*/
|
||||
export interface DeletePathRequest {
|
||||
/**
|
||||
* @generated from protobuf field: uint64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.DeletePathResponse
|
||||
*/
|
||||
export interface DeletePathResponse {}
|
||||
/**
|
||||
* @generated from protobuf message settings.RefreshPathRequest
|
||||
*/
|
||||
export interface RefreshPathRequest {
|
||||
/**
|
||||
* @generated from protobuf field: uint64 id = 1;
|
||||
*/
|
||||
id: bigint;
|
||||
}
|
||||
/**
|
||||
* @generated from protobuf message settings.RefreshPathResponse
|
||||
*/
|
||||
export interface RefreshPathResponse {}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class SettingsData$Type extends MessageType<SettingsData> {
|
||||
constructor() {
|
||||
super('settings.SettingsData', [
|
||||
{
|
||||
no: 1,
|
||||
name: 'library_paths',
|
||||
kind: 'message',
|
||||
repeat: 1 /*RepeatType.PACKED*/,
|
||||
T: () => LibraryPath
|
||||
}
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<SettingsData>): SettingsData {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.libraryPaths = [];
|
||||
if (value !== undefined) reflectionMergePartial<SettingsData>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: SettingsData
|
||||
): SettingsData {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* repeated settings.LibraryPath library_paths */ 1:
|
||||
message.libraryPaths.push(
|
||||
LibraryPath.internalBinaryRead(reader, reader.uint32(), options)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: SettingsData,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* repeated settings.LibraryPath library_paths = 1; */
|
||||
for (let i = 0; i < message.libraryPaths.length; i++)
|
||||
LibraryPath.internalBinaryWrite(
|
||||
message.libraryPaths[i],
|
||||
writer.tag(1, WireType.LengthDelimited).fork(),
|
||||
options
|
||||
).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.SettingsData
|
||||
*/
|
||||
export const SettingsData = new SettingsData$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class LibraryPath$Type extends MessageType<LibraryPath> {
|
||||
constructor() {
|
||||
super('settings.LibraryPath', [
|
||||
{ no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
|
||||
{ no: 2, name: 'path', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<LibraryPath>): LibraryPath {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.id = 0n;
|
||||
message.path = '';
|
||||
if (value !== undefined) reflectionMergePartial<LibraryPath>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: LibraryPath
|
||||
): LibraryPath {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* uint64 id */ 1:
|
||||
message.id = reader.uint64().toBigInt();
|
||||
break;
|
||||
case /* string path */ 2:
|
||||
message.path = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: LibraryPath,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* uint64 id = 1; */
|
||||
if (message.id !== 0n) writer.tag(1, WireType.Varint).uint64(message.id);
|
||||
/* string path = 2; */
|
||||
if (message.path !== '') writer.tag(2, WireType.LengthDelimited).string(message.path);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.LibraryPath
|
||||
*/
|
||||
export const LibraryPath = new LibraryPath$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class AddPathRequest$Type extends MessageType<AddPathRequest> {
|
||||
constructor() {
|
||||
super('settings.AddPathRequest', [
|
||||
{ no: 1, name: 'path', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<AddPathRequest>): AddPathRequest {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.path = '';
|
||||
if (value !== undefined) reflectionMergePartial<AddPathRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: AddPathRequest
|
||||
): AddPathRequest {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string path */ 1:
|
||||
message.path = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: AddPathRequest,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* string path = 1; */
|
||||
if (message.path !== '') writer.tag(1, WireType.LengthDelimited).string(message.path);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.AddPathRequest
|
||||
*/
|
||||
export const AddPathRequest = new AddPathRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class AddPathResponse$Type extends MessageType<AddPathResponse> {
|
||||
constructor() {
|
||||
super('settings.AddPathResponse', [
|
||||
{ no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<AddPathResponse>): AddPathResponse {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.id = 0n;
|
||||
if (value !== undefined) reflectionMergePartial<AddPathResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: AddPathResponse
|
||||
): AddPathResponse {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* uint64 id */ 1:
|
||||
message.id = reader.uint64().toBigInt();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: AddPathResponse,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* uint64 id = 1; */
|
||||
if (message.id !== 0n) writer.tag(1, WireType.Varint).uint64(message.id);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.AddPathResponse
|
||||
*/
|
||||
export const AddPathResponse = new AddPathResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class DeletePathRequest$Type extends MessageType<DeletePathRequest> {
|
||||
constructor() {
|
||||
super('settings.DeletePathRequest', [
|
||||
{ no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<DeletePathRequest>): DeletePathRequest {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.id = 0n;
|
||||
if (value !== undefined) reflectionMergePartial<DeletePathRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: DeletePathRequest
|
||||
): DeletePathRequest {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* uint64 id */ 1:
|
||||
message.id = reader.uint64().toBigInt();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: DeletePathRequest,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* uint64 id = 1; */
|
||||
if (message.id !== 0n) writer.tag(1, WireType.Varint).uint64(message.id);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.DeletePathRequest
|
||||
*/
|
||||
export const DeletePathRequest = new DeletePathRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class DeletePathResponse$Type extends MessageType<DeletePathResponse> {
|
||||
constructor() {
|
||||
super('settings.DeletePathResponse', []);
|
||||
}
|
||||
create(value?: PartialMessage<DeletePathResponse>): DeletePathResponse {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
if (value !== undefined) reflectionMergePartial<DeletePathResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: DeletePathResponse
|
||||
): DeletePathResponse {
|
||||
return target ?? this.create();
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: DeletePathResponse,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.DeletePathResponse
|
||||
*/
|
||||
export const DeletePathResponse = new DeletePathResponse$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class RefreshPathRequest$Type extends MessageType<RefreshPathRequest> {
|
||||
constructor() {
|
||||
super('settings.RefreshPathRequest', [
|
||||
{ no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<RefreshPathRequest>): RefreshPathRequest {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
message.id = 0n;
|
||||
if (value !== undefined) reflectionMergePartial<RefreshPathRequest>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: RefreshPathRequest
|
||||
): RefreshPathRequest {
|
||||
let message = target ?? this.create(),
|
||||
end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* uint64 id */ 1:
|
||||
message.id = reader.uint64().toBigInt();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === 'throw')
|
||||
throw new globalThis.Error(
|
||||
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||
);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||
this.typeName,
|
||||
message,
|
||||
fieldNo,
|
||||
wireType,
|
||||
d
|
||||
);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: RefreshPathRequest,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
/* uint64 id = 1; */
|
||||
if (message.id !== 0n) writer.tag(1, WireType.Varint).uint64(message.id);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.RefreshPathRequest
|
||||
*/
|
||||
export const RefreshPathRequest = new RefreshPathRequest$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class RefreshPathResponse$Type extends MessageType<RefreshPathResponse> {
|
||||
constructor() {
|
||||
super('settings.RefreshPathResponse', []);
|
||||
}
|
||||
create(value?: PartialMessage<RefreshPathResponse>): RefreshPathResponse {
|
||||
const message = globalThis.Object.create(this.messagePrototype!);
|
||||
if (value !== undefined) reflectionMergePartial<RefreshPathResponse>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(
|
||||
reader: IBinaryReader,
|
||||
length: number,
|
||||
options: BinaryReadOptions,
|
||||
target?: RefreshPathResponse
|
||||
): RefreshPathResponse {
|
||||
return target ?? this.create();
|
||||
}
|
||||
internalBinaryWrite(
|
||||
message: RefreshPathResponse,
|
||||
writer: IBinaryWriter,
|
||||
options: BinaryWriteOptions
|
||||
): IBinaryWriter {
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message settings.RefreshPathResponse
|
||||
*/
|
||||
export const RefreshPathResponse = new RefreshPathResponse$Type();
|
||||
/**
|
||||
* @generated ServiceType for protobuf service settings.Settings
|
||||
*/
|
||||
export const Settings = new ServiceType('settings.Settings', [
|
||||
{ name: 'ListPaths', options: {}, I: Empty, O: SettingsData },
|
||||
{ name: 'AddPath', options: {}, I: AddPathRequest, O: AddPathResponse },
|
||||
{ name: 'DeletePath', options: {}, I: DeletePathRequest, O: DeletePathResponse },
|
||||
{ name: 'RefreshPath', options: {}, I: RefreshPathRequest, O: RefreshPathResponse }
|
||||
]);
|
||||
6
src/lib/song.ts
Normal file
6
src/lib/song.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Song = {
|
||||
hash: string;
|
||||
name: string;
|
||||
artistName: string;
|
||||
artistId: bigint;
|
||||
};
|
||||
4
src/lib/stores/player.ts
Normal file
4
src/lib/stores/player.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const currentlyPlaying = writable<string | null>(null);
|
||||
export const volume = writable<number>(1.0);
|
||||
8
src/routes/+layout.server.ts
Normal file
8
src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PlayerClient } from '$lib/proto/player.client';
|
||||
import { protoTransport } from '../hooks.server';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
// const client = new PlayerClient(protoTransport);
|
||||
// TODO: Get current song
|
||||
};
|
||||
@@ -1,6 +1,31 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||
import AppSidebar from '$lib/components/groove/AppSidebar.svelte';
|
||||
import Footer from '$lib/components/groove/Footer.svelte';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar />
|
||||
|
||||
<div class="flex max-h-screen flex-auto grow flex-col overflow-hidden">
|
||||
<main class="grow overflow-hidden">
|
||||
<Sidebar.Trigger />
|
||||
<ScrollArea class="flex h-full flex-col overflow-auto">
|
||||
<div class="m-4 flex flex-col">
|
||||
<!-- <Sidebar.Trigger /> -->
|
||||
{@render children()}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Sidebar.Provider>
|
||||
|
||||
<svelte:head><title>Groove</title></svelte:head>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<h1>Home</h1>
|
||||
|
||||
0
src/routes/albums/+page.server.ts
Normal file
0
src/routes/albums/+page.server.ts
Normal file
1
src/routes/albums/+page.svelte
Normal file
1
src/routes/albums/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
Albums
|
||||
16
src/routes/player/+page.server.ts
Normal file
16
src/routes/player/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { PlayerClient } from '$lib/proto/player.client';
|
||||
import { protoTransport } from '../../hooks.server';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions = {
|
||||
resume: async () => {
|
||||
const client = new PlayerClient(protoTransport);
|
||||
|
||||
await client.resumeTrack({});
|
||||
},
|
||||
pause: async () => {
|
||||
const client = new PlayerClient(protoTransport);
|
||||
|
||||
await client.pauseTrack({});
|
||||
}
|
||||
} satisfies Actions;
|
||||
0
src/routes/playlists/+page.server.ts
Normal file
0
src/routes/playlists/+page.server.ts
Normal file
1
src/routes/playlists/+page.svelte
Normal file
1
src/routes/playlists/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
Playlists
|
||||
43
src/routes/settings/+page.server.ts
Normal file
43
src/routes/settings/+page.server.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SettingsClient } from '$lib/proto/settings.client';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { protoTransport } from '../../hooks.server';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ depends }) => {
|
||||
depends('settings:library');
|
||||
|
||||
const client = new SettingsClient(protoTransport);
|
||||
|
||||
const response = await client.listPaths({});
|
||||
|
||||
return {
|
||||
libraryPaths: response.response.libraryPaths.map((p) => ({
|
||||
id: p.id,
|
||||
path: p.path
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
'add-path': async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const path = formData.get('path')?.toString();
|
||||
|
||||
if (!path) {
|
||||
return fail(400, {
|
||||
errors: 'Invalid path'
|
||||
});
|
||||
}
|
||||
|
||||
const client = new SettingsClient(protoTransport);
|
||||
|
||||
const response = await client.addPath({
|
||||
path
|
||||
});
|
||||
|
||||
return {
|
||||
id: response.response.id
|
||||
};
|
||||
}
|
||||
} satisfies Actions;
|
||||
73
src/routes/settings/+page.svelte
Normal file
73
src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Plus, Settings } from 'lucide-svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||
import ThemeSwitcher from '$lib/components/groove/ThemeSwitcher.svelte';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { enhance } from '$app/forms';
|
||||
import { Trash, RefreshCw } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageServerData;
|
||||
}
|
||||
|
||||
let { data: settings }: Props = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="flex scroll-m-20 items-center text-2xl font-semibold tracking-tight">
|
||||
<Settings class="mr-2" />Settings
|
||||
</h3>
|
||||
|
||||
<Separator class="mb-5 mt-3" />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="w-fit space-y-2">
|
||||
<div class="text-lg font-semibold">
|
||||
<p>Appearance</p>
|
||||
<p class="text-sm font-medium leading-none text-muted-foreground">
|
||||
Change the way Groove looks
|
||||
</p>
|
||||
</div>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
|
||||
<div class="w-fit space-y-2">
|
||||
<div class="text-lg font-semibold">
|
||||
<p>Library</p>
|
||||
<p class="text-sm font-medium leading-none text-muted-foreground">
|
||||
Add the directories you want to include in your library
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="flex items-center gap-1" method="POST" action="?/add-path" use:enhance>
|
||||
<Input name="path" placeholder="~/Music" />
|
||||
<Button type="submit" class="aspect-square" variant="outline" size="icon"><Plus /></Button>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each settings.libraryPaths as path}
|
||||
<div class="group flex items-center justify-between pr-0">
|
||||
<span>{path.path}</span>
|
||||
<form class="flex items-center gap-1" method="POST" use:enhance>
|
||||
<Button
|
||||
type="submit"
|
||||
formaction="/settings/path/{path.id}?/refresh"
|
||||
variant="outline"
|
||||
size="icon"><RefreshCw /></Button
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
formaction="/settings/path/{path.id}?/delete"
|
||||
variant="destructive"
|
||||
size="icon"><Trash /></Button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">You have not added a directory yet</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
20
src/routes/settings/path/[id]/+page.server.ts
Normal file
20
src/routes/settings/path/[id]/+page.server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Actions } from './$types';
|
||||
import { SettingsClient } from '$lib/proto/settings.client';
|
||||
import { protoTransport } from '../../../../hooks.server';
|
||||
|
||||
export const actions = {
|
||||
refresh: async ({ params }) => {
|
||||
const client = new SettingsClient(protoTransport);
|
||||
|
||||
await client.refreshPath({
|
||||
id: BigInt(params.id)
|
||||
});
|
||||
},
|
||||
delete: async ({ params }) => {
|
||||
const client = new SettingsClient(protoTransport);
|
||||
|
||||
await client.deletePath({
|
||||
id: BigInt(params.id)
|
||||
});
|
||||
}
|
||||
} satisfies Actions;
|
||||
20
src/routes/songs/+page.server.ts
Normal file
20
src/routes/songs/+page.server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { LibraryClient } from '$lib/proto/library.client';
|
||||
import { protoTransport } from '../../hooks.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const client = new LibraryClient(protoTransport);
|
||||
|
||||
const response = await client.listTracks({});
|
||||
|
||||
const tracks = response.response.tracks.map((t) => ({
|
||||
hash: t.hash,
|
||||
name: t.name,
|
||||
artistId: t.artistId,
|
||||
artistName: t.artistName
|
||||
}));
|
||||
|
||||
return {
|
||||
songs: tracks
|
||||
};
|
||||
};
|
||||
18
src/routes/songs/+page.svelte
Normal file
18
src/routes/songs/+page.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import SongListing from '$lib/components/groove/SongListing.svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageServerData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8"
|
||||
>
|
||||
{#each data.songs as song}
|
||||
<SongListing {song} />
|
||||
{/each}
|
||||
</div>
|
||||
13
src/routes/songs/[hash]/+page.server.ts
Normal file
13
src/routes/songs/[hash]/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { PlayerClient } from '$lib/proto/player.client';
|
||||
import { protoTransport } from '../../../hooks.server';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions = {
|
||||
play: async ({ params }) => {
|
||||
const client = new PlayerClient(protoTransport);
|
||||
|
||||
await client.playTrack({
|
||||
hash: params.hash
|
||||
});
|
||||
}
|
||||
} satisfies Actions;
|
||||
Reference in New Issue
Block a user