feat: implement better track view, fix virtual list issue, implement icons

This commit is contained in:
dusk 2023-04-23 07:06:52 +03:00
parent a4999bb665
commit ef1af77dff
Signed by: dusk
GPG Key ID: 1D8F8FAF2294D6EA
11 changed files with 263 additions and 47 deletions

View File

@ -12,6 +12,8 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@iconify-json/line-md": "^1.1.24",
"@iconify-json/mdi": "^1.1.50",
"@skeletonlabs/skeleton": "^1.2.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2",
@ -30,6 +32,7 @@
"tailwindcss": "^3.3.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"unplugin-icons": "^0.16.1",
"vite": "^4.2.1"
},
"type": "module",

View File

@ -9,6 +9,12 @@ dependencies:
version: 2.0.5
devDependencies:
'@iconify-json/line-md':
specifier: ^1.1.24
version: 1.1.24
'@iconify-json/mdi':
specifier: ^1.1.50
version: 1.1.50
'@skeletonlabs/skeleton':
specifier: ^1.2.0
version: 1.2.0
@ -63,12 +69,26 @@ devDependencies:
typescript:
specifier: ^5.0.4
version: 5.0.4
unplugin-icons:
specifier: ^0.16.1
version: 0.16.1
vite:
specifier: ^4.2.1
version: 4.2.1
packages:
/@antfu/install-pkg@0.1.1:
resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==}
dependencies:
execa: 5.1.1
find-up: 5.0.0
dev: true
/@antfu/utils@0.7.2:
resolution: {integrity: sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==}
dev: true
/@esbuild/android-arm64@0.17.16:
resolution: {integrity: sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==}
engines: {node: '>=12'}
@ -324,6 +344,35 @@ packages:
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
dev: true
/@iconify-json/line-md@1.1.24:
resolution: {integrity: sha512-vHQnRZ3ZVDnrFoj8BMU0iTYR9404TrePP4UhCjkfOqkH4gibFoyM6JGO5OiIHv5m/GQvu4nteTICjklsRFUKpA==}
dependencies:
'@iconify/types': 2.0.0
dev: true
/@iconify-json/mdi@1.1.50:
resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==}
dependencies:
'@iconify/types': 2.0.0
dev: true
/@iconify/types@2.0.0:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true
/@iconify/utils@2.1.5:
resolution: {integrity: sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==}
dependencies:
'@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.2
'@iconify/types': 2.0.0
debug: 4.3.4
kolorist: 1.8.0
local-pkg: 0.4.3
transitivePeerDependencies:
- supports-color
dev: true
/@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'}
@ -1028,6 +1077,21 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
dev: true
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
@ -1111,6 +1175,11 @@ packages:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -1194,6 +1263,11 @@ packages:
function-bind: 1.1.1
dev: true
/human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@ -1262,6 +1336,11 @@ packages:
engines: {node: '>=8'}
dev: true
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: true
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
@ -1295,6 +1374,10 @@ packages:
engines: {node: '>=6'}
dev: true
/kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
dev: true
/levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@ -1312,6 +1395,11 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/local-pkg@0.4.3:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
engines: {node: '>=14'}
dev: true
/locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@ -1344,6 +1432,10 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@ -1363,6 +1455,11 @@ packages:
hasBin: true
dev: true
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
/min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@ -1435,6 +1532,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@ -1451,6 +1555,13 @@ packages:
wrappy: 1.0.2
dev: true
/onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
/optionator@0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
@ -1732,6 +1843,10 @@ packages:
engines: {node: '>=8'}
dev: true
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
/sirv@2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'}
@ -1773,6 +1888,11 @@ packages:
ansi-regex: 5.0.1
dev: true
/strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
/strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@ -2026,6 +2146,43 @@ packages:
busboy: 1.6.0
dev: true
/unplugin-icons@0.16.1:
resolution: {integrity: sha512-qTunFUkpAyDnwzwV7YV1ZgCWRYfLuURcCurhhXOWMy2ipY88qx1pADvral2hJu4Xymh0X0t3Zcll3BIru2AVLQ==}
peerDependencies:
'@svgr/core': '>=7.0.0'
'@vue/compiler-sfc': ^3.0.2 || ^2.7.0
vue-template-compiler: ^2.6.12
vue-template-es2015-compiler: ^1.9.0
peerDependenciesMeta:
'@svgr/core':
optional: true
'@vue/compiler-sfc':
optional: true
vue-template-compiler:
optional: true
vue-template-es2015-compiler:
optional: true
dependencies:
'@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.2
'@iconify/utils': 2.1.5
debug: 4.3.4
kolorist: 1.8.0
local-pkg: 0.4.3
unplugin: 1.3.1
transitivePeerDependencies:
- supports-color
dev: true
/unplugin@1.3.1:
resolution: {integrity: sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==}
dependencies:
acorn: 8.8.2
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.5.0
dev: true
/update-browserslist-db@1.0.10(browserslist@4.21.5):
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true
@ -2091,6 +2248,15 @@ packages:
vite: 4.2.1
dev: true
/webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
dev: true
/webpack-virtual-modules@0.5.0:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
dev: true
/which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}

4
src/app.d.ts vendored
View File

@ -1,5 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
/// <reference types="@sveltejs/kit" />
/// <reference types="unplugin-icons/types/svelte" />
declare global {
namespace App {
// interface Error {}
@ -9,4 +11,4 @@ declare global {
}
}
export {};
export { };

View File

@ -16,6 +16,8 @@ interface Message {
type MessageType = 'request' | 'response' | 'broadcast';
type RequestCallback = (arg0: Message | null) => void;
type Category = 'album' | 'artist' | 'album_artist' | 'genre' | 'playlist';
interface Callbacks {
onDisconnect: (authenticated: boolean, reason: string) => void;
onConnect: (initial: Message) => void;
@ -111,9 +113,11 @@ export class MetadataCommunicator {
id: t.id,
track: {
title: t.title,
album_id: t.album_id,
artist_id: t.artist_id,
track_num: t.track,
album_title: t.album,
album_id: t.album_id,
artist_name: t.artist,
artist_id: t.artist_id,
thumbnail_id: t.thumbnail_id,
}
})));
@ -159,7 +163,7 @@ export class MetadataCommunicator {
);
}
onConnect(cb: () => void) {
onConnect(cb: () => Promise<void>) {
if (!this.isClosed() && this.authenticated) {
cb();
} else {

View File

@ -6,6 +6,6 @@
$: isOnPage = href === $page.route.id;
</script>
<a {href} class="btn {isOnPage ? 'variant-ghost-primary' : 'hover:variant-soft-primary'}">
<a {href} class="btn p-2 px-3 {isOnPage ? 'variant-ghost-primary' : 'hover:variant-soft-primary'}">
<slot />
</a>

View File

@ -1,10 +1,14 @@
<script>
import Link from './a.svelte';
import IconMusic from '~icons/mdi/music';
import IconSettings from '~icons/mdi/settings';
import IconArtist from '~icons/mdi/artist';
import IconAlbum from '~icons/mdi/album';
</script>
<nav class="flex">
<Link href="/">t</Link>
<Link href="/albums">a</Link>
<Link href="/artists">a</Link>
<Link href="/settings">s</Link>
<Link href="/"><IconMusic class="w-7 h-7" /></Link>
<Link href="/albums"><IconAlbum class="w-7 h-7" /></Link>
<Link href="/artists"><IconArtist class="w-7 h-7" /></Link>
<Link href="/settings"><IconSettings class="w-7 h-7" /></Link>
</nav>

View File

@ -1,20 +1,42 @@
<script lang="ts">
import type { Track } from '../types';
import { address, token } from '../stores';
import Spinnny from '~icons/line-md/loading-loop';
import IconPlay from '~icons/mdi/play';
export let track: Track;
$: url = `http://${$address}/thumbnail/${track.thumbnail_id}?token=${$token}`;
let showSpinner = false;
let isError = false;
</script>
<div class="card flex gap-2">
<!-- svelte-ignore a11y-missing-attribute -->
<div class="rounded placeholder w-12 h-12">
<div class="card flex gap-4 m-2 p-2 w-fit max-w-full">
<button class="relative w-12 h-12 invisible hover:visible">
<div class="visible rounded placeholder w-12 h-12" />
<Spinnny class="absolute top-1 left-1 w-10 h-10 {showSpinner ? 'visible' : 'hidden'}" />
<!-- svelte-ignore a11y-missing-attribute -->
<img
src={url}
loading="lazy"
class="w-12 h-12"
on:error={(ev) => (ev.target.style.display = 'none')}
class="absolute top-0 left-0 rounded w-12 h-12 {showSpinner || isError
? 'hidden'
: 'visible'}"
on:error={() => {
isError = true;
showSpinner = false;
}}
on:loadstart={() => (showSpinner = true)}
on:load={() => (showSpinner = false)}
/>
<IconPlay
class="absolute top-0 left-0 w-12 h-12 rounded variant-glass-surface backdrop-blur-sm"
/>
</button>
<div class="whitespace-nowrap overflow-ellipsis overflow-hidden">
#{track.track_num} - {track.title}
<div class="text-sm whitespace-nowrap overflow-ellipsis overflow-hidden">
<span class="opacity-70">{track.album_title ? `from ${track.album_title}` : ''}</span>
<span class="opacity-40">{track.artist_name ? `by ${track.artist_name}` : ''}</span>
</div>
</div>
{track.track_num} - {track.title}
</div>

View File

@ -34,28 +34,23 @@
}
});
comm.connect($address, $token);
comm.onConnect(() => {
comm
.fetchTracksCount()
.then((count) => {
let remaining = count;
console.log(count);
while (remaining > 0) {
const offset = count - remaining;
comm.fetchTracks(500, offset).then((ts) => {
tracks.update((map) => {
ts.forEach((t) => map.set(t.id, t.track));
return map;
});
tracksSorted.update((map) => {
ts.forEach((t, index) => map.set(index + offset, t.id));
return map;
});
});
remaining -= 500;
}
})
.catch(() => null);
comm.onConnect(async () => {
const count = await comm.fetchTracksCount();
let remaining = count;
while (remaining > 0) {
const offset = count - remaining;
const ts = await comm.fetchTracks(500, offset);
tracks.update((map) => {
ts.forEach((t) => map.set(t.id, t.track));
return map;
});
tracksSorted.update((map) => {
ts.forEach((t, index) => map.set(index + offset, t.id));
return map;
});
remaining -= 500;
}
});
</script>

View File

@ -1,14 +1,27 @@
<script lang="ts">
import VirtualList from 'svelte-tiny-virtual-list';
import { tracks, tracksSorted } from '../stores';
import Track from '../components/track.svelte';
import TrackComponent from '../components/track.svelte';
import type { Track } from '../types';
$: trackCount = $tracksSorted.size;
let trackItemSize = 60;
let trackItemSize = 72;
let listHeight = 0;
function getTrack(index: number): Track {
return $tracks.get($tracksSorted.get(index)!)!;
}
</script>
<VirtualList height={trackCount * trackItemSize} itemSize={trackItemSize} itemCount={trackCount}>
<div slot="item" let:index let:style {style}>
<Track track={$tracks.get($tracksSorted.get(index) ?? BigInt(0))} />
</div>
</VirtualList>
<div class="h-full" bind:offsetHeight={listHeight}>
<VirtualList
height={listHeight}
itemSize={trackItemSize}
itemCount={trackCount}
overscanCount={1}
>
<div slot="item" let:index let:style {style}>
<div class="pr-4 md:ml-32"><TrackComponent track={getTrack(index)} /></div>
</div>
</VirtualList>
</div>

View File

@ -3,7 +3,9 @@ export type ResourceId = bigint;
export interface Track {
title: string,
track_num: number,
album_title: string,
album_id: ResourceId,
artist_name: string,
artist_id: ResourceId,
thumbnail_id: ResourceId,
}
@ -20,5 +22,4 @@ export interface Artist {
export interface Album {
title: string,
artist_id: ResourceId,
thumbnail_id: ResourceId,
}

View File

@ -1,6 +1,12 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import Icons from 'unplugin-icons/vite'
export default defineConfig({
plugins: [sveltekit()]
plugins: [
sveltekit(),
Icons({
compiler: 'svelte',
})
]
});