fix!: performance issues related to lucide-svelte and shadcn
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"build:proto": "bunx protoc --ts_out ./src/lib/proto/ --proto_path protos protos/*"
|
"build:proto": "bunx protoc --ts_out ./src/lib/proto/ --proto_path protos protos/*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.17",
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
"bits-ui": "^1.0.0-next.64",
|
"bits-ui": "^1.0.0-next.64",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
"lucide-svelte": "^0.460.1",
|
"lucide-svelte": "^0.462.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
"unplugin-icons": "^0.20.2",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
import 'unplugin-icons/types/svelte';
|
||||||
|
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex h-full flex-col gap-1 overflow-y-auto pr-3"
|
class="flex h-full flex-col gap-1 overflow-y-auto"
|
||||||
method="POST"
|
method="POST"
|
||||||
use:enhance={submitPlayerAction}
|
use:enhance={submitPlayerAction}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,149 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import { getCoverUrl } from '$lib/covers';
|
import { getCoverUrl } from '$lib/covers';
|
||||||
import { getPlayerState } from '$lib/player.svelte';
|
|
||||||
import { AudioLines, Music2, ListMusic, ListEnd } from 'lucide-svelte';
|
|
||||||
import type { SubmitFunction } from '../../../routes/tracks/[hash]/$types';
|
|
||||||
import type { Track } from '$lib/proto/library';
|
import type { Track } from '$lib/proto/library';
|
||||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
import AudioLines from 'virtual:icons/lucide/audio-lines';
|
||||||
import type { Action } from 'svelte/action';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
track: Track;
|
track: Track;
|
||||||
hideCover?: boolean;
|
currentlyPlaying?: boolean;
|
||||||
action: Action;
|
oncontextmenu?: (event: MouseEvent, hash: string) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { track, action, hideCover = false }: Props = $props();
|
let { track, currentlyPlaying = false, oncontextmenu }: Props = $props();
|
||||||
|
|
||||||
let isOpen = $state(false);
|
|
||||||
|
|
||||||
const player = getPlayerState();
|
|
||||||
|
|
||||||
const submitPlayTrack: SubmitFunction = async () => {
|
|
||||||
return async ({ update, result }) => {
|
|
||||||
isOpen = false;
|
|
||||||
|
|
||||||
await update({
|
|
||||||
invalidateAll: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.type === 'success' && result.data && 'track' in result.data) {
|
|
||||||
player.queue = [];
|
|
||||||
player.currentlyPlaying = result.data.track ?? null;
|
|
||||||
player.progress = result.data.position;
|
|
||||||
player.isPaused = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitEnqueue: SubmitFunction = async () => {
|
|
||||||
return async ({ update, result }) => {
|
|
||||||
isOpen = false;
|
|
||||||
|
|
||||||
await update({
|
|
||||||
invalidateAll: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.type === 'success' && result.data && 'tracks' in result.data) {
|
|
||||||
player.queue = result.data.tracks;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function contextMenuItemClicked(e: MouseEvent) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<div
|
||||||
class="flex flex-col gap-3"
|
class="flex flex-col gap-3"
|
||||||
method="POST"
|
|
||||||
action="/tracks/{track.hash}?/play"
|
|
||||||
use:enhance={submitPlayTrack}
|
|
||||||
use:action
|
|
||||||
data-track-hash={track.hash}
|
data-track-hash={track.hash}
|
||||||
|
oncontextmenu={(e) => oncontextmenu?.(e, track.hash)}
|
||||||
>
|
>
|
||||||
<ContextMenu.Root bind:open={isOpen}>
|
<div class="relative aspect-square w-full">
|
||||||
<ContextMenu.Trigger>
|
<button
|
||||||
<div class="relative aspect-square w-full">
|
type="submit"
|
||||||
<button
|
class="relative aspect-square h-full w-full overflow-hidden rounded-lg"
|
||||||
type="submit"
|
formaction="/tracks/{track.hash}?/play"
|
||||||
class="relative aspect-square h-full w-full overflow-hidden rounded-lg"
|
>
|
||||||
>
|
<img
|
||||||
<img
|
class="aspect-square h-full w-full transform-gpu transition-all duration-150 hover:scale-105 hover:saturate-150"
|
||||||
class="aspect-square h-full w-full transform-gpu transition-all duration-150 hover:scale-105 hover:saturate-150"
|
src={getCoverUrl(track.hash)}
|
||||||
src={hideCover ? undefined : getCoverUrl(track.hash)}
|
alt={track.name}
|
||||||
alt={track.name}
|
width="640"
|
||||||
width="640"
|
height="640"
|
||||||
height="640"
|
/>
|
||||||
/>
|
</button>
|
||||||
</button>
|
<div class="absolute bottom-6 left-2 size-4 animate-pulse" class:hidden={!currentlyPlaying}>
|
||||||
|
<AudioLines class="text-primary" />
|
||||||
<div
|
</div>
|
||||||
class="absolute bottom-6 left-2 size-4 animate-pulse"
|
</div>
|
||||||
class:hidden={player.currentlyPlaying?.hash !== track.hash}
|
<div class="relative space-y-1 text-sm">
|
||||||
>
|
<h3 class="font-medium leading-none">{track.name}</h3>
|
||||||
<AudioLines class="text-primary" />
|
<p class="text-xs text-muted-foreground">{track.artistName}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative space-y-1 text-sm">
|
|
||||||
<h3 class="font-medium leading-none">{track.name}</h3>
|
|
||||||
<p class="text-xs text-muted-foreground">{track.artistName}</p>
|
|
||||||
</div>
|
|
||||||
</ContextMenu.Trigger>
|
|
||||||
<ContextMenu.Content>
|
|
||||||
<ContextMenu.Item class="!p-0">
|
|
||||||
<form
|
|
||||||
class="flex w-full"
|
|
||||||
method="POST"
|
|
||||||
action="/tracks/{track.hash}?/play"
|
|
||||||
use:enhance={submitPlayTrack}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
|
||||||
onclick={contextMenuItemClicked}
|
|
||||||
>
|
|
||||||
<Music2 class="mr-1 size-4" />
|
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
<ContextMenu.Item class="!p-0">
|
|
||||||
<form
|
|
||||||
class="flex w-full"
|
|
||||||
method="POST"
|
|
||||||
action="/tracks/{track.hash}?/play-next"
|
|
||||||
use:enhance={submitEnqueue}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
|
||||||
onclick={contextMenuItemClicked}
|
|
||||||
>
|
|
||||||
<ListMusic class="mr-1 size-4" />
|
|
||||||
Play Next
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
<ContextMenu.Item class="!p-0">
|
|
||||||
<form
|
|
||||||
class="flex w-full"
|
|
||||||
method="POST"
|
|
||||||
action="/tracks/{track.hash}?/add-to-queue"
|
|
||||||
use:enhance={submitEnqueue}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
|
||||||
onclick={contextMenuItemClicked}
|
|
||||||
>
|
|
||||||
<ListEnd class="mr-1 size-4" />
|
|
||||||
Add to Queue
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ContextMenu.Item>
|
|
||||||
</ContextMenu.Content>
|
|
||||||
</ContextMenu.Root>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SearchState {
|
|||||||
filterTracks(tracks: Track[]) {
|
filterTracks(tracks: Track[]) {
|
||||||
const fzf = new Fzf(tracks, {
|
const fzf = new Fzf(tracks, {
|
||||||
selector: (t) => `${t.name} ${t.artistName}`,
|
selector: (t) => `${t.name} ${t.artistName}`,
|
||||||
sort: true,
|
sort: true
|
||||||
});
|
});
|
||||||
return fzf.find(this.input);
|
return fzf.find(this.input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,150 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import TrackListing from '$lib/components/groove/TrackListing.svelte';
|
import TrackListing from '$lib/components/groove/TrackListing.svelte';
|
||||||
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
import { getSearchState } from '$lib/search.svelte';
|
import type { SubmitFunction } from './[hash]/$types';
|
||||||
import type { Action } from 'svelte/action';
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import { SvelteSet as Set } from 'svelte/reactivity';
|
import Music2 from 'virtual:icons/lucide/music-2';
|
||||||
|
import ListMusic from 'virtual:icons/lucide/list-music';
|
||||||
|
import ListEnd from 'virtual:icons/lucide/list-end';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageServerData;
|
data: PageServerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = getSearchState();
|
const player = getPlayerState();
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let contextMenuTarget = $state<string | null>(null);
|
||||||
let visibleTracks = $state<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const options: IntersectionObserverInit = {
|
const submitPlayTrack: SubmitFunction = async () => {
|
||||||
// @ts-ignore
|
return async ({ update, result }) => {
|
||||||
root: container,
|
contextMenuTarget = null;
|
||||||
rootMargin: "400px 0px 400px 0px",
|
|
||||||
threshold: 0.0,
|
await update({
|
||||||
|
invalidateAll: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'track' in result.data) {
|
||||||
|
player.queue = [];
|
||||||
|
player.currentlyPlaying = result.data.track ?? null;
|
||||||
|
player.progress = result.data.position;
|
||||||
|
player.isPaused = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const callback: IntersectionObserverCallback = (entries, _observer) => {
|
const submitEnqueue: SubmitFunction = async () => {
|
||||||
entries.forEach(entry => {
|
return async ({ update, result }) => {
|
||||||
const hash = entry.target.getAttribute('data-track-hash')!;
|
contextMenuTarget = null;
|
||||||
if (entry.isIntersecting) {
|
|
||||||
if (!visibleTracks.has(hash)) {
|
await update({
|
||||||
visibleTracks.add(hash);
|
invalidateAll: false
|
||||||
}
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'tracks' in result.data) {
|
||||||
|
player.queue = result.data.tracks;
|
||||||
}
|
}
|
||||||
else if (visibleTracks.has(hash)) {
|
};
|
||||||
visibleTracks.delete(hash);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new IntersectionObserver(callback, options);
|
function contextMenuItemClicked(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
const observe: Action = (node) => {
|
|
||||||
observer.observe(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const firstFewTracks = search.filterTracks(data.tracks).slice(0,40);
|
|
||||||
firstFewTracks.forEach(e => {
|
|
||||||
visibleTracks.add(e.item.hash);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<form
|
||||||
|
method="POST"
|
||||||
class="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8"
|
class="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8"
|
||||||
bind:this={container}
|
use:enhance={submitPlayTrack}
|
||||||
>
|
>
|
||||||
{#each search.input.length > 0 ? (search.filterTracks(data.tracks).map(r => r.item) ?? []) : data.tracks as track (track.hash)}
|
{#each data.tracks as track}
|
||||||
<TrackListing {track} action={observe} hideCover={!visibleTracks.has(track.hash)}/>
|
<TrackListing
|
||||||
|
{track}
|
||||||
|
currentlyPlaying={player.currentlyPlaying?.hash === track.hash}
|
||||||
|
oncontextmenu={(e, hash) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
contextMenuTarget = hash;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
|
<ContextMenu.Root
|
||||||
|
open={contextMenuTarget !== null}
|
||||||
|
onOpenChange={(value) => {
|
||||||
|
if (!value) {
|
||||||
|
contextMenuTarget = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContextMenu.Content
|
||||||
|
customAnchor={`div[data-track-hash="${contextMenuTarget!}"]`}
|
||||||
|
strategy="fixed"
|
||||||
|
>
|
||||||
|
<ContextMenu.Item class="!p-0">
|
||||||
|
<form
|
||||||
|
class="flex w-full"
|
||||||
|
method="POST"
|
||||||
|
action="/tracks/{contextMenuTarget!}?/play"
|
||||||
|
use:enhance={submitPlayTrack}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
||||||
|
onclick={contextMenuItemClicked}
|
||||||
|
>
|
||||||
|
<Music2 class="mr-1 size-4" />
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item class="!p-0">
|
||||||
|
<form
|
||||||
|
class="flex w-full"
|
||||||
|
method="POST"
|
||||||
|
action="/tracks/{contextMenuTarget!}?/play-next"
|
||||||
|
use:enhance={submitEnqueue}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
||||||
|
onclick={contextMenuItemClicked}
|
||||||
|
>
|
||||||
|
<ListMusic class="mr-1 size-4" />
|
||||||
|
Play Next
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item class="!p-0">
|
||||||
|
<form
|
||||||
|
class="flex w-full"
|
||||||
|
method="POST"
|
||||||
|
action="/tracks/{contextMenuTarget!}?/add-to-queue"
|
||||||
|
use:enhance={submitEnqueue}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
||||||
|
onclick={contextMenuItemClicked}
|
||||||
|
>
|
||||||
|
<ListEnd class="mr-1 size-4" />
|
||||||
|
Add to Queue
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
oncontextmenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (contextMenuTarget) {
|
||||||
|
contextMenuTarget = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -7,17 +7,22 @@ import type { PlayTrackResponse, Queue } from '$lib/proto/player';
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
play: async ({ params }) => {
|
play: async ({ params }) => {
|
||||||
const client = new PlayerClient(protoTransport);
|
try {
|
||||||
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
const response = await client.playTrack({
|
const response = await client.playTrack({
|
||||||
hash: params.hash
|
hash: params.hash
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.response.track === undefined) {
|
if (response.response.track === undefined) {
|
||||||
return fail(500);
|
return fail(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializable<PlayTrackResponse>(response.response);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('play error:');
|
||||||
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializable<PlayTrackResponse>(response.response);
|
|
||||||
},
|
},
|
||||||
'add-to-queue': async ({ params }) => {
|
'add-to-queue': async ({ params }) => {
|
||||||
const client = new PlayerClient(protoTransport);
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()]
|
plugins: [
|
||||||
|
sveltekit(),
|
||||||
|
Icons({
|
||||||
|
compiler: 'svelte'
|
||||||
|
})
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user