diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/List/CompletedMangaList.svelte | 169 | ||||
| -rw-r--r-- | src/lib/List/MangaListTemplate.svelte | 169 | ||||
| -rw-r--r-- | src/routes/+layout.svelte | 4 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 13 | ||||
| -rw-r--r-- | src/routes/completed/+page.svelte | 121 |
5 files changed, 462 insertions, 14 deletions
diff --git a/src/lib/List/CompletedMangaList.svelte b/src/lib/List/CompletedMangaList.svelte new file mode 100644 index 00000000..9eee9d22 --- /dev/null +++ b/src/lib/List/CompletedMangaList.svelte @@ -0,0 +1,169 @@ +<script lang="ts"> + import { mediaListCollection, Type, type Media } from '$lib/AniList/media'; + import type { UserIdentity, AniListAuthorisation } from '$lib/AniList/identity'; + import { onDestroy, onMount } from 'svelte'; + import { chapterCount, pruneAllManga } from '$lib/Media/manga'; + import manga from '../../stores/manga'; + import { chapterDatabase } from '$lib/Media/chapters'; + import settings from '../../stores/settings'; + import lastPruneTimes from '../../stores/lastPruneTimes'; + import ListTitle from './ListTitle.svelte'; + import Error from '$lib/Error.svelte'; + + export let user: AniListAuthorisation; + export let identity: UserIdentity; + export let displayUnresolved: boolean; + + let mangaLists: Promise<Media[]>; + let startTime: number; + let endTime: number; + + const keyCacher = setInterval(() => { + startTime = performance.now(); + endTime = -1; + mangaLists = mediaListCollection(user, identity, Type.Manga, $manga, $lastPruneTimes.manga); + }, $settings.cacheMinutes * 1000 * 60); + + onMount(async () => { + startTime = performance.now(); + mangaLists = mediaListCollection(user, identity, Type.Manga, $manga, $lastPruneTimes.manga); + }); + + onDestroy(() => clearInterval(keyCacher)); + + const cleanMedia = async (media: Media[], displayUnresolved: boolean) => { + if (media === undefined) { + return []; + } + + if ($lastPruneTimes.chapters === 1) { + lastPruneTimes.setKey('chapters', new Date().getTime()); + } else { + const currentDate = new Date(); + + if ( + (currentDate.getTime() - $lastPruneTimes.chapters) / 1000 / 60 > + $settings.cacheMangaMinutes + ) { + const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray(); + const ids = unresolved.map((m) => m.id); + + lastPruneTimes.setKey('chapters', currentDate.getTime()); + await chapterDatabase.chapters.bulkDelete(ids); + } + } + + const releasingMedia = media.filter( + (media: Media) => + media.status == 'FINISHED' && + (media.mediaListEntry || { status: 'DROPPED' }).status != + ($settings.displayPausedMedia ? '' : 'PAUSED') && + (media.mediaListEntry || { progress: 0 }).progress >= + ($settings.displayNotStarted === true ? 0 : 1) + ); + let finalMedia = releasingMedia; + const chapterPromises = finalMedia.map((m: Media) => chapterCount(identity, m)); + const chapterCounts = await Promise.all(chapterPromises); + + finalMedia.forEach((m: Media, i) => { + m.episodes = chapterCounts[i] || -1337; + }); + + if (!displayUnresolved) { + finalMedia = finalMedia.filter((m: Media) => m.episodes !== -1337); + } + + finalMedia.sort((a: Media, b: Media) => { + return ( + (a.episodes || 9999) - + (a.mediaListEntry || { progress: 0 }).progress - + ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress) + ); + }); + + finalMedia = finalMedia.filter((item, index, array) => { + return ( + array.findIndex((i) => { + return i.id === item.id; + }) === index && + (item.episodes === -1337 && displayUnresolved + ? true + : (item.mediaListEntry?.progress || 0) < + ($settings.roundDownChapters === true ? Math.floor(item.episodes) : item.episodes)) + ); + }); + + if (!endTime || endTime === -1) { + endTime = performance.now() - startTime; + } + + return finalMedia; + }; + + const updateMedia = async (id: number, progress: number | undefined) => { + await chapterDatabase.chapters.delete(id); + await fetch(`/api/anilist-increment?id=${id}&progress=${(progress || 0) + 1}`).then(() => { + mangaLists = mediaListCollection( + user, + identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + true + ); + }); + }; + + const cleanCache = async () => { + await pruneAllManga(); + + mangaLists = mediaListCollection( + user, + identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + true + ); + }; +</script> + +{#await mangaLists} + <ListTitle /> + + <ul><li>Loading ...</li></ul> +{:then media} + {#await cleanMedia(media, displayUnresolved)} + <ListTitle /> + + <ul><li>Loading ...</li></ul> + {:then cleanedMedia} + <ListTitle count={cleanedMedia.length} time={endTime / 1000}> + <a href={'#'} title="Force a refresh" on:click={cleanCache}>Force</a> + </ListTitle> + + {#if cleanedMedia.length === 0} + <ul> + <li>No manga to display. <a href={'#'} on:click={cleanCache}>Force refresh</a></li> + </ul> + {/if} + + <ul> + {#each cleanedMedia as manga} + <li> + <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> + {manga.title.english || manga.title.romaji || manga.title.native} + </a> + <span style="opacity: 50%;">|</span> + {(manga.mediaListEntry || { progress: 0 }).progress} + <a href={'#'} on:click={() => updateMedia(manga.id, manga.mediaListEntry?.progress)}>+</a> + [{manga.episodes || '?'}] + </li> + {/each} + </ul> + {:catch} + <ListTitle count={'?'} time={0} /> + + <Error /> + {/await} +{/await} diff --git a/src/lib/List/MangaListTemplate.svelte b/src/lib/List/MangaListTemplate.svelte new file mode 100644 index 00000000..b14ce85e --- /dev/null +++ b/src/lib/List/MangaListTemplate.svelte @@ -0,0 +1,169 @@ +<script lang="ts"> + import { mediaListCollection, Type, type Media } from '$lib/AniList/media'; + import type { UserIdentity, AniListAuthorisation } from '$lib/AniList/identity'; + import { onDestroy, onMount } from 'svelte'; + import { chapterCount, pruneAllManga } from '$lib/Media/manga'; + import manga from '../../stores/manga'; + import { chapterDatabase } from '$lib/Media/chapters'; + import settings from '../../stores/settings'; + import lastPruneTimes from '../../stores/lastPruneTimes'; + import ListTitle from './ListTitle.svelte'; + import Error from '$lib/Error.svelte'; + + export let user: AniListAuthorisation; + export let identity: UserIdentity; + export let displayUnresolved: boolean; + + let mangaLists: Promise<Media[]>; + let startTime: number; + let endTime: number; + + const keyCacher = setInterval(() => { + startTime = performance.now(); + endTime = -1; + mangaLists = mediaListCollection(user, identity, Type.Manga, $manga, $lastPruneTimes.manga); + }, $settings.cacheMinutes * 1000 * 60); + + onMount(async () => { + startTime = performance.now(); + mangaLists = mediaListCollection(user, identity, Type.Manga, $manga, $lastPruneTimes.manga); + }); + + onDestroy(() => clearInterval(keyCacher)); + + const cleanMedia = async (media: Media[], displayUnresolved: boolean) => { + if (media === undefined) { + return []; + } + + if ($lastPruneTimes.chapters === 1) { + lastPruneTimes.setKey('chapters', new Date().getTime()); + } else { + const currentDate = new Date(); + + if ( + (currentDate.getTime() - $lastPruneTimes.chapters) / 1000 / 60 > + $settings.cacheMangaMinutes + ) { + const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray(); + const ids = unresolved.map((m) => m.id); + + lastPruneTimes.setKey('chapters', currentDate.getTime()); + await chapterDatabase.chapters.bulkDelete(ids); + } + } + + const releasingMedia = media.filter( + (media: Media) => + media.status == 'RELEASING' && + (media.mediaListEntry || { status: 'DROPPED' }).status != + ($settings.displayPausedMedia ? '' : 'PAUSED') && + (media.mediaListEntry || { progress: 0 }).progress >= + ($settings.displayNotStarted === true ? 0 : 1) + ); + let finalMedia = releasingMedia; + const chapterPromises = finalMedia.map((m: Media) => chapterCount(identity, m)); + const chapterCounts = await Promise.all(chapterPromises); + + finalMedia.forEach((m: Media, i) => { + m.episodes = chapterCounts[i] || -1337; + }); + + if (!displayUnresolved) { + finalMedia = finalMedia.filter((m: Media) => m.episodes !== -1337); + } + + finalMedia.sort((a: Media, b: Media) => { + return ( + (a.episodes || 9999) - + (a.mediaListEntry || { progress: 0 }).progress - + ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress) + ); + }); + + finalMedia = finalMedia.filter((item, index, array) => { + return ( + array.findIndex((i) => { + return i.id === item.id; + }) === index && + (item.episodes === -1337 && displayUnresolved + ? true + : (item.mediaListEntry?.progress || 0) < + ($settings.roundDownChapters === true ? Math.floor(item.episodes) : item.episodes)) + ); + }); + + if (!endTime || endTime === -1) { + endTime = performance.now() - startTime; + } + + return finalMedia; + }; + + const updateMedia = async (id: number, progress: number | undefined) => { + await chapterDatabase.chapters.delete(id); + await fetch(`/api/anilist-increment?id=${id}&progress=${(progress || 0) + 1}`).then(() => { + mangaLists = mediaListCollection( + user, + identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + true + ); + }); + }; + + const cleanCache = async () => { + await pruneAllManga(); + + mangaLists = mediaListCollection( + user, + identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + true + ); + }; +</script> + +{#await mangaLists} + <ListTitle /> + + <ul><li>Loading ...</li></ul> +{:then media} + {#await cleanMedia(media, displayUnresolved)} + <ListTitle /> + + <ul><li>Loading ...</li></ul> + {:then cleanedMedia} + <ListTitle count={cleanedMedia.length} time={endTime / 1000}> + <a href={'#'} title="Force a refresh" on:click={cleanCache}>Force</a> + </ListTitle> + + {#if cleanedMedia.length === 0} + <ul> + <li>No manga to display. <a href={'#'} on:click={cleanCache}>Force refresh</a></li> + </ul> + {/if} + + <ul> + {#each cleanedMedia as manga} + <li> + <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> + {manga.title.english || manga.title.romaji || manga.title.native} + </a> + <span style="opacity: 50%;">|</span> + {(manga.mediaListEntry || { progress: 0 }).progress} + <a href={'#'} on:click={() => updateMedia(manga.id, manga.mediaListEntry?.progress)}>+</a> + [{manga.episodes || '?'}] + </li> + {/each} + </ul> + {:catch} + <ListTitle count={'?'} time={0} /> + + <Error /> + {/await} +{/await} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8b802755..4080d5be 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -50,7 +50,9 @@ <p /> <p> - 「 <a href="/">Home</a> • <a href="/updates">Manga & LN Updates</a> • + 「 <a href="/">Home</a> • <a href="/completed">Completed</a> • + <a href="/updates">Manga & LN Updates</a> + • <a href="/tools">Tools</a> • <a href="/settings">Settings</a> 」 </p> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2d4fed88..9204eb1f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,7 +6,6 @@ import UpcomingAnimeList from '$lib/List/UpcomingAnimeList.svelte'; import userIdentity from '../stores/userIdentity'; import settings from '../stores/settings'; - import WatchingAnimeList from '$lib/List/WatchingAnimeList.svelte'; import { lastActivityDate } from '$lib/AniList/activity'; import ListTitle from '$lib/List/ListTitle.svelte'; @@ -116,18 +115,6 @@ <ul><li>Loading ...</li></ul> {/if} </details> - - {#if $settings.showCompletedAnime} - <details open={!$settings.closeAnimeByDefault} class="list"> - {#if currentUserIdentity.id != -1} - <WatchingAnimeList user={data.user} identity={currentUserIdentity} /> - {:else} - <ListTitle custom="Completed Anime" /> - - <ul><li>Loading ...</li></ul> - {/if} - </details> - {/if} {/if} </div> diff --git a/src/routes/completed/+page.svelte b/src/routes/completed/+page.svelte new file mode 100644 index 00000000..4c2537a8 --- /dev/null +++ b/src/routes/completed/+page.svelte @@ -0,0 +1,121 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import { userIdentity as getUserIdentity } from '$lib/AniList/identity'; + import userIdentity from '../../stores/userIdentity'; + import settings from '../../stores/settings'; + import WatchingAnimeList from '$lib/List/WatchingAnimeList.svelte'; + import { lastActivityDate } from '$lib/AniList/activity'; + import ListTitle from '$lib/List/ListTitle.svelte'; + import CompletedMangaList from '$lib/List/CompletedMangaList.svelte'; + + export let data; + + let currentUserIdentity = { name: '', id: -1 }; + let lastActivityWasToday = true; + + onMount(async () => { + settings.setKey('limitListHeight', false); + + if ($settings.limitListHeight) { + document.querySelectorAll('.list').forEach((list) => { + (list as HTMLElement).style.maxHeight = `calc(100vh - ${ + document.querySelector('#list-container')?.getBoundingClientRect().bottom + }px + 1.1rem)`; + }); + } + + if (data.user !== undefined) { + if ($userIdentity === '') { + userIdentity.set(JSON.stringify(await getUserIdentity(data.user))); + } + + currentUserIdentity = JSON.parse($userIdentity); + currentUserIdentity.name = currentUserIdentity.name; + lastActivityWasToday = + (await lastActivityDate(currentUserIdentity)).toDateString() >= new Date().toDateString(); + + if (!lastActivityWasToday) { + if ($settings.limitListHeight) { + document.querySelectorAll('.list').forEach((list) => { + (list as HTMLElement).style.maxHeight = `calc((100vh - ${ + document.querySelector('#list-container')?.getBoundingClientRect().bottom + }px) - 1.9rem)`; + }); + } + } + } + }); + + const timeLeftToday = () => { + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + const hoursLeft = 24 - currentHour; + let minutesLeft = 0; + let timeLeft = ''; + + if (hoursLeft > 0) { + minutesLeft = hoursLeft * 60 - currentMinute; + } else { + minutesLeft = 24 * 60 - (currentHour * 60 + currentMinute); + } + + if (minutesLeft > 60) { + timeLeft = `${Math.round(minutesLeft / 60)} hours`; + } else { + timeLeft = `${minutesLeft} minutes`; + } + + return timeLeft; + }; +</script> + +{#if !lastActivityWasToday} + <p> + You don't have any new activity statuses from the past day! Create one within {timeLeftToday()} + to keep your streak! + </p> +{/if} + +<div id="list-container"> + {#if data.user === undefined} + Please log in to view due media. + {:else} + <details open={!$settings.closeAnimeByDefault} class="list"> + {#if currentUserIdentity.id != -1} + <WatchingAnimeList user={data.user} identity={currentUserIdentity} /> + {:else} + <ListTitle custom="Completed Anime" /> + + <ul><li>Loading ...</li></ul> + {/if} + </details> + + <details open={!$settings.closeMangaByDefault} class="list"> + {#if currentUserIdentity.id != -1} + <CompletedMangaList + user={data.user} + identity={currentUserIdentity} + displayUnresolved={$settings.displayUnresolved} + /> + {:else} + <ListTitle custom="Completed Anime" /> + + <ul><li>Loading ...</li></ul> + {/if} + </details> + {/if} +</div> + +<style> + #list-container { + display: flex; + flex-wrap: wrap; + } + + .list { + overflow-y: auto; + min-width: 300px; + flex: 1 1 300px; + } +</style> |