diff --git a/web/package-lock.json b/web/package-lock.json index cefb562..3efbb67 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.0.1", "dependencies": { + "animejs": "^4.2.2", "lucide-svelte": "^0.544.0", "openapi-fetch": "^0.14.0", "uuid": "^11.1.0" @@ -2777,6 +2778,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/animejs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/animejs/-/animejs-4.2.2.tgz", + "integrity": "sha512-Ys3RuvLdAeI14fsdKCQy7ytu4057QX6Bb7m4jwmfd6iKmUmLquTwk1ut0e4NtRQgCeq/s2Lv5+oMBjz6c7ZuIg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/juliangarnier" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/web/package.json b/web/package.json index 687280c..071bef4 100644 --- a/web/package.json +++ b/web/package.json @@ -70,6 +70,7 @@ "zod": "^3.25.76" }, "dependencies": { + "animejs": "^4.2.2", "lucide-svelte": "^0.544.0", "openapi-fetch": "^0.14.0", "uuid": "^11.1.0" diff --git a/web/src/lib/components/stats/stat-cards.svelte b/web/src/lib/components/stats/stat-cards.svelte index 634f0ae..5e50223 100644 --- a/web/src/lib/components/stats/stat-cards.svelte +++ b/web/src/lib/components/stats/stat-cards.svelte @@ -4,7 +4,7 @@ import client from '$lib/api'; import { isSemver, semverIsGreater } from '$lib/utils.ts'; import { env } from '$env/dynamic/public'; - import { resolve } from '$app/paths'; + import { animate } from 'animejs'; let moviesCount: string | null = $state(null); let episodeCount: string | null = $state(null); @@ -14,18 +14,55 @@ let releaseUrl: string | null = $state(null); let newestVersion: string | null = $state(null); + // Elements to animate + let showEl: HTMLSpanElement; + let episodeEl: HTMLSpanElement; + let moviesEl: HTMLSpanElement; + let torrentEl: HTMLSpanElement; + + function animateCounter(el: HTMLElement | undefined, target: number, pad = 3) { + if (!el) return; + + const obj = { value: 0 }; + + animate(obj, { + value: target, + duration: 2000, + easing: 'easeInOutSine', + onUpdate: () => { + el.textContent = Math.floor(obj.value).toString().padStart(pad, '0'); + } + }); + } + onMount(async () => { let tvShows = await client.GET('/api/v1/tv/shows'); - if (!tvShows.error) showCount = tvShows.data.length.toString().padStart(3, '0'); + if (!tvShows.error) { + const target = tvShows.data.length; + showCount = target.toString().padStart(3, '0'); + animateCounter(showEl, target, 3); + } let episodes = await client.GET('/api/v1/tv/episodes/count'); - if (!episodes.error) episodeCount = episodes.data.toString().padStart(3, '0'); + if (!episodes.error) { + const target = Number(episodes.data); + episodeCount = target.toString().padStart(3, '0'); + animateCounter(episodeEl, target, 3); + } let movies = await client.GET('/api/v1/movies'); - if (!movies.error) moviesCount = movies.data.length.toString().padStart(3, '0'); + if (!movies.error) { + const target = movies.data.length; + moviesCount = target.toString().padStart(3, '0'); + animateCounter(moviesEl, target, 3); + } let torrents = await client.GET('/api/v1/torrent'); - if (!torrents.error) torrentCount = torrents.data.length.toString().padStart(3, '0'); + if (!torrents.error) { + const target = torrents.data.length; + torrentCount = target.toString().padStart(3, '0'); + animateCounter(torrentEl, target, 3); + } let releases = await fetch('https://api.github.com/repos/maxdorninger/mediamanager/releases'); if (releases.ok) { @@ -38,25 +75,32 @@
- {showCount ?? 'Error'} + + {showCount ?? 'Error'} +
- {episodeCount ?? 'Error'} + + {episodeCount ?? 'Error'} +
- {moviesCount ?? 'Error'} + + {moviesCount ?? 'Error'} +
- {torrentCount ?? 'Error'} + + {torrentCount ?? 'Error'} +
{#if semverIsGreater(newestVersion ?? '', installedVersion ?? '') || !isSemver(installedVersion ?? '')} {installedVersion} → v{newestVersion}