aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/List/CompletedMangaList.svelte169
-rw-r--r--src/lib/List/MangaListTemplate.svelte169
-rw-r--r--src/routes/+layout.svelte4
-rw-r--r--src/routes/+page.svelte13
-rw-r--r--src/routes/completed/+page.svelte121
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>