From 7ae47d02f2a2282e0246ffa7680807249a20df64 Mon Sep 17 00:00:00 2001 From: 409 Date: Sun, 24 Nov 2024 00:11:36 +0100 Subject: [PATCH] feat: basic song ui + settings --- bun.lockb | Bin 117846 -> 90271 bytes eslint.config.js | 33 -- package.json | 15 +- protos/library.proto | 20 + protos/player.proto | 18 + protos/settings.proto | 43 ++ src/app.css | 50 +- src/hooks.server.ts | 5 + src/lib/components/groove/AppSidebar.svelte | 73 +++ src/lib/components/groove/Footer.svelte | 160 +++++ src/lib/components/groove/SongListing.svelte | 41 ++ .../components/groove/ThemeSwitcher.svelte | 13 + src/lib/components/ui/button/button.svelte | 69 +++ src/lib/components/ui/button/index.ts | 17 + .../context-menu-checkbox-item.svelte | 40 ++ .../context-menu/context-menu-content.svelte | 24 + .../context-menu-group-heading.svelte | 19 + .../ui/context-menu/context-menu-item.svelte | 23 + .../context-menu-radio-item.svelte | 30 + .../context-menu-separator.svelte | 16 + .../context-menu/context-menu-shortcut.svelte | 20 + .../context-menu-sub-content.svelte | 19 + .../context-menu-sub-trigger.svelte | 28 + src/lib/components/ui/context-menu/index.ts | 49 ++ src/lib/components/ui/input/index.ts | 7 + src/lib/components/ui/input/input.svelte | 22 + src/lib/components/ui/popover/index.ts | 17 + .../ui/popover/popover-content.svelte | 28 + src/lib/components/ui/progress/index.ts | 7 + .../components/ui/progress/progress.svelte | 24 + src/lib/components/ui/scroll-area/index.ts | 10 + .../scroll-area/scroll-area-scrollbar.svelte | 29 + .../ui/scroll-area/scroll-area.svelte | 32 + src/lib/components/ui/separator/index.ts | 7 + .../components/ui/separator/separator.svelte | 22 + src/lib/components/ui/sheet/index.ts | 37 ++ .../components/ui/sheet/sheet-content.svelte | 56 ++ .../ui/sheet/sheet-description.svelte | 16 + .../components/ui/sheet/sheet-footer.svelte | 20 + .../components/ui/sheet/sheet-header.svelte | 20 + .../components/ui/sheet/sheet-overlay.svelte | 19 + .../components/ui/sheet/sheet-title.svelte | 16 + src/lib/components/ui/sidebar/constants.ts | 6 + .../components/ui/sidebar/context.svelte.ts | 79 +++ src/lib/components/ui/sidebar/index.ts | 75 +++ .../ui/sidebar/sidebar-content.svelte | 24 + .../ui/sidebar/sidebar-footer.svelte | 21 + .../ui/sidebar/sidebar-group-action.svelte | 36 ++ .../ui/sidebar/sidebar-group-content.svelte | 21 + .../ui/sidebar/sidebar-group-label.svelte | 34 ++ .../ui/sidebar/sidebar-group.svelte | 21 + .../ui/sidebar/sidebar-header.svelte | 21 + .../ui/sidebar/sidebar-input.svelte | 23 + .../ui/sidebar/sidebar-inset.svelte | 24 + .../ui/sidebar/sidebar-menu-action.svelte | 43 ++ .../ui/sidebar/sidebar-menu-badge.svelte | 29 + .../ui/sidebar/sidebar-menu-button.svelte | 95 +++ .../ui/sidebar/sidebar-menu-item.svelte | 21 + .../ui/sidebar/sidebar-menu-skeleton.svelte | 36 ++ .../ui/sidebar/sidebar-menu-sub-button.svelte | 43 ++ .../ui/sidebar/sidebar-menu-sub-item.svelte | 14 + .../ui/sidebar/sidebar-menu-sub.svelte | 25 + .../components/ui/sidebar/sidebar-menu.svelte | 21 + .../ui/sidebar/sidebar-provider.svelte | 59 ++ .../components/ui/sidebar/sidebar-rail.svelte | 36 ++ .../ui/sidebar/sidebar-separator.svelte | 18 + .../ui/sidebar/sidebar-trigger.svelte | 34 ++ src/lib/components/ui/sidebar/sidebar.svelte | 98 ++++ src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 + src/lib/components/ui/slider/index.ts | 7 + src/lib/components/ui/slider/slider.svelte | 32 + src/lib/components/ui/tooltip/index.ts | 18 + .../ui/tooltip/tooltip-content.svelte | 21 + src/lib/covers.ts | 3 + src/lib/hooks/is-mobile.svelte.ts | 27 + src/lib/proto/google/protobuf/empty.ts | 87 +++ src/lib/proto/library.client.ts | 37 ++ src/lib/proto/library.ts | 207 +++++++ src/lib/proto/player.client.ts | 74 +++ src/lib/proto/player.ts | 129 +++++ src/lib/proto/settings.client.ts | 109 ++++ src/lib/proto/settings.ts | 545 ++++++++++++++++++ src/lib/song.ts | 6 + src/lib/stores/player.ts | 4 + src/routes/+layout.server.ts | 8 + src/routes/+layout.svelte | 27 +- src/routes/+page.svelte | 3 +- src/routes/albums/+page.server.ts | 0 src/routes/albums/+page.svelte | 1 + src/routes/player/+page.server.ts | 16 + src/routes/playlists/+page.server.ts | 0 src/routes/playlists/+page.svelte | 1 + src/routes/settings/+page.server.ts | 43 ++ src/routes/settings/+page.svelte | 73 +++ src/routes/settings/path/[id]/+page.server.ts | 20 + src/routes/songs/+page.server.ts | 20 + src/routes/songs/+page.svelte | 18 + src/routes/songs/[hash]/+page.server.ts | 13 + 99 files changed, 3680 insertions(+), 64 deletions(-) delete mode 100644 eslint.config.js create mode 100644 protos/library.proto create mode 100644 protos/player.proto create mode 100644 protos/settings.proto create mode 100644 src/hooks.server.ts create mode 100644 src/lib/components/groove/AppSidebar.svelte create mode 100644 src/lib/components/groove/Footer.svelte create mode 100644 src/lib/components/groove/SongListing.svelte create mode 100644 src/lib/components/groove/ThemeSwitcher.svelte create mode 100644 src/lib/components/ui/button/button.svelte create mode 100644 src/lib/components/ui/button/index.ts create mode 100644 src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-content.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-group-heading.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-radio-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-separator.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-shortcut.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-sub-content.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte create mode 100644 src/lib/components/ui/context-menu/index.ts create mode 100644 src/lib/components/ui/input/index.ts create mode 100644 src/lib/components/ui/input/input.svelte create mode 100644 src/lib/components/ui/popover/index.ts create mode 100644 src/lib/components/ui/popover/popover-content.svelte create mode 100644 src/lib/components/ui/progress/index.ts create mode 100644 src/lib/components/ui/progress/progress.svelte create mode 100644 src/lib/components/ui/scroll-area/index.ts create mode 100644 src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte create mode 100644 src/lib/components/ui/scroll-area/scroll-area.svelte create mode 100644 src/lib/components/ui/separator/index.ts create mode 100644 src/lib/components/ui/separator/separator.svelte create mode 100644 src/lib/components/ui/sheet/index.ts create mode 100644 src/lib/components/ui/sheet/sheet-content.svelte create mode 100644 src/lib/components/ui/sheet/sheet-description.svelte create mode 100644 src/lib/components/ui/sheet/sheet-footer.svelte create mode 100644 src/lib/components/ui/sheet/sheet-header.svelte create mode 100644 src/lib/components/ui/sheet/sheet-overlay.svelte create mode 100644 src/lib/components/ui/sheet/sheet-title.svelte create mode 100644 src/lib/components/ui/sidebar/constants.ts create mode 100644 src/lib/components/ui/sidebar/context.svelte.ts create mode 100644 src/lib/components/ui/sidebar/index.ts create mode 100644 src/lib/components/ui/sidebar/sidebar-content.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-footer.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-group-action.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-group-content.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-group-label.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-group.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-header.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-input.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-inset.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-action.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-badge.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-button.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-item.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu-sub.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-menu.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-provider.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-rail.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-separator.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar-trigger.svelte create mode 100644 src/lib/components/ui/sidebar/sidebar.svelte create mode 100644 src/lib/components/ui/skeleton/index.ts create mode 100644 src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 src/lib/components/ui/slider/index.ts create mode 100644 src/lib/components/ui/slider/slider.svelte create mode 100644 src/lib/components/ui/tooltip/index.ts create mode 100644 src/lib/components/ui/tooltip/tooltip-content.svelte create mode 100644 src/lib/covers.ts create mode 100644 src/lib/hooks/is-mobile.svelte.ts create mode 100644 src/lib/proto/google/protobuf/empty.ts create mode 100644 src/lib/proto/library.client.ts create mode 100644 src/lib/proto/library.ts create mode 100644 src/lib/proto/player.client.ts create mode 100644 src/lib/proto/player.ts create mode 100644 src/lib/proto/settings.client.ts create mode 100644 src/lib/proto/settings.ts create mode 100644 src/lib/song.ts create mode 100644 src/lib/stores/player.ts create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/albums/+page.server.ts create mode 100644 src/routes/albums/+page.svelte create mode 100644 src/routes/player/+page.server.ts create mode 100644 src/routes/playlists/+page.server.ts create mode 100644 src/routes/playlists/+page.svelte create mode 100644 src/routes/settings/+page.server.ts create mode 100644 src/routes/settings/+page.svelte create mode 100644 src/routes/settings/path/[id]/+page.server.ts create mode 100644 src/routes/songs/+page.server.ts create mode 100644 src/routes/songs/+page.svelte create mode 100644 src/routes/songs/[hash]/+page.server.ts diff --git a/bun.lockb b/bun.lockb index 4fababa678fdda9efa3b073f25f265a504159790..f4c7ad993877056ac19622ba72c0700baa651f35 100755 GIT binary patch delta 21304 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 z z^SWs@i*UIl70j?Ip_1ZVYc};E>0oTQd-L<|8Y>9R!?$aw%?h&$ zd>7CW-C?Gaq(U{3)PqwJWsFHe1L-FJdJTp6zVq*D$L69 zEqU*rxNNlKc|Eo2j!_66U+fT}S%;JyRH-wfc<)|XRozxRua{Oc9Ls+XR4DgJeVE4h zrZIp8ZZbIX9zhBZ)$9Q$4;oq>e)*JFi~&`*jUZrcp-G0Z(-qucaJWN5&pv9yo%?82 zL!)``J{TC$@*0D{!vCk>q@YQ|xn5hj1Lf(H3QqPKR@AG(1xZu?W?Np=SF3Vr$DL!f znt>RW)KT*KvKbs^4^0So^#B~T26e$JXWVenS|M?&7#;83FFeN)F(y#Fod6pE*AD3* zO7glg`xi=bEdSC$R0UummyXX-(r;kSCrbJVSdNsyM3D}n0lni5fN_Np&dPo$|9nwLR>?17F(GK;S6Qz`*VUXGXeQK`%f3zY8I6utXfd7u> z{C5if8~N0ASwJ;fBBvoieKrH2qb#)mKSz?9DUUTc`jd1%Ks~w8oLiQX`9%QH#gxx| zhB#^{u|(p|8S2?ljmwF{K~%-x8yq!M;v0bR#X}tV`XN?1H0ss^bPy$t4FHwjWac-6 z(m|BuTLIGB2GCKK;z38)3sC+(fDWR>?+2*-LBJ9?LDPc{B&35VHTo1l1x}mk8BjVt zN6SNgLCTdWRdC776D50B0kU@upre$ce#*EmiDgO#ip+djN_sZ|qPNWXL`naSnI}pP zl>k)nLxA)j0dx>0`BP$WJXNL#2}tponLY=lgD4ev2~hqkfDWQ0|AiPF|1Xpr&v_#i z7X+fzC+`8O=mS6pQQ|+Esl*^n)DFasGR>3>Rsl~U2eZ5^CHplXC&%4ny&R#YS&=9i z@Q^bEPcyA$&L>Lw^s5RTL@8MpCo=3~mVb`Ygrav6RB@15Po{`}SpkjSCQu-qre?*m zltfyyoG2M?0a_Wf3n)3*&75DBQpG*Y^1o2(|Gxxs^yTu&t1+S zmgH2yXD(|}OaBM1YV!CGMJ43O|NUhR$~a0db8<8cviRS)*vR$&8wKPV{{4#np`5vX z$$@|8vi3i9)iVA&hG;Bg0o8#2T-E+_Rr}9XjjnG0xvJ6H_LqxT!@mpuPdx86G_!q4 z$E9Ds5htiq-*x&T`G=qh_1Dj4P517YllSgOYp+w5uQhwOJh$BcY=(PY!i`@8k6A7Z zcxutRNncxsp~jL*snvf$KQQ4#G{cY8~*;7j;Z;DLwY_U!-mHk z*D<_3I<99{e8G4<)9`mlJ*Ξ9QZjZ}rTE_rkdn?=?%$D)TK!+w%H{^~{cs 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 Z5YJ3w{lv;AFiS?AVAh3Ozhx^e{tNU#U?Bhi delta 38057 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 XDe7$Ae}z8Uz?WX5EzJj~X?4T@Fnax* 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 @@ + + +
+ +
+ {dayjs.duration(0, 'seconds').format('mm:ss')} + + {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 @@ + + +
+
+ + + +
+
+

{song.name}

+

{song.artistName}

+
+
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 @@ + + + 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} + +{/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} + +{/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} + +{/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} + + {/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 @@ + + +
  • + {@render children?.()} +
  • 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 @@ + + + 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 @@ + + + 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} + + +
    + {@render children?.()} +
    +
    +
    +{: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;