diff --git a/common/components/cards/EpisodeCard.svelte b/common/components/cards/EpisodeCard.svelte index 795dc2c..2e6c9de 100644 --- a/common/components/cards/EpisodeCard.svelte +++ b/common/components/cards/EpisodeCard.svelte @@ -2,8 +2,9 @@ import { statusColorMap } from '@/modules/anime.js' import EpisodePreviewCard from './EpisodePreviewCard.svelte' import { hoverClick } from '@/modules/click.js' - import { since } from '@/modules/util' + import { since } from '@/modules/util.js' import { getContext } from 'svelte' + import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js' export let data let preview = false @@ -19,6 +20,8 @@ function setHoverState (state) { preview = state } + + const progress = liveAnimeEpisodeProgress(media.id, data.episode)
@@ -34,6 +37,11 @@ {media.duration}m {/if}
+ {#if $progress > 0} +
+
+
+ {/if}
diff --git a/common/components/cards/EpisodePreviewCard.svelte b/common/components/cards/EpisodePreviewCard.svelte index d08f77e..ede9ca4 100644 --- a/common/components/cards/EpisodePreviewCard.svelte +++ b/common/components/cards/EpisodePreviewCard.svelte @@ -1,12 +1,15 @@
@@ -29,6 +32,11 @@ {media.duration}m {/if}
+ {#if $progress > 0} +
+
+
+ {/if}
diff --git a/common/components/cards/SmallCard.svelte b/common/components/cards/SmallCard.svelte index 3769cb5..8d6624e 100644 --- a/common/components/cards/SmallCard.svelte +++ b/common/components/cards/SmallCard.svelte @@ -37,6 +37,7 @@
{/if} cover +
{#if media.mediaListEntry?.status}
diff --git a/common/modules/animeprogress.js b/common/modules/animeprogress.js new file mode 100644 index 0000000..66e3603 --- /dev/null +++ b/common/modules/animeprogress.js @@ -0,0 +1,69 @@ +import { writable, derived } from 'simple-store-svelte' + +// Maximum number of entries to keep in LocalStorage +const maxEntries = 1000 + +// LocalStorage is structured as an array of objects with the following properties: +// mediaId, episode, currentTime, safeduration, createdAt, updatedAt +function loadFromLocalStorage() { + const data = localStorage.getItem('animeEpisodeProgress') + return data ? JSON.parse(data) : [] +} + +function saveToLocalStorage(data) { + localStorage.setItem('animeEpisodeProgress', JSON.stringify(data)) + animeProgressStore.set(data) +} + +const animeProgressStore = writable(loadFromLocalStorage()) + +// Return an object with the progress of each episode in percent (0-100), keyed by episode number +export function liveAnimeProgress (mediaId){ + return derived(animeProgressStore, (data) => { + if (!mediaId) return {} + const results = data.filter(item => item.mediaId === mediaId) + if (!results) return {} + // Return an object with the episode as the key and the progress as the value + return Object.fromEntries(results.map(result => [ + result.episode, + Math.ceil(result.currentTime / result.safeduration * 100) + ])) + }) +} + +// Return an individual episode's progress in percent (0-100) +export function liveAnimeEpisodeProgress (mediaId, episode) { + return derived(animeProgressStore, (data) => { + if (!mediaId || !episode) return 0 + const result = data.find(item => item.mediaId === mediaId && item.episode === episode) + if (!result) return 0 + return Math.ceil(result.currentTime / result.safeduration * 100) + }) +} + +// Return an individual episode's record { mediaId, episode, currentTime, safeduration, createdAt, updatedAt } +export function getAnimeProgress(mediaId, episode) { + const data = loadFromLocalStorage() + return data.find(item => item.mediaId === mediaId && item.episode === episode) +} + +// Set an individual episode's progress +export function setAnimeProgress({ mediaId, episode, currentTime, safeduration }) { + if (!mediaId || !episode || !currentTime || !safeduration) return + const data = loadFromLocalStorage() + // Update the existing entry or create a new one + const existing = data.find(item => item.mediaId === mediaId && item.episode === episode) + if (existing) { + existing.currentTime = currentTime + existing.safeduration = safeduration + existing.updatedAt = Date.now() + } else { + data.push({ mediaId, episode, currentTime, safeduration, createdAt: Date.now(), updatedAt: Date.now() }) + } + // Remove the oldest entries if we have too many + while (data.length > maxEntries) { + const oldest = data.reduce((a, b) => a.updatedAt < b.updatedAt ? a : b) + data.splice(data.indexOf(oldest), 1) + } + saveToLocalStorage(data) +} diff --git a/common/package.json b/common/package.json index 02c9e26..6cfc163 100644 --- a/common/package.json +++ b/common/package.json @@ -23,4 +23,4 @@ "video-deband": "^1.0.5", "webpack-merge": "^5.10.0" } -} \ No newline at end of file +} diff --git a/common/views/Player/Player.svelte b/common/views/Player/Player.svelte index 1daed25..7c98754 100644 --- a/common/views/Player/Player.svelte +++ b/common/views/Player/Player.svelte @@ -1,5 +1,6 @@ {#each episodeOrder ? episodeList : [...episodeList].reverse() as { episode, image, summary, rating, title, length, airdate }} {@const completed = userProgress >= episode} {@const target = userProgress + 1 === episode} + {@const progress = $animeProgress?.[episode] ?? 0}
play(episode)}>
{#if image} @@ -67,6 +71,10 @@
+ {:else if progress} +
+
+
{/if}
{summary || ''}