fix!: performance issues related to lucide-svelte and shadcn

This commit is contained in:
2024-11-29 04:01:01 +01:00
parent c89073e025
commit 1287b71580
9 changed files with 183 additions and 186 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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
View File

@@ -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 {

View File

@@ -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}
> >

View File

@@ -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>

View File

@@ -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);
} }

View File

@@ -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;
}
}}
/>

View File

@@ -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);

View File

@@ -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'
})
]
}); });