diff --git a/bun.lockb b/bun.lockb
index 4fababa..f4c7ad9 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index a526565..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -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/']
- }
-);
diff --git a/package.json b/package.json
index c10bc5f..f97d1d4 100644
--- a/package.json
+++ b/package.json
@@ -8,19 +8,17 @@
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "format": "prettier --write .",
- "lint": "prettier --check . && eslint ."
+ "format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
+ "bits-ui": "^1.0.0-next.64",
"clsx": "^2.1.1",
- "eslint": "^9.7.0",
- "eslint-config-prettier": "^9.1.0",
- "eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
+ "lucide-svelte": "^0.460.1",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
@@ -31,7 +29,12 @@
"tailwindcss": "^3.4.9",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.0.0",
- "typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
+ },
+ "dependencies": {
+ "@protobuf-ts/grpcweb-transport": "^2.9.4",
+ "@protobuf-ts/plugin": "^2.9.4",
+ "dayjs": "^1.11.13",
+ "mode-watcher": "^0.5.0"
}
}
diff --git a/protos/library.proto b/protos/library.proto
new file mode 100644
index 0000000..3787455
--- /dev/null
+++ b/protos/library.proto
@@ -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;
+}
diff --git a/protos/player.proto b/protos/player.proto
new file mode 100644
index 0000000..837622d
--- /dev/null
+++ b/protos/player.proto
@@ -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 {
+}
diff --git a/protos/settings.proto b/protos/settings.proto
new file mode 100644
index 0000000..727a20a
--- /dev/null
+++ b/protos/settings.proto
@@ -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 {
+}
diff --git a/src/app.css b/src/app.css
index cd4053f..1f1ad5f 100644
--- a/src/app.css
+++ b/src/app.css
@@ -6,24 +6,24 @@
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --primary: 222.2 47.4% 11.2%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 72.2% 50.6%;
- --destructive-foreground: 210 40% 98%;
- --ring: 222.2 84% 4.9%;
- --radius: 0.5rem;
+ --destructive: 0 100% 67%;
+ --destructive-foreground: 0 100% 5%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
@@ -32,28 +32,30 @@
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
- }
+ --radius: 0.5rem;
+ }
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --primary: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 210 40% 98%;
- --ring: 212.7 26.8% 83.9%;
+ --destructive: 0 100% 67%;
+ --destructive-foreground: 0 100% 5%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
@@ -73,3 +75,7 @@
@apply bg-background text-foreground;
}
}
+
+* {
+ -webkit-user-drag: none !important;
+}
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
new file mode 100644
index 0000000..ebfd068
--- /dev/null
+++ b/src/hooks.server.ts
@@ -0,0 +1,5 @@
+import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
+
+export const protoTransport = new GrpcWebFetchTransport({
+ baseUrl: 'http://[::1]:39993'
+});
diff --git a/src/lib/components/groove/AppSidebar.svelte b/src/lib/components/groove/AppSidebar.svelte
new file mode 100644
index 0000000..09c5f7f
--- /dev/null
+++ b/src/lib/components/groove/AppSidebar.svelte
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+ {#snippet child({ props })}
+
+
+ Home
+
+ {/snippet}
+
+
+
+
+
+
+ Library
+
+
+
+ {#snippet child({ props })}
+
+
+ Songs
+
+ {/snippet}
+
+
+
+
+ {#snippet child({ props })}
+
+
+ Albums
+
+ {/snippet}
+
+
+
+
+ {#snippet child({ props })}
+
+
+ Playlists
+
+ {/snippet}
+
+
+
+
+
+
+
+
+
+ {#snippet child({ props })}
+
+
+ Settings
+
+ {/snippet}
+
+
+
+
+
diff --git a/src/lib/components/groove/Footer.svelte b/src/lib/components/groove/Footer.svelte
new file mode 100644
index 0000000..718cacf
--- /dev/null
+++ b/src/lib/components/groove/Footer.svelte
@@ -0,0 +1,160 @@
+
+
+
+
+
+ {#if $currentlyPlaying}
+
+
+
Song name
+
Artist name
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+ {#snippet child({ props })}
+
+
+ 15
+
+ {/snippet}
+
+
+ Next up
+
+
+
+
+ {#each [] as _song, i}
+
skipToQueueIndex(i)}
+ >
+
+
+
+
+
+ Song name
+
+
+ Song artist
+
+
+
+ {/each}
+
+
+
+
+
+
+ {#if $volume <= 0}
+
+ {:else if $volume < 50.0}
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+ {Math.round($volume * 100.0)}%
+
+
+
+
+
+
+
{dayjs.duration(0, 'seconds').format('mm:ss')}
+
+ {#key progressValue}
+
+ {/key}
+
+
{dayjs.duration(0, 'seconds').format('mm:ss')}
+
+
diff --git a/src/lib/components/groove/SongListing.svelte b/src/lib/components/groove/SongListing.svelte
new file mode 100644
index 0000000..de31017
--- /dev/null
+++ b/src/lib/components/groove/SongListing.svelte
@@ -0,0 +1,41 @@
+
+
+
diff --git a/src/lib/components/groove/ThemeSwitcher.svelte b/src/lib/components/groove/ThemeSwitcher.svelte
new file mode 100644
index 0000000..baafb13
--- /dev/null
+++ b/src/lib/components/groove/ThemeSwitcher.svelte
@@ -0,0 +1,13 @@
+
+
+
+ {#if $mode === 'light'}
+
+ {:else}
+
+ {/if}
+
diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte
new file mode 100644
index 0000000..9f526ea
--- /dev/null
+++ b/src/lib/components/ui/button/button.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+{#if href}
+
+ {@render children?.()}
+
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts
new file mode 100644
index 0000000..5414d9d
--- /dev/null
+++ b/src/lib/components/ui/button/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte
new file mode 100644
index 0000000..2244617
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte
@@ -0,0 +1,40 @@
+
+
+
+ {#snippet children({ checked, indeterminate })}
+
+ {#if indeterminate}
+
+ {:else}
+
+ {/if}
+
+ {@render childrenProp?.()}
+ {/snippet}
+
diff --git a/src/lib/components/ui/context-menu/context-menu-content.svelte b/src/lib/components/ui/context-menu/context-menu-content.svelte
new file mode 100644
index 0000000..7063285
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-content.svelte
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/src/lib/components/ui/context-menu/context-menu-group-heading.svelte
new file mode 100644
index 0000000..2ef5f35
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-group-heading.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/lib/components/ui/context-menu/context-menu-item.svelte b/src/lib/components/ui/context-menu/context-menu-item.svelte
new file mode 100644
index 0000000..0746b32
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-item.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte
new file mode 100644
index 0000000..4260177
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {#snippet children({ checked })}
+
+ {#if checked}
+
+ {/if}
+
+ {@render childrenProp?.({ checked })}
+ {/snippet}
+
diff --git a/src/lib/components/ui/context-menu/context-menu-separator.svelte b/src/lib/components/ui/context-menu/context-menu-separator.svelte
new file mode 100644
index 0000000..023ee02
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-separator.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte
new file mode 100644
index 0000000..ace2d5f
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte
new file mode 100644
index 0000000..8d20ab5
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte
new file mode 100644
index 0000000..8d9af3c
--- /dev/null
+++ b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte
@@ -0,0 +1,28 @@
+
+
+
+ {@render children?.()}
+
+
diff --git a/src/lib/components/ui/context-menu/index.ts b/src/lib/components/ui/context-menu/index.ts
new file mode 100644
index 0000000..3b3ed7d
--- /dev/null
+++ b/src/lib/components/ui/context-menu/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts
new file mode 100644
index 0000000..15c0933
--- /dev/null
+++ b/src/lib/components/ui/input/index.ts
@@ -0,0 +1,7 @@
+import Root from './input.svelte';
+
+export {
+ Root,
+ //
+ Root as Input
+};
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
new file mode 100644
index 0000000..50012de
--- /dev/null
+++ b/src/lib/components/ui/input/input.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts
new file mode 100644
index 0000000..5db432e
--- /dev/null
+++ b/src/lib/components/ui/popover/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte
new file mode 100644
index 0000000..583ed09
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-content.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/progress/index.ts b/src/lib/components/ui/progress/index.ts
new file mode 100644
index 0000000..97f57fc
--- /dev/null
+++ b/src/lib/components/ui/progress/index.ts
@@ -0,0 +1,7 @@
+import Root from './progress.svelte';
+
+export {
+ Root,
+ //
+ Root as Progress
+};
diff --git a/src/lib/components/ui/progress/progress.svelte b/src/lib/components/ui/progress/progress.svelte
new file mode 100644
index 0000000..0bed8e3
--- /dev/null
+++ b/src/lib/components/ui/progress/progress.svelte
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/lib/components/ui/scroll-area/index.ts b/src/lib/components/ui/scroll-area/index.ts
new file mode 100644
index 0000000..d546806
--- /dev/null
+++ b/src/lib/components/ui/scroll-area/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte
new file mode 100644
index 0000000..9bc3d13
--- /dev/null
+++ b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte
@@ -0,0 +1,29 @@
+
+
+
+ {@render children?.()}
+
+
diff --git a/src/lib/components/ui/scroll-area/scroll-area.svelte b/src/lib/components/ui/scroll-area/scroll-area.svelte
new file mode 100644
index 0000000..614fbdc
--- /dev/null
+++ b/src/lib/components/ui/scroll-area/scroll-area.svelte
@@ -0,0 +1,32 @@
+
+
+
+
+ {@render children?.()}
+
+ {#if orientation === 'vertical' || orientation === 'both'}
+
+ {/if}
+ {#if orientation === 'horizontal' || orientation === 'both'}
+
+ {/if}
+
+
diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts
new file mode 100644
index 0000000..768efac
--- /dev/null
+++ b/src/lib/components/ui/separator/index.ts
@@ -0,0 +1,7 @@
+import Root from './separator.svelte';
+
+export {
+ Root,
+ //
+ Root as Separator
+};
diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte
new file mode 100644
index 0000000..76b5bad
--- /dev/null
+++ b/src/lib/components/ui/separator/separator.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts
new file mode 100644
index 0000000..d315263
--- /dev/null
+++ b/src/lib/components/ui/sheet/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte
new file mode 100644
index 0000000..c2ad5df
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-content.svelte
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+ {@render children?.()}
+
+
+ Close
+
+
+
diff --git a/src/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte
new file mode 100644
index 0000000..83574ad
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-description.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte
new file mode 100644
index 0000000..6c0e68d
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-footer.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte
new file mode 100644
index 0000000..691cbee
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-header.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte
new file mode 100644
index 0000000..3cfbef4
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-overlay.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte
new file mode 100644
index 0000000..7696932
--- /dev/null
+++ b/src/lib/components/ui/sheet/sheet-title.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts
new file mode 100644
index 0000000..2d3bbfb
--- /dev/null
+++ b/src/lib/components/ui/sidebar/constants.ts
@@ -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';
diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts
new file mode 100644
index 0000000..6fa2aa3
--- /dev/null
+++ b/src/lib/components/ui/sidebar/context.svelte.ts
@@ -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;
+
+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;
+
+ /**
+ * 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 ``
+ 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));
+}
diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts
new file mode 100644
index 0000000..280e640
--- /dev/null
+++ b/src/lib/components/ui/sidebar/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte
new file mode 100644
index 0000000..d3c9be2
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-content.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte
new file mode 100644
index 0000000..9bad817
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte
new file mode 100644
index 0000000..21f103b
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte
@@ -0,0 +1,36 @@
+
+
+{#if child}
+ {@render child({ props: propObj })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte
new file mode 100644
index 0000000..38eb796
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte
new file mode 100644
index 0000000..2abc7d0
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte
@@ -0,0 +1,34 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte
new file mode 100644
index 0000000..fc5c9e1
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-group.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte
new file mode 100644
index 0000000..b95409c
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-header.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte
new file mode 100644
index 0000000..5a899fc
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-input.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte
new file mode 100644
index 0000000..220c64b
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
new file mode 100644
index 0000000..33f029f
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
@@ -0,0 +1,43 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
new file mode 100644
index 0000000..9dcd223
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
@@ -0,0 +1,29 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
new file mode 100644
index 0000000..d3e5fbc
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -0,0 +1,95 @@
+
+
+
+
+{#snippet Button({ props }: { props?: Record })}
+ {@const mergedProps = mergeProps(buttonProps, props)}
+ {#if child}
+ {@render child({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
+{/snippet}
+
+{#if !tooltipContent}
+ {@render Button({})}
+{:else}
+
+
+ {#snippet child({ props })}
+ {@render Button({ props })}
+ {/snippet}
+
+
+
+{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
new file mode 100644
index 0000000..9d5dab6
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
new file mode 100644
index 0000000..50ac650
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
@@ -0,0 +1,36 @@
+
+
+
+ {#if showIcon}
+
+ {/if}
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
new file mode 100644
index 0000000..3fd6b40
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
@@ -0,0 +1,43 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
new file mode 100644
index 0000000..a163119
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
@@ -0,0 +1,14 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
new file mode 100644
index 0000000..39c9de3
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
@@ -0,0 +1,25 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte
new file mode 100644
index 0000000..c0d1d7b
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte
new file mode 100644
index 0000000..5c9059c
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte
new file mode 100644
index 0000000..f747a35
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte
@@ -0,0 +1,36 @@
+
+
+ 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?.()}
+
diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte
new file mode 100644
index 0000000..94ed358
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte
new file mode 100644
index 0000000..e7a2ba9
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte
@@ -0,0 +1,34 @@
+
+
+ {
+ onclick?.(e);
+ sidebar.toggle();
+ }}
+ data-sidebar="trigger"
+ variant="ghost"
+ size="icon"
+ class={cn('h-7 w-7', className)}
+ {...restProps}
+>
+
+ Toggle Sidebar
+
diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte
new file mode 100644
index 0000000..a19a657
--- /dev/null
+++ b/src/lib/components/ui/sidebar/sidebar.svelte
@@ -0,0 +1,98 @@
+
+
+{#if collapsible === 'none'}
+
+ {@render children?.()}
+
+{:else if sidebar.isMobile}
+
+
+
+{:else}
+
+{/if}
diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts
new file mode 100644
index 0000000..3120ce1
--- /dev/null
+++ b/src/lib/components/ui/skeleton/index.ts
@@ -0,0 +1,7 @@
+import Root from './skeleton.svelte';
+
+export {
+ Root,
+ //
+ Root as Skeleton
+};
diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte
new file mode 100644
index 0000000..0b62163
--- /dev/null
+++ b/src/lib/components/ui/skeleton/skeleton.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/src/lib/components/ui/slider/index.ts b/src/lib/components/ui/slider/index.ts
new file mode 100644
index 0000000..f1524fc
--- /dev/null
+++ b/src/lib/components/ui/slider/index.ts
@@ -0,0 +1,7 @@
+import Root from './slider.svelte';
+
+export {
+ Root,
+ //
+ Root as Slider
+};
diff --git a/src/lib/components/ui/slider/slider.svelte b/src/lib/components/ui/slider/slider.svelte
new file mode 100644
index 0000000..4c16135
--- /dev/null
+++ b/src/lib/components/ui/slider/slider.svelte
@@ -0,0 +1,32 @@
+
+
+
+ {#snippet children({ thumbs })}
+
+
+
+ {#each thumbs as thumb}
+
+ {/each}
+ {/snippet}
+
diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts
new file mode 100644
index 0000000..034ab20
--- /dev/null
+++ b/src/lib/components/ui/tooltip/index.ts
@@ -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
+};
diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte
new file mode 100644
index 0000000..103e43c
--- /dev/null
+++ b/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/src/lib/covers.ts b/src/lib/covers.ts
new file mode 100644
index 0000000..fc87d3a
--- /dev/null
+++ b/src/lib/covers.ts
@@ -0,0 +1,3 @@
+export function getCoverUrl(songHash: string) {
+ return `http://localhost:39994/${songHash}.webp`;
+}
diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts
new file mode 100644
index 0000000..a957a64
--- /dev/null
+++ b/src/lib/hooks/is-mobile.svelte.ts
@@ -0,0 +1,27 @@
+import { untrack } from 'svelte';
+
+const MOBILE_BREAKPOINT = 768;
+
+export class IsMobile {
+ #current = $state(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;
+ }
+}
diff --git a/src/lib/proto/google/protobuf/empty.ts b/src/lib/proto/google/protobuf/empty.ts
new file mode 100644
index 0000000..77093fe
--- /dev/null
+++ b/src/lib/proto/google/protobuf/empty.ts
@@ -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 {
+ constructor() {
+ super('google.protobuf.Empty', []);
+ }
+ create(value?: PartialMessage): Empty {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ if (value !== undefined) reflectionMergePartial(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();
diff --git a/src/lib/proto/library.client.ts b/src/lib/proto/library.client.ts
new file mode 100644
index 0000000..d5177dd
--- /dev/null
+++ b/src/lib/proto/library.client.ts
@@ -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;
+}
+/**
+ * @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 {
+ const method = this.methods[0],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept('unary', this._transport, method, opt, input);
+ }
+}
diff --git a/src/lib/proto/library.ts b/src/lib/proto/library.ts
new file mode 100644
index 0000000..022586f
--- /dev/null
+++ b/src/lib/proto/library.ts
@@ -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 {
+ constructor() {
+ super('library.TrackList', [
+ { no: 1, name: 'tracks', kind: 'message', repeat: 1 /*RepeatType.PACKED*/, T: () => Track }
+ ]);
+ }
+ create(value?: PartialMessage): TrackList {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.tracks = [];
+ if (value !== undefined) reflectionMergePartial(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 {
+ 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 {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.hash = '';
+ message.name = '';
+ message.artistName = '';
+ message.artistId = 0n;
+ if (value !== undefined) reflectionMergePartial(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 }
+]);
diff --git a/src/lib/proto/player.client.ts b/src/lib/proto/player.client.ts
new file mode 100644
index 0000000..5360cae
--- /dev/null
+++ b/src/lib/proto/player.client.ts
@@ -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;
+ /**
+ * @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
+ */
+ resumeTrack(input: Empty, options?: RpcOptions): UnaryCall;
+ /**
+ * @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
+ */
+ pauseTrack(input: Empty, options?: RpcOptions): UnaryCall;
+}
+/**
+ * @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 {
+ const method = this.methods[0],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept(
+ 'unary',
+ this._transport,
+ method,
+ opt,
+ input
+ );
+ }
+ /**
+ * @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
+ */
+ resumeTrack(input: Empty, options?: RpcOptions): UnaryCall {
+ const method = this.methods[1],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept('unary', this._transport, method, opt, input);
+ }
+ /**
+ * @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
+ */
+ pauseTrack(input: Empty, options?: RpcOptions): UnaryCall {
+ const method = this.methods[2],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept('unary', this._transport, method, opt, input);
+ }
+}
diff --git a/src/lib/proto/player.ts b/src/lib/proto/player.ts
new file mode 100644
index 0000000..9748c76
--- /dev/null
+++ b/src/lib/proto/player.ts
@@ -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 {
+ constructor() {
+ super('player.PlayTrackRequest', [
+ { no: 1, name: 'hash', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }
+ ]);
+ }
+ create(value?: PartialMessage): PlayTrackRequest {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.hash = '';
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('player.PlayTrackResponse', []);
+ }
+ create(value?: PartialMessage): PlayTrackResponse {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ if (value !== undefined) reflectionMergePartial(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 }
+]);
diff --git a/src/lib/proto/settings.client.ts b/src/lib/proto/settings.client.ts
new file mode 100644
index 0000000..d3e775e
--- /dev/null
+++ b/src/lib/proto/settings.client.ts
@@ -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;
+ /**
+ * @generated from protobuf rpc: AddPath(settings.AddPathRequest) returns (settings.AddPathResponse);
+ */
+ addPath(input: AddPathRequest, options?: RpcOptions): UnaryCall;
+ /**
+ * @generated from protobuf rpc: DeletePath(settings.DeletePathRequest) returns (settings.DeletePathResponse);
+ */
+ deletePath(
+ input: DeletePathRequest,
+ options?: RpcOptions
+ ): UnaryCall;
+ /**
+ * @generated from protobuf rpc: RefreshPath(settings.RefreshPathRequest) returns (settings.RefreshPathResponse);
+ */
+ refreshPath(
+ input: RefreshPathRequest,
+ options?: RpcOptions
+ ): UnaryCall;
+}
+/**
+ * @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 {
+ const method = this.methods[0],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept('unary', this._transport, method, opt, input);
+ }
+ /**
+ * @generated from protobuf rpc: AddPath(settings.AddPathRequest) returns (settings.AddPathResponse);
+ */
+ addPath(input: AddPathRequest, options?: RpcOptions): UnaryCall {
+ const method = this.methods[1],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept(
+ 'unary',
+ this._transport,
+ method,
+ opt,
+ input
+ );
+ }
+ /**
+ * @generated from protobuf rpc: DeletePath(settings.DeletePathRequest) returns (settings.DeletePathResponse);
+ */
+ deletePath(
+ input: DeletePathRequest,
+ options?: RpcOptions
+ ): UnaryCall {
+ const method = this.methods[2],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept(
+ 'unary',
+ this._transport,
+ method,
+ opt,
+ input
+ );
+ }
+ /**
+ * @generated from protobuf rpc: RefreshPath(settings.RefreshPathRequest) returns (settings.RefreshPathResponse);
+ */
+ refreshPath(
+ input: RefreshPathRequest,
+ options?: RpcOptions
+ ): UnaryCall {
+ const method = this.methods[3],
+ opt = this._transport.mergeOptions(options);
+ return stackIntercept(
+ 'unary',
+ this._transport,
+ method,
+ opt,
+ input
+ );
+ }
+}
diff --git a/src/lib/proto/settings.ts b/src/lib/proto/settings.ts
new file mode 100644
index 0000000..38af3b8
--- /dev/null
+++ b/src/lib/proto/settings.ts
@@ -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 {
+ constructor() {
+ super('settings.SettingsData', [
+ {
+ no: 1,
+ name: 'library_paths',
+ kind: 'message',
+ repeat: 1 /*RepeatType.PACKED*/,
+ T: () => LibraryPath
+ }
+ ]);
+ }
+ create(value?: PartialMessage): SettingsData {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.libraryPaths = [];
+ if (value !== undefined) reflectionMergePartial(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 {
+ 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 {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.id = 0n;
+ message.path = '';
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.AddPathRequest', [
+ { no: 1, name: 'path', kind: 'scalar', T: 9 /*ScalarType.STRING*/ }
+ ]);
+ }
+ create(value?: PartialMessage): AddPathRequest {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.path = '';
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.AddPathResponse', [
+ { no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
+ ]);
+ }
+ create(value?: PartialMessage): AddPathResponse {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.id = 0n;
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.DeletePathRequest', [
+ { no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
+ ]);
+ }
+ create(value?: PartialMessage): DeletePathRequest {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.id = 0n;
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.DeletePathResponse', []);
+ }
+ create(value?: PartialMessage): DeletePathResponse {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.RefreshPathRequest', [
+ { no: 1, name: 'id', kind: 'scalar', T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
+ ]);
+ }
+ create(value?: PartialMessage): RefreshPathRequest {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ message.id = 0n;
+ if (value !== undefined) reflectionMergePartial(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 {
+ constructor() {
+ super('settings.RefreshPathResponse', []);
+ }
+ create(value?: PartialMessage): RefreshPathResponse {
+ const message = globalThis.Object.create(this.messagePrototype!);
+ if (value !== undefined) reflectionMergePartial(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 }
+]);
diff --git a/src/lib/song.ts b/src/lib/song.ts
new file mode 100644
index 0000000..6c30da4
--- /dev/null
+++ b/src/lib/song.ts
@@ -0,0 +1,6 @@
+export type Song = {
+ hash: string;
+ name: string;
+ artistName: string;
+ artistId: bigint;
+};
diff --git a/src/lib/stores/player.ts b/src/lib/stores/player.ts
new file mode 100644
index 0000000..ed409c1
--- /dev/null
+++ b/src/lib/stores/player.ts
@@ -0,0 +1,4 @@
+import { writable } from 'svelte/store';
+
+export const currentlyPlaying = writable(null);
+export const volume = writable(1.0);
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
new file mode 100644
index 0000000..7e6d447
--- /dev/null
+++ b/src/routes/+layout.server.ts
@@ -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
+};
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 9b776b7..18fef6e 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,6 +1,31 @@
-{@render children()}
+
+
+
+
+
+
+
+
+
+
+
+ {@render children()}
+
+
+
+
+
+
+
+Groove
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index cc88df0..f95bef3 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,2 +1 @@
-Welcome to SvelteKit
-Visit svelte.dev/docs/kit to read the documentation
+Home
diff --git a/src/routes/albums/+page.server.ts b/src/routes/albums/+page.server.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/routes/albums/+page.svelte b/src/routes/albums/+page.svelte
new file mode 100644
index 0000000..b8f0f1a
--- /dev/null
+++ b/src/routes/albums/+page.svelte
@@ -0,0 +1 @@
+Albums
diff --git a/src/routes/player/+page.server.ts b/src/routes/player/+page.server.ts
new file mode 100644
index 0000000..dd52c95
--- /dev/null
+++ b/src/routes/player/+page.server.ts
@@ -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;
diff --git a/src/routes/playlists/+page.server.ts b/src/routes/playlists/+page.server.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/routes/playlists/+page.svelte b/src/routes/playlists/+page.svelte
new file mode 100644
index 0000000..aeef039
--- /dev/null
+++ b/src/routes/playlists/+page.svelte
@@ -0,0 +1 @@
+Playlists
diff --git a/src/routes/settings/+page.server.ts b/src/routes/settings/+page.server.ts
new file mode 100644
index 0000000..780c789
--- /dev/null
+++ b/src/routes/settings/+page.server.ts
@@ -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;
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
new file mode 100644
index 0000000..2ea00b6
--- /dev/null
+++ b/src/routes/settings/+page.svelte
@@ -0,0 +1,73 @@
+
+
+
+ Settings
+
+
+
+
+
+
+
+
Appearance
+
+ Change the way Groove looks
+
+
+
+
+
+
+
+
Library
+
+ Add the directories you want to include in your library
+
+
+
+
+
+
+ {#each settings.libraryPaths as path}
+
+ {path.path}
+
+
+ {:else}
+
You have not added a directory yet
+ {/each}
+
+
+
diff --git a/src/routes/settings/path/[id]/+page.server.ts b/src/routes/settings/path/[id]/+page.server.ts
new file mode 100644
index 0000000..7a18b85
--- /dev/null
+++ b/src/routes/settings/path/[id]/+page.server.ts
@@ -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;
diff --git a/src/routes/songs/+page.server.ts b/src/routes/songs/+page.server.ts
new file mode 100644
index 0000000..cfa03cc
--- /dev/null
+++ b/src/routes/songs/+page.server.ts
@@ -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
+ };
+};
diff --git a/src/routes/songs/+page.svelte b/src/routes/songs/+page.svelte
new file mode 100644
index 0000000..d1d2ca9
--- /dev/null
+++ b/src/routes/songs/+page.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {#each data.songs as song}
+
+ {/each}
+
diff --git a/src/routes/songs/[hash]/+page.server.ts b/src/routes/songs/[hash]/+page.server.ts
new file mode 100644
index 0000000..ca4abb1
--- /dev/null
+++ b/src/routes/songs/[hash]/+page.server.ts
@@ -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;