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",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"lint": "prettier --check . && eslint ."
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "^1.0.0-next.64",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.7.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
|
"lucide-svelte": "^0.460.1",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
@@ -31,7 +29,12 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
|
||||||
"vite": "^5.0.3"
|
"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 {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--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: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--popover: 0 0% 100%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 221.2 83.2% 53.3%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--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: 210 40% 96.1%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 100% 67%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 100% 5%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--radius: 0.5rem;
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
@@ -32,28 +32,30 @@
|
|||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 210 40% 98%;
|
--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: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--popover-foreground: 210 40% 98%;
|
||||||
--primary: 210 40% 98%;
|
--primary: 217.2 91.2% 59.8%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--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: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 100% 67%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 100% 5%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--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-background: 240 5.9% 10%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
@@ -73,3 +75,7 @@
|
|||||||
@apply bg-background text-foreground;
|
@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">
|
<script lang="ts">
|
||||||
import '../app.css';
|
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();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
<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>
|
<h1>Home</h1>
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
|
||||||
|
|||||||
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