diff options
Diffstat (limited to 'src/lib/List')
| -rw-r--r-- | src/lib/List/Anime/AnimeListTemplate.svelte | 58 | ||||
| -rw-r--r-- | src/lib/List/Anime/CleanAnimeList.svelte | 253 | ||||
| -rw-r--r-- | src/lib/List/Anime/CompletedAnimeList.svelte | 181 | ||||
| -rw-r--r-- | src/lib/List/Anime/DueAnimeList.svelte | 222 | ||||
| -rw-r--r-- | src/lib/List/Anime/DueIndexColumn.svelte | 16 | ||||
| -rw-r--r-- | src/lib/List/Anime/PlaceholderList.svelte | 8 | ||||
| -rw-r--r-- | src/lib/List/Anime/UpcomingAnimeList.svelte | 148 | ||||
| -rw-r--r-- | src/lib/List/CleanGrid.svelte | 19 | ||||
| -rw-r--r-- | src/lib/List/CleanList.svelte | 12 | ||||
| -rw-r--r-- | src/lib/List/ListTitle.svelte | 8 | ||||
| -rw-r--r-- | src/lib/List/Manga/CleanMangaList.svelte | 95 | ||||
| -rw-r--r-- | src/lib/List/Manga/MangaListTemplate.svelte | 492 | ||||
| -rw-r--r-- | src/lib/List/MediaRoulette.svelte | 92 | ||||
| -rw-r--r-- | src/lib/List/MediaTitleDisplay.svelte | 12 | ||||
| -rw-r--r-- | src/lib/List/covers.css | 84 | ||||
| -rw-r--r-- | src/lib/List/mediaTitle.ts | 21 |
16 files changed, 942 insertions, 779 deletions
diff --git a/src/lib/List/Anime/AnimeListTemplate.svelte b/src/lib/List/Anime/AnimeListTemplate.svelte index 8f2846c6..2bf6df77 100644 --- a/src/lib/List/Anime/AnimeListTemplate.svelte +++ b/src/lib/List/Anime/AnimeListTemplate.svelte @@ -1,27 +1,27 @@ <script lang="ts"> /* eslint svelte/no-at-html-tags: "off" */ -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import type { Media } from '$lib/Data/AniList/media'; -import RateLimitedError from '$lib/Error/RateLimited.svelte'; -import settings from '$stores/settings'; -import CleanAnimeList from './CleanAnimeList.svelte'; -import ListTitle from '../ListTitle.svelte'; -import type { SubsPlease } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; -import PlaceholderList from './PlaceholderList.svelte'; -import { browser } from '$app/environment'; -import { onMount } from 'svelte'; -import subsPlease from '$stores/subsPlease'; -import identity from '$stores/identity'; -import localforage from 'localforage'; -import type { Title } from '../mediaTitle'; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import type { Media } from "$lib/Data/AniList/media"; +import RateLimitedError from "$lib/Error/RateLimited.svelte"; +import settings from "$stores/settings"; +import CleanAnimeList from "./CleanAnimeList.svelte"; +import ListTitle from "../ListTitle.svelte"; +import type { SubsPlease } from "$lib/Media/Anime/Airing/Subtitled/subsPlease"; +import PlaceholderList from "./PlaceholderList.svelte"; +import { browser } from "$app/environment"; +import { onMount } from "svelte"; +import subsPlease from "$stores/subsPlease"; +import identity from "$stores/identity"; +import localforage from "localforage"; +import type { Title } from "../mediaTitle"; export let endTime: number; export let cleanMedia: ( - media: Media[], - displayUnresolved: boolean, - subsPlease: SubsPlease | null, - plannedOnly?: boolean + media: Media[], + displayUnresolved: boolean, + subsPlease: SubsPlease | null, + plannedOnly?: boolean, ) => Media[]; export let animeLists: Promise<Media[]>; export let user: AniListAuthorisation; @@ -40,15 +40,21 @@ let pendingUpdate: number | null = null; let lastListSize = 8; onMount(async () => { - if (browser) { - const lastStoredList = (await localforage.getItem( - `last${ - notYetReleased ? 'NotYetReleased' : upcoming ? 'Upcoming' : completed ? 'Completed' : '' - }AnimeListLength` - )) as string | null; + if (browser) { + const lastStoredList = (await localforage.getItem( + `last${ + notYetReleased + ? "NotYetReleased" + : upcoming + ? "Upcoming" + : completed + ? "Completed" + : "" + }AnimeListLength`, + )) as string | null; - if (lastStoredList) lastListSize = parseInt(lastStoredList); - } + if (lastStoredList) lastListSize = parseInt(lastStoredList); + } }); </script> diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte index 14406f04..d1efa3a8 100644 --- a/src/lib/List/Anime/CleanAnimeList.svelte +++ b/src/lib/List/Anime/CleanAnimeList.svelte @@ -1,25 +1,25 @@ <script lang="ts"> -import Spacer from '$lib/Layout/Spacer.svelte'; +import Spacer from "$lib/Layout/Spacer.svelte"; /* eslint svelte/no-at-html-tags: "off" */ -import settings from '$stores/settings'; -import type { Media } from '$lib/Data/AniList/media'; -import { cleanCache, incrementMediaProgress } from '$lib/Media/Anime/cache'; -import { totalEpisodes } from '$lib/Media/Anime/episodes'; -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import ListTitle from '../ListTitle.svelte'; -import { onDestroy, onMount } from 'svelte'; -import AiringTime from '$lib/Media/Anime/Airing/AiringTime.svelte'; -import { browser } from '$app/environment'; -import identity from '$stores/identity'; -import '../covers.css'; -import revalidateAnime from '$stores/revalidateAnime'; -import CleanGrid from '$lib/List/CleanGrid.svelte'; -import CleanList from '../CleanList.svelte'; -import stateBin from '$stores/stateBin'; -import localforage from 'localforage'; -import MediaRoulette from '../MediaRoulette.svelte'; -import type { Title } from '../mediaTitle'; +import settings from "$stores/settings"; +import type { Media } from "$lib/Data/AniList/media"; +import { cleanCache, incrementMediaProgress } from "$lib/Media/Anime/cache"; +import { totalEpisodes } from "$lib/Media/Anime/episodes"; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import ListTitle from "../ListTitle.svelte"; +import { onDestroy, onMount } from "svelte"; +import AiringTime from "$lib/Media/Anime/Airing/AiringTime.svelte"; +import { browser } from "$app/environment"; +import identity from "$stores/identity"; +import "../covers.css"; +import revalidateAnime from "$stores/revalidateAnime"; +import CleanGrid from "$lib/List/CleanGrid.svelte"; +import CleanList from "../CleanList.svelte"; +import stateBin from "$stores/stateBin"; +import localforage from "localforage"; +import MediaRoulette from "../MediaRoulette.svelte"; +import type { Title } from "../mediaTitle"; export let media: Media[]; export let title: Title; @@ -40,136 +40,165 @@ let showRoulette = false; let airingRefreshTimeout: ReturnType<typeof setTimeout> | undefined; let scheduledAiringAt: number | null = null; let totalEpisodeDueCount = media - .map((anime) => { - if ($settings.displayTotalEpisodes && !$settings.displayTotalDueEpisodes) return 1; - - if ($settings.displayTotalDueEpisodes && completed && !$settings.displayTotalEpisodes) return 1; - - if ($settings.displayTotalEpisodes && anime.status === 'FINISHED') - return anime.episodes - (anime.mediaListEntry?.progress || 0); - - if (anime.status === 'NOT_YET_RELEASED') return 1; - - return ( - (anime.nextAiringEpisode?.episode || 1) - - (anime.mediaListEntry?.progress || 0) - - (upcoming || notYetReleased ? 0 : 1) - ); - }) - .reduce((a, b) => a + b, 0); + .map((anime) => { + if ($settings.displayTotalEpisodes && !$settings.displayTotalDueEpisodes) + return 1; + + if ( + $settings.displayTotalDueEpisodes && + completed && + !$settings.displayTotalEpisodes + ) + return 1; + + if ($settings.displayTotalEpisodes && anime.status === "FINISHED") + return anime.episodes - (anime.mediaListEntry?.progress || 0); + + if (anime.status === "NOT_YET_RELEASED") return 1; + + return ( + (anime.nextAiringEpisode?.episode || 1) - + (anime.mediaListEntry?.progress || 0) - + (upcoming || notYetReleased ? 0 : 1) + ); + }) + .reduce((a, b) => a + b, 0); const lists = Array.from( - new Set( - media - .flatMap((m) => Object.entries(m.mediaListEntry?.customLists ?? {})) - .filter(([_key, value]) => value) - .map(([key]) => key) - ) + new Set( + media + .flatMap((m) => Object.entries(m.mediaListEntry?.customLists ?? {})) + .filter(([_key, value]) => value) + .map(([key]) => key), + ), ); let filterKind = upcoming - ? 'Upcoming' - : notYetReleased - ? 'NotYetReleased' - : completed - ? 'Completed' - : 'Due'; + ? "Upcoming" + : notYetReleased + ? "NotYetReleased" + : completed + ? "Completed" + : "Due"; const filterKey = `${filterKind}AnimeListFilter`; -$: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All'; +$: selectedList = disableFilter + ? "All" + : ($stateBin[filterKey] as string) || "All"; $: filteredMedia = - selectedList === 'All' || !$settings.displayMediaListFilter - ? media - : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]); + selectedList === "All" || !$settings.displayMediaListFilter + ? media + : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]); const clearAiringRefreshTimeout = () => { - if (airingRefreshTimeout) clearTimeout(airingRefreshTimeout); + if (airingRefreshTimeout) clearTimeout(airingRefreshTimeout); - airingRefreshTimeout = undefined; - scheduledAiringAt = null; + airingRefreshTimeout = undefined; + scheduledAiringAt = null; }; const scheduleAiringRefresh = () => { - if (!browser) return; + if (!browser) return; - if (dummy || media.length === 0) { - clearAiringRefreshTimeout(); + if (dummy || media.length === 0) { + clearAiringRefreshTimeout(); - return; - } + return; + } - const nextAiringAt = media.reduce<number | null>((closest, currentMedia) => { - if (currentMedia.status !== 'RELEASING' && currentMedia.status !== 'NOT_YET_RELEASED') - return closest; + const nextAiringAt = media.reduce<number | null>((closest, currentMedia) => { + if ( + currentMedia.status !== "RELEASING" && + currentMedia.status !== "NOT_YET_RELEASED" + ) + return closest; - const airingAt = currentMedia.nextAiringEpisode?.airingAt; + const airingAt = currentMedia.nextAiringEpisode?.airingAt; - if (!airingAt) return closest; - if (closest === null) return airingAt; + if (!airingAt) return closest; + if (closest === null) return airingAt; - return airingAt < closest ? airingAt : closest; - }, null); + return airingAt < closest ? airingAt : closest; + }, null); - if (!nextAiringAt) { - clearAiringRefreshTimeout(); + if (!nextAiringAt) { + clearAiringRefreshTimeout(); - return; - } + return; + } - if (airingRefreshTimeout && scheduledAiringAt === nextAiringAt) return; + if (airingRefreshTimeout && scheduledAiringAt === nextAiringAt) return; - clearAiringRefreshTimeout(); - scheduledAiringAt = nextAiringAt; - airingRefreshTimeout = setTimeout( - () => { - const now = Date.now() / 1000; + clearAiringRefreshTimeout(); + scheduledAiringAt = nextAiringAt; + airingRefreshTimeout = setTimeout( + () => { + const now = Date.now() / 1000; - if (media.some((m) => m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < now)) - animeLists = cleanCache(user, $identity); + if ( + media.some( + (m) => + m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < now, + ) + ) + animeLists = cleanCache(user, $identity); - scheduleAiringRefresh(); - }, - Math.max(1000, nextAiringAt * 1000 - Date.now() + 250) - ); + scheduleAiringRefresh(); + }, + Math.max(1000, nextAiringAt * 1000 - Date.now() + 250), + ); }; onMount(async () => { - if (dummy) return; - - scheduleAiringRefresh(); - - if (browser) - await localforage.setItem( - `last${ - notYetReleased ? 'NotYetReleased' : upcoming ? 'Upcoming' : completed ? 'Completed' : '' - }AnimeListLength`, - media.length.toString() - ); + if (dummy) return; + + scheduleAiringRefresh(); + + if (browser) + await localforage.setItem( + `last${ + notYetReleased + ? "NotYetReleased" + : upcoming + ? "Upcoming" + : completed + ? "Completed" + : "" + }AnimeListLength`, + media.length.toString(), + ); }); $: if (browser && !dummy) { - media; + media; - scheduleAiringRefresh(); + scheduleAiringRefresh(); } onDestroy(() => clearAiringRefreshTimeout()); const increment = (anime: Media, progress: number) => { - if (!dummy && pendingUpdate !== anime.id) { - $revalidateAnime = true; - lastUpdatedMedia = anime.id; - pendingUpdate = anime.id; - - incrementMediaProgress(anime.id, anime.mediaListEntry?.progress, user, () => { - const mediaListEntry = media.find((m) => m.id === anime.id)?.mediaListEntry; - - if (mediaListEntry) mediaListEntry.progress = progress + 1; - - previousAnimeList = media; - animeLists = cleanCache(user, $identity); - pendingUpdate = null; - }); - } + if (!dummy && pendingUpdate !== anime.id) { + $revalidateAnime = true; + lastUpdatedMedia = anime.id; + pendingUpdate = anime.id; + + incrementMediaProgress( + anime.id, + anime.mediaListEntry?.progress, + user, + () => { + const mediaListEntry = media.find( + (m) => m.id === anime.id, + )?.mediaListEntry; + + if (mediaListEntry) mediaListEntry.progress = progress + 1; + + previousAnimeList = media; + animeLists = cleanCache(user, $identity); + pendingUpdate = null; + }, + ); + } }; </script> diff --git a/src/lib/List/Anime/CompletedAnimeList.svelte b/src/lib/List/Anime/CompletedAnimeList.svelte index e308a230..6a393dff 100644 --- a/src/lib/List/Anime/CompletedAnimeList.svelte +++ b/src/lib/List/Anime/CompletedAnimeList.svelte @@ -1,21 +1,21 @@ <script lang="ts"> -import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import { onMount } from 'svelte'; -import anime from '$stores/anime'; -import lastPruneTimes from '$stores/lastPruneTimes'; -import settings from '$stores/settings'; -import AnimeList from './AnimeListTemplate.svelte'; -import { addNotification } from '$lib/Notification/store'; -import locale from '$stores/locale'; -import identity from '$stores/identity'; -import sampleAnime from '$lib/Data/Static/SampleMedia/anime.json'; +import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media"; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import { onMount } from "svelte"; +import anime from "$stores/anime"; +import lastPruneTimes from "$stores/lastPruneTimes"; +import settings from "$stores/settings"; +import AnimeList from "./AnimeListTemplate.svelte"; +import { addNotification } from "$lib/Notification/store"; +import locale from "$stores/locale"; +import identity from "$stores/identity"; +import sampleAnime from "$lib/Data/Static/SampleMedia/anime.json"; export let user: AniListAuthorisation = { - accessToken: '', - refreshToken: '', - expiresIn: 0, - tokenType: '' + accessToken: "", + refreshToken: "", + expiresIn: 0, + tokenType: "", }; export let dummy = false; export let dummyCount = 7; @@ -26,89 +26,102 @@ let startTime: number; let endTime: number; onMount(async () => { - startTime = performance.now(); + startTime = performance.now(); - if (dummy) { - // Use deterministic selection for consistent display - const filtered = sampleAnime.filter( - (anime) => - anime.episodes && - !anime.tags.some((tag) => tag.name === 'Nudity') && - !anime.tags.some((tag) => tag.name === 'Rape') && - !anime.tags.some((tag) => tag.name === 'Tragedy') && - !anime.tags.some((tag) => tag.name === 'Bondage') && - !anime.genres.some((genre) => genre === 'Hentai') && - anime.genres.some((genre) => genre === 'Comedy') && - anime.status !== 'NOT_YET_RELEASED' && - anime.episodes > 1 - ); - animeLists = Promise.resolve( - filtered.slice(0, dummyCount).map((anime, index) => { - anime.status = 'FINISHED'; - anime.nextAiringEpisode = { - airingAt: Math.floor(Date.now() / 1000) + (index + 1) * 24 * 60 * 60, - episode: Math.floor((anime.episodes || 12) * 0.8) - }; - anime.mediaListEntry.progress = Math.floor((anime.nextAiringEpisode.episode || 5) * 0.6); - return anime; - }) as unknown as Media[] - ); - } else { - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - addNotification - }); - } + if (dummy) { + // Use deterministic selection for consistent display + const filtered = sampleAnime.filter( + (anime) => + anime.episodes && + !anime.tags.some((tag) => tag.name === "Nudity") && + !anime.tags.some((tag) => tag.name === "Rape") && + !anime.tags.some((tag) => tag.name === "Tragedy") && + !anime.tags.some((tag) => tag.name === "Bondage") && + !anime.genres.some((genre) => genre === "Hentai") && + anime.genres.some((genre) => genre === "Comedy") && + anime.status !== "NOT_YET_RELEASED" && + anime.episodes > 1, + ); + animeLists = Promise.resolve( + filtered.slice(0, dummyCount).map((anime, index) => { + anime.status = "FINISHED"; + anime.nextAiringEpisode = { + airingAt: Math.floor(Date.now() / 1000) + (index + 1) * 24 * 60 * 60, + episode: Math.floor((anime.episodes || 12) * 0.8), + }; + anime.mediaListEntry.progress = Math.floor( + (anime.nextAiringEpisode.episode || 5) * 0.6, + ); + return anime; + }) as unknown as Media[], + ); + } else { + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + addNotification, + }, + ); + } }); const cleanMedia = (anime: Media[]) => { - if (anime && dummy) return anime; + if (anime && dummy) return anime; - if (anime === undefined) return []; + if (anime === undefined) return []; - const outdatedCompletedAnime = anime.filter( - (media: Media) => - media.status === 'FINISHED' && - (media.mediaListEntry || { status: 'DROPPED' }).status !== 'DROPPED' && - (media.mediaListEntry || { status: 'DROPPED' }).status !== - ($settings.displayPausedMedia ? '' : 'PAUSED') && - (media.mediaListEntry || { progress: 0 }).progress >= ($settings.displayNotStarted ? 0 : 1) - ); + const outdatedCompletedAnime = anime.filter( + (media: Media) => + media.status === "FINISHED" && + (media.mediaListEntry || { status: "DROPPED" }).status !== "DROPPED" && + (media.mediaListEntry || { status: "DROPPED" }).status !== + ($settings.displayPausedMedia ? "" : "PAUSED") && + (media.mediaListEntry || { progress: 0 }).progress >= + ($settings.displayNotStarted ? 0 : 1), + ); - outdatedCompletedAnime.sort((a: Media, b: Media) => { - switch ($settings.displayAnimeSort) { - case 'difference': { - const difference = (anime: Media) => - (anime.nextAiringEpisode?.episode === -1 - ? 99999 - : anime.nextAiringEpisode?.episode || -1) - - (anime.mediaListEntry || { progress: 0 }).progress; + outdatedCompletedAnime.sort((a: Media, b: Media) => { + switch ($settings.displayAnimeSort) { + case "difference": { + const difference = (anime: Media) => + (anime.nextAiringEpisode?.episode === -1 + ? 99999 + : anime.nextAiringEpisode?.episode || -1) - + (anime.mediaListEntry || { progress: 0 }).progress; - return difference(a) - difference(b); - } + return difference(a) - difference(b); + } - case 'end_date': - return ( - new Date(a.endDate.year, a.endDate.month - 1).getTime() - - new Date(b.endDate.year, b.endDate.month - 1).getTime() - ); + case "end_date": + return ( + new Date(a.endDate.year, a.endDate.month - 1).getTime() - + new Date(b.endDate.year, b.endDate.month - 1).getTime() + ); - case 'start_date': - return ( - new Date(a.startDate.year, a.startDate.month - 1).getTime() - - new Date(b.startDate.year, b.startDate.month - 1).getTime() - ); + case "start_date": + return ( + new Date(a.startDate.year, a.startDate.month - 1).getTime() - + new Date(b.startDate.year, b.startDate.month - 1).getTime() + ); - case 'time_remaining': - return (a.nextAiringEpisode?.airingAt || 9999) - (b.nextAiringEpisode?.airingAt || 9999); + case "time_remaining": + return ( + (a.nextAiringEpisode?.airingAt || 9999) - + (b.nextAiringEpisode?.airingAt || 9999) + ); - default: - return 0; - } - }); + default: + return 0; + } + }); - if (!endTime) endTime = performance.now() - startTime; + if (!endTime) endTime = performance.now() - startTime; - return outdatedCompletedAnime; + return outdatedCompletedAnime; }; </script> diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte index 69b84b8f..4da6836f 100644 --- a/src/lib/List/Anime/DueAnimeList.svelte +++ b/src/lib/List/Anime/DueAnimeList.svelte @@ -1,17 +1,20 @@ <script lang="ts"> -import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import { onDestroy, onMount } from 'svelte'; -import anime from '$stores/anime'; -import settings from '$stores/settings'; -import lastPruneTimes from '$stores/lastPruneTimes'; -import AnimeList from './AnimeListTemplate.svelte'; -import type { SubsPlease } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; -import { injectAiringTime } from '$lib/Media/Anime/Airing/Subtitled/match'; -import { hasDueEpisodes, hasNoAiredEpisodes } from '$lib/Media/Anime/Airing/classify'; -import { addNotification } from '$lib/Notification/store'; -import locale from '$stores/locale'; -import identity from '$stores/identity'; +import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media"; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import { onDestroy, onMount } from "svelte"; +import anime from "$stores/anime"; +import settings from "$stores/settings"; +import lastPruneTimes from "$stores/lastPruneTimes"; +import AnimeList from "./AnimeListTemplate.svelte"; +import type { SubsPlease } from "$lib/Media/Anime/Airing/Subtitled/subsPlease"; +import { injectAiringTime } from "$lib/Media/Anime/Airing/Subtitled/match"; +import { + hasDueEpisodes, + hasNoAiredEpisodes, +} from "$lib/Media/Anime/Airing/classify"; +import { addNotification } from "$lib/Notification/store"; +import locale from "$stores/locale"; +import identity from "$stores/identity"; export let user: AniListAuthorisation; let animeLists: Promise<Media[]>; @@ -21,101 +24,124 @@ let keyCacher: ReturnType<typeof setInterval> | undefined; let keyCacheMinutes = -1; const restartKeyCacher = (cacheMinutes: number) => { - if (keyCacher) clearInterval(keyCacher); - - keyCacheMinutes = cacheMinutes; - keyCacher = setInterval( - () => { - startTime = performance.now(); - endTime = -1; - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - forcePrune: true, - addNotification - }); - }, - cacheMinutes * 1000 * 60 - ); + if (keyCacher) clearInterval(keyCacher); + + keyCacheMinutes = cacheMinutes; + keyCacher = setInterval( + () => { + startTime = performance.now(); + endTime = -1; + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + forcePrune: true, + addNotification, + }, + ); + }, + cacheMinutes * 1000 * 60, + ); }; onMount(async () => { - restartKeyCacher($settings.cacheMinutes); - - startTime = performance.now(); - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - addNotification - }); + restartKeyCacher($settings.cacheMinutes); + + startTime = performance.now(); + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + addNotification, + }, + ); }); $: if (keyCacher && keyCacheMinutes !== $settings.cacheMinutes) - restartKeyCacher($settings.cacheMinutes); + restartKeyCacher($settings.cacheMinutes); onDestroy(() => { - if (keyCacher) clearInterval(keyCacher); + if (keyCacher) clearInterval(keyCacher); }); -const cleanMedia = (anime: Media[], displayUnresolved: boolean, subsPlease: SubsPlease | null) => { - if (anime === undefined) return []; - - let dueAnime = anime - .map((media) => injectAiringTime(media, subsPlease)) - .filter( - // Releasing media - (media: Media) => - media.status === 'RELEASING' && - (media.mediaListEntry || { status: 'DROPPED' }).status !== - ($settings.displayPausedMedia ? '' : 'PAUSED') && - (media.mediaListEntry || { progress: 0 }).progress >= - ($settings.displayNotStarted === true ? 0 : 1) && - (media.mediaListEntry || { status: 'DROPPED' }).status !== 'DROPPED' - ) - .filter((media: Media) => - // Outdated media - hasDueEpisodes(media) - ) - .map((media: Media) => { - if (hasNoAiredEpisodes(media)) media.nextAiringEpisode = { episode: -1 }; - - return media; - }); - - if (!displayUnresolved) - dueAnime = dueAnime.filter((media: Media) => media.nextAiringEpisode?.episode !== -1); - - dueAnime.sort((a: Media, b: Media) => { - switch ($settings.displayAnimeSort) { - case 'difference': { - const difference = (anime: Media) => - (anime.nextAiringEpisode?.episode === -1 - ? 99999 - : anime.nextAiringEpisode?.episode || -1) - - (anime.mediaListEntry || { progress: 0 }).progress; - - return difference(a) - difference(b); - } - - case 'end_date': - return ( - new Date(a.endDate.year, a.endDate.month - 1).getTime() - - new Date(b.endDate.year, b.endDate.month - 1).getTime() - ); - - case 'start_date': - return ( - new Date(a.startDate.year, a.startDate.month - 1).getTime() - - new Date(b.startDate.year, b.startDate.month - 1).getTime() - ); - - case 'time_remaining': - return (a.nextAiringEpisode?.airingAt || 9999) - (b.nextAiringEpisode?.airingAt || 9999); - - default: - return 0; - } - }); - - if (!endTime || endTime === -1) endTime = performance.now() - startTime; - - return dueAnime; +const cleanMedia = ( + anime: Media[], + displayUnresolved: boolean, + subsPlease: SubsPlease | null, +) => { + if (anime === undefined) return []; + + let dueAnime = anime + .map((media) => injectAiringTime(media, subsPlease)) + .filter( + // Releasing media + (media: Media) => + media.status === "RELEASING" && + (media.mediaListEntry || { status: "DROPPED" }).status !== + ($settings.displayPausedMedia ? "" : "PAUSED") && + (media.mediaListEntry || { progress: 0 }).progress >= + ($settings.displayNotStarted === true ? 0 : 1) && + (media.mediaListEntry || { status: "DROPPED" }).status !== "DROPPED", + ) + .filter((media: Media) => + // Outdated media + hasDueEpisodes(media), + ) + .map((media: Media) => { + if (hasNoAiredEpisodes(media)) media.nextAiringEpisode = { episode: -1 }; + + return media; + }); + + if (!displayUnresolved) + dueAnime = dueAnime.filter( + (media: Media) => media.nextAiringEpisode?.episode !== -1, + ); + + dueAnime.sort((a: Media, b: Media) => { + switch ($settings.displayAnimeSort) { + case "difference": { + const difference = (anime: Media) => + (anime.nextAiringEpisode?.episode === -1 + ? 99999 + : anime.nextAiringEpisode?.episode || -1) - + (anime.mediaListEntry || { progress: 0 }).progress; + + return difference(a) - difference(b); + } + + case "end_date": + return ( + new Date(a.endDate.year, a.endDate.month - 1).getTime() - + new Date(b.endDate.year, b.endDate.month - 1).getTime() + ); + + case "start_date": + return ( + new Date(a.startDate.year, a.startDate.month - 1).getTime() - + new Date(b.startDate.year, b.startDate.month - 1).getTime() + ); + + case "time_remaining": + return ( + (a.nextAiringEpisode?.airingAt || 9999) - + (b.nextAiringEpisode?.airingAt || 9999) + ); + + default: + return 0; + } + }); + + if (!endTime || endTime === -1) endTime = performance.now() - startTime; + + return dueAnime; }; </script> diff --git a/src/lib/List/Anime/DueIndexColumn.svelte b/src/lib/List/Anime/DueIndexColumn.svelte index 6ffc17be..1df355a4 100644 --- a/src/lib/List/Anime/DueIndexColumn.svelte +++ b/src/lib/List/Anime/DueIndexColumn.svelte @@ -1,17 +1,17 @@ <script lang="ts"> -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import Skeleton from '$lib/Loading/Skeleton.svelte'; -import locale from '$stores/locale'; -import ListTitle from '../ListTitle.svelte'; -import AnimeList from '$lib/List/Anime/DueAnimeList.svelte'; -import { onMount } from 'svelte'; -import stateBin from '$stores/stateBin'; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import locale from "$stores/locale"; +import ListTitle from "../ListTitle.svelte"; +import AnimeList from "$lib/List/Anime/DueAnimeList.svelte"; +import { onMount } from "svelte"; +import stateBin from "$stores/stateBin"; export let userIdentity: { id: number }; export let user: AniListAuthorisation; onMount(() => { - $stateBin.dueAnimeListOpen ??= true; + $stateBin.dueAnimeListOpen ??= true; }); </script> diff --git a/src/lib/List/Anime/PlaceholderList.svelte b/src/lib/List/Anime/PlaceholderList.svelte index 4ac0d8ff..4009d003 100644 --- a/src/lib/List/Anime/PlaceholderList.svelte +++ b/src/lib/List/Anime/PlaceholderList.svelte @@ -1,8 +1,8 @@ <script lang="ts"> -import Skeleton from '$lib/Loading/Skeleton.svelte'; -import settings from '$stores/settings'; -import ListTitle from '../ListTitle.svelte'; -import type { Title } from '../mediaTitle'; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import settings from "$stores/settings"; +import ListTitle from "../ListTitle.svelte"; +import type { Title } from "../mediaTitle"; export let title: Title; export let count = 8; diff --git a/src/lib/List/Anime/UpcomingAnimeList.svelte b/src/lib/List/Anime/UpcomingAnimeList.svelte index 109584f0..2dd69a68 100644 --- a/src/lib/List/Anime/UpcomingAnimeList.svelte +++ b/src/lib/List/Anime/UpcomingAnimeList.svelte @@ -1,19 +1,22 @@ <script lang="ts"> -import Spacer from '$lib/Layout/Spacer.svelte'; -import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import { onMount } from 'svelte'; -import anime from '$stores/anime'; -import lastPruneTimes from '$stores/lastPruneTimes'; -import AnimeList from './AnimeListTemplate.svelte'; -import settings from '$stores/settings'; -import type { SubsPlease } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; -import { addNotification } from '$lib/Notification/store'; -import locale from '$stores/locale'; -import identity from '$stores/identity'; -import { injectAiringTime } from '$lib/Media/Anime/Airing/Subtitled/match'; -import { hasDueEpisodes, hasNoAiredEpisodes } from '$lib/Media/Anime/Airing/classify'; -import revalidateAnime from '$stores/revalidateAnime'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media"; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import { onMount } from "svelte"; +import anime from "$stores/anime"; +import lastPruneTimes from "$stores/lastPruneTimes"; +import AnimeList from "./AnimeListTemplate.svelte"; +import settings from "$stores/settings"; +import type { SubsPlease } from "$lib/Media/Anime/Airing/Subtitled/subsPlease"; +import { addNotification } from "$lib/Notification/store"; +import locale from "$stores/locale"; +import identity from "$stores/identity"; +import { injectAiringTime } from "$lib/Media/Anime/Airing/Subtitled/match"; +import { + hasDueEpisodes, + hasNoAiredEpisodes, +} from "$lib/Media/Anime/Airing/classify"; +import revalidateAnime from "$stores/revalidateAnime"; export let user: AniListAuthorisation; let animeLists: Promise<Media[]>; @@ -21,65 +24,88 @@ let startTime: number; let endTime: number; onMount(async () => { - startTime = performance.now(); - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - addNotification, - notificationType: 'Upcoming Episodes' - }); + startTime = performance.now(); + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + addNotification, + notificationType: "Upcoming Episodes", + }, + ); }); const cleanMedia = ( - anime: Media[], - displayUnresolved: boolean, - subsPlease: SubsPlease | null, - plannedOnly = true + anime: Media[], + displayUnresolved: boolean, + subsPlease: SubsPlease | null, + plannedOnly = true, ) => { - if (anime === undefined) return []; + if (anime === undefined) return []; - const filterAnime = (status: 'RELEASING' | 'NOT_YET_RELEASED') => - anime - .filter((media: Media) => media.status === status && media.nextAiringEpisode !== null) - .map((media) => injectAiringTime(media, subsPlease)) - .filter( - (media: Media) => - // Outdated media - ($settings.displayPlannedAnime ? media.mediaListEntry?.status === 'PLANNING' : false) || - !hasDueEpisodes(media) - ) - .map((media: Media) => { - // Adjust for planned anime - if ( - ($settings.displayPlannedAnime ? media.episodes !== 1 : true) && - hasNoAiredEpisodes(media) - ) - media.nextAiringEpisode = { episode: -1 }; + const filterAnime = (status: "RELEASING" | "NOT_YET_RELEASED") => + anime + .filter( + (media: Media) => + media.status === status && media.nextAiringEpisode !== null, + ) + .map((media) => injectAiringTime(media, subsPlease)) + .filter( + (media: Media) => + // Outdated media + ($settings.displayPlannedAnime + ? media.mediaListEntry?.status === "PLANNING" + : false) || !hasDueEpisodes(media), + ) + .map((media: Media) => { + // Adjust for planned anime + if ( + ($settings.displayPlannedAnime ? media.episodes !== 1 : true) && + hasNoAiredEpisodes(media) + ) + media.nextAiringEpisode = { episode: -1 }; - return media; - }); - let upcomingAnime = filterAnime(plannedOnly ? 'NOT_YET_RELEASED' : 'RELEASING'); + return media; + }); + let upcomingAnime = filterAnime( + plannedOnly ? "NOT_YET_RELEASED" : "RELEASING", + ); - if (!displayUnresolved) - upcomingAnime = upcomingAnime.filter((media: Media) => media.nextAiringEpisode?.episode !== -1); + if (!displayUnresolved) + upcomingAnime = upcomingAnime.filter( + (media: Media) => media.nextAiringEpisode?.episode !== -1, + ); - upcomingAnime.sort( - (a: Media, b: Media) => - (a.nextAiringEpisode?.airingAt || 9999) - (b.nextAiringEpisode?.airingAt || 9999) - ); + upcomingAnime.sort( + (a: Media, b: Media) => + (a.nextAiringEpisode?.airingAt || 9999) - + (b.nextAiringEpisode?.airingAt || 9999), + ); - if (!endTime) endTime = performance.now() - startTime; + if (!endTime) endTime = performance.now() - startTime; - return upcomingAnime; + return upcomingAnime; }; $: { - if ($revalidateAnime) { - $revalidateAnime = false; - $lastPruneTimes.anime = -1; - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - addNotification, - notificationType: 'Upcoming Episodes' - }); - } + if ($revalidateAnime) { + $revalidateAnime = false; + $lastPruneTimes.anime = -1; + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + addNotification, + notificationType: "Upcoming Episodes", + }, + ); + } } </script> diff --git a/src/lib/List/CleanGrid.svelte b/src/lib/List/CleanGrid.svelte index 08a7ef83..006a5e4e 100644 --- a/src/lib/List/CleanGrid.svelte +++ b/src/lib/List/CleanGrid.svelte @@ -1,15 +1,15 @@ <script lang="ts"> -import type { Media } from '$lib/Data/AniList/media'; -import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; -import { outboundLink } from '$lib/Media/links'; -import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; -import settings from '$stores/settings'; -import { mediaTitle } from './mediaTitle'; -import './covers.css'; +import type { Media } from "$lib/Data/AniList/media"; +import ParallaxImage from "$lib/Image/ParallaxImage.svelte"; +import { outboundLink } from "$lib/Media/links"; +import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte"; +import settings from "$stores/settings"; +import { mediaTitle } from "./mediaTitle"; +import "./covers.css"; export let media: Media[]; export let dummy = false; -export let type: 'anime' | 'manga'; +export let type: "anime" | "manga"; export let upcoming = false; export let notYetReleased = false; export let reverseSort = false; @@ -18,7 +18,8 @@ export let limit: number | undefined = undefined; let uniqueID = new Date().getTime(); $: sortedMedia = reverseSort ? [...media].reverse() : media; -$: processedMedia = limit !== undefined ? sortedMedia.slice(0, limit) : sortedMedia; +$: processedMedia = + limit !== undefined ? sortedMedia.slice(0, limit) : sortedMedia; </script> <div diff --git a/src/lib/List/CleanList.svelte b/src/lib/List/CleanList.svelte index 377b9302..0ae3c65b 100644 --- a/src/lib/List/CleanList.svelte +++ b/src/lib/List/CleanList.svelte @@ -1,12 +1,12 @@ <script lang="ts"> -import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; -import type { Media } from '$lib/Data/AniList/media'; -import { outboundLink } from '$lib/Media/links'; -import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; -import settings from '$stores/settings'; +import MediaTitleDisplay from "$lib/List/MediaTitleDisplay.svelte"; +import type { Media } from "$lib/Data/AniList/media"; +import { outboundLink } from "$lib/Media/links"; +import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte"; +import settings from "$stores/settings"; export let media: Media[]; -export let type: 'anime' | 'manga'; +export let type: "anime" | "manga"; export let upcoming = false; export let notYetReleased = false; export let lastUpdatedMedia: number; diff --git a/src/lib/List/ListTitle.svelte b/src/lib/List/ListTitle.svelte index 71b5e649..d000e91f 100644 --- a/src/lib/List/ListTitle.svelte +++ b/src/lib/List/ListTitle.svelte @@ -1,12 +1,12 @@ <script lang="ts"> -import tooltip from '$lib/Tooltip/tooltip'; -import type { Title } from './mediaTitle'; +import tooltip from "$lib/Tooltip/tooltip"; +import type { Title } from "./mediaTitle"; export let time: number | undefined = undefined; export let count = -1337; export let title: Title = { - title: 'Media List', - hint: 'This is a media list.' + title: "Media List", + hint: "This is a media list.", }; export let progress: undefined | number = undefined; export let hideTime = false; diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte index 428c45f2..cacc7006 100644 --- a/src/lib/List/Manga/CleanMangaList.svelte +++ b/src/lib/List/Manga/CleanMangaList.svelte @@ -1,28 +1,32 @@ <script lang="ts"> -import Spacer from '$lib/Layout/Spacer.svelte'; -import type { Media } from '$lib/Data/AniList/media'; -import RateLimitedError from '$lib/Error/RateLimited.svelte'; -import { volumeCount } from '$lib/Media/Manga/volumes'; -import settings from '$stores/settings'; -import ListTitle from '../ListTitle.svelte'; -import { onMount } from 'svelte'; -import root from '$lib/Utility/root'; -import locale from '$stores/locale'; -import Skeleton from '$lib/Loading/Skeleton.svelte'; -import { browser } from '$app/environment'; -import proxy from '$lib/Utility/proxy'; -import '../covers.css'; -import CleanGrid from '../CleanGrid.svelte'; -import CleanList from '../CleanList.svelte'; -import stateBin from '$stores/stateBin'; -import localforage from 'localforage'; -import MediaRoulette from '../MediaRoulette.svelte'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import type { Media } from "$lib/Data/AniList/media"; +import RateLimitedError from "$lib/Error/RateLimited.svelte"; +import { volumeCount } from "$lib/Media/Manga/volumes"; +import settings from "$stores/settings"; +import ListTitle from "../ListTitle.svelte"; +import { onMount } from "svelte"; +import root from "$lib/Utility/root"; +import locale from "$stores/locale"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import { browser } from "$app/environment"; +import proxy from "$lib/Utility/proxy"; +import "../covers.css"; +import CleanGrid from "../CleanGrid.svelte"; +import CleanList from "../CleanList.svelte"; +import stateBin from "$stores/stateBin"; +import localforage from "localforage"; +import MediaRoulette from "../MediaRoulette.svelte"; export let media: Media[]; export let cleanCache: () => void; export let endTime: number; export let lastUpdatedMedia: number; -export let updateMedia: (id: number, progress: number | undefined, media: Media[]) => Promise<void>; +export let updateMedia: ( + id: number, + progress: number | undefined, + media: Media[], +) => Promise<void>; export let pendingUpdate: number | null; export let due: boolean; export let rateLimited: boolean; @@ -34,45 +38,48 @@ export let limit: number | undefined = undefined; let showRoulette = false; let serviceStatusResponse: Promise<Response>; let totalEpisodeDueCount = media - .map((manga) => { - if ($settings.displayTotalEpisodes && !$settings.displayTotalDueEpisodes) return 1; + .map((manga) => { + if ($settings.displayTotalEpisodes && !$settings.displayTotalDueEpisodes) + return 1; - if (!due && !$settings.displayTotalEpisodes) return 1; + if (!due && !$settings.displayTotalEpisodes) return 1; - return (manga.episodes || 1) - (manga.mediaListEntry?.progress || 0); - }) - .reduce((a, b) => a + b, 0); + return (manga.episodes || 1) - (manga.mediaListEntry?.progress || 0); + }) + .reduce((a, b) => a + b, 0); const lists = Array.from( - new Set( - media - .flatMap((m) => Object.entries(m.mediaListEntry?.customLists ?? {})) - .filter(([_key, value]) => value) - .map(([key]) => key) - ) + new Set( + media + .flatMap((m) => Object.entries(m.mediaListEntry?.customLists ?? {})) + .filter(([_key, value]) => value) + .map(([key]) => key), + ), ); -const filterKind = due ? 'due' : 'completed'; +const filterKind = due ? "due" : "completed"; const filterKey = `${filterKind}MangaListFilter`; -$: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All'; +$: selectedList = disableFilter + ? "All" + : ($stateBin[filterKey] as string) || "All"; $: filteredMedia = - selectedList === 'All' || !$settings.displayMediaListFilter - ? media - : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]); + selectedList === "All" || !$settings.displayMediaListFilter + ? media + : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]); onMount(async () => { - serviceStatusResponse = fetch(proxy('https://api.mangadex.org/ping')); + serviceStatusResponse = fetch(proxy("https://api.mangadex.org/ping")); - if (browser) - await localforage.setItem( - `last${due ? '' : 'Completed'}MangaListLength`, - media.length.toString() - ); + if (browser) + await localforage.setItem( + `last${due ? "" : "Completed"}MangaListLength`, + media.length.toString(), + ); }); const increment = (manga: Media) => { - if (!(pendingUpdate === manga.id || dummy)) - updateMedia(manga.id, manga.mediaListEntry?.progress, media); + if (!(pendingUpdate === manga.id || dummy)) + updateMedia(manga.id, manga.mediaListEntry?.progress, media); }; </script> diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte index 0b967cb2..c9f66dd4 100644 --- a/src/lib/List/Manga/MangaListTemplate.svelte +++ b/src/lib/List/Manga/MangaListTemplate.svelte @@ -1,32 +1,32 @@ <script lang="ts"> -import sampleManga from '$lib/Data/Static/SampleMedia/manga.json'; -import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; -import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; -import { onDestroy, onMount } from 'svelte'; -import { chapterCount } from '$lib/Media/Manga/chapters'; -import { pruneAllManga } from '$lib/Media/Manga/cache'; -import manga from '$stores/manga'; -import { database } from '$lib/Database/IDB/chapters'; -import settings from '$stores/settings'; -import lastPruneTimes from '$stores/lastPruneTimes'; -import ListTitle from '../ListTitle.svelte'; -import RateLimitedError from '$lib/Error/RateLimited.svelte'; -import CleanMangaList from './CleanMangaList.svelte'; -import { incrementMediaProgress } from '$lib/Media/Anime/cache'; -import { addNotification } from '$lib/Notification/store'; -import { options } from '$lib/Notification/options'; -import Skeleton from '$lib/Loading/Skeleton.svelte'; -import locale from '$stores/locale'; -import { browser } from '$app/environment'; -import identity from '$stores/identity'; -import privilegedUser from '$lib/Utility/privilegedUser'; -import localforage from 'localforage'; +import sampleManga from "$lib/Data/Static/SampleMedia/manga.json"; +import { mediaListCollection, Type, type Media } from "$lib/Data/AniList/media"; +import type { AniListAuthorisation } from "$lib/Data/AniList/identity"; +import { onDestroy, onMount } from "svelte"; +import { chapterCount } from "$lib/Media/Manga/chapters"; +import { pruneAllManga } from "$lib/Media/Manga/cache"; +import manga from "$stores/manga"; +import { database } from "$lib/Database/IDB/chapters"; +import settings from "$stores/settings"; +import lastPruneTimes from "$stores/lastPruneTimes"; +import ListTitle from "../ListTitle.svelte"; +import RateLimitedError from "$lib/Error/RateLimited.svelte"; +import CleanMangaList from "./CleanMangaList.svelte"; +import { incrementMediaProgress } from "$lib/Media/Anime/cache"; +import { addNotification } from "$lib/Notification/store"; +import { options } from "$lib/Notification/options"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import locale from "$stores/locale"; +import { browser } from "$app/environment"; +import identity from "$stores/identity"; +import privilegedUser from "$lib/Utility/privilegedUser"; +import localforage from "localforage"; export let user: AniListAuthorisation = { - accessToken: '', - refreshToken: '', - expiresIn: 0, - tokenType: '' + accessToken: "", + refreshToken: "", + expiresIn: 0, + tokenType: "", }; export let displayUnresolved: boolean; export let due: boolean; @@ -49,215 +49,265 @@ let keyCacher: ReturnType<typeof setInterval> | undefined; let keyCacheMinutes = -1; const restartKeyCacher = (cacheMinutes: number) => { - if (keyCacher) clearInterval(keyCacher); - - keyCacheMinutes = cacheMinutes; - keyCacher = setInterval( - () => { - startTime = performance.now(); - endTime = -1; - mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, { - addNotification - }); - }, - cacheMinutes * 1000 * 60 - ); + if (keyCacher) clearInterval(keyCacher); + + keyCacheMinutes = cacheMinutes; + keyCacher = setInterval( + () => { + startTime = performance.now(); + endTime = -1; + mangaLists = mediaListCollection( + user, + $identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + addNotification, + }, + ); + }, + cacheMinutes * 1000 * 60, + ); }; onMount(async () => { - restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); - - if (browser) { - const lastStoredList = (await localforage.getItem( - `last${due ? '' : 'Completed'}MangaListLength` - )) as number | null; - - if (lastStoredList) lastListSize = parseInt(String(lastStoredList)); - } - - startTime = performance.now(); - - if (dummy) { - // Use deterministic selection for consistent display - const filtered = sampleManga.filter( - (manga) => - manga.chapters && - !manga.tags.some((tag) => tag.name === 'Nudity') && - !manga.tags.some((tag) => tag.name === 'Rape') && - !manga.tags.some((tag) => tag.name === 'Tragedy') && - !manga.tags.some((tag) => tag.name === 'Bondage') && - !manga.genres.some((genre) => genre === 'Hentai') && - manga.genres.some((genre) => genre === 'Comedy') && - manga.status !== 'NOT_YET_RELEASED' - ); - mangaLists = Promise.resolve( - filtered.slice(0, dummyCount).map((manga) => { - manga.status = 'FINISHED'; - manga.episodes = Math.floor((manga.chapters || 10) * 0.7) as unknown as null; - manga.mediaListEntry.progress = Math.floor((manga.episodes || 5) * 0.5) + 1; - return manga; - }) as unknown as Media[] - ); - } else { - mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, { - addNotification - }); - } + restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); + + if (browser) { + const lastStoredList = (await localforage.getItem( + `last${due ? "" : "Completed"}MangaListLength`, + )) as number | null; + + if (lastStoredList) lastListSize = parseInt(String(lastStoredList)); + } + + startTime = performance.now(); + + if (dummy) { + // Use deterministic selection for consistent display + const filtered = sampleManga.filter( + (manga) => + manga.chapters && + !manga.tags.some((tag) => tag.name === "Nudity") && + !manga.tags.some((tag) => tag.name === "Rape") && + !manga.tags.some((tag) => tag.name === "Tragedy") && + !manga.tags.some((tag) => tag.name === "Bondage") && + !manga.genres.some((genre) => genre === "Hentai") && + manga.genres.some((genre) => genre === "Comedy") && + manga.status !== "NOT_YET_RELEASED", + ); + mangaLists = Promise.resolve( + filtered.slice(0, dummyCount).map((manga) => { + manga.status = "FINISHED"; + manga.episodes = Math.floor( + (manga.chapters || 10) * 0.7, + ) as unknown as null; + manga.mediaListEntry.progress = + Math.floor((manga.episodes || 5) * 0.5) + 1; + return manga; + }) as unknown as Media[], + ); + } else { + mangaLists = mediaListCollection( + user, + $identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + addNotification, + }, + ); + } }); -$: if (keyCacher && keyCacheMinutes !== Math.max($settings.cacheMangaMinutes, 5)) - restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); +$: if ( + keyCacher && + keyCacheMinutes !== Math.max($settings.cacheMangaMinutes, 5) +) + restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); onDestroy(() => { - if (keyCacher) clearInterval(keyCacher); + if (keyCacher) clearInterval(keyCacher); }); -const cleanMedia = async (manga: Media[], displayUnresolved: boolean, force: boolean) => { - progress = 0; - - if (manga && dummy) return manga; - - if (manga === undefined) return []; - - if (!authorised && (await database.chapters.toArray()).length <= 0 && !force) return []; - - if (authorised) { - let refreshing = false; - - if ($lastPruneTimes.chapters === 1) { - refreshing = true; - - lastPruneTimes.setKey('chapters', new Date().getTime()); - } else { - const currentDate = new Date(); - - if ( - (currentDate.getTime() - $lastPruneTimes.chapters) / 1000 / 60 > - Math.max($settings.cacheMangaMinutes, 5) - ) { - refreshing = true; - - lastPruneTimes.setKey('chapters', currentDate.getTime()); - (async () => { - await database.chapters.bulkDelete((await database.chapters.toArray()).map((m) => m.id)); - })(); - } - } - - if (refreshing) { - addNotification( - options({ - heading: 'Manga', - description: 'Re-freshing manga data ...' - }) - ); - } - } - - const releasingMedia = manga.filter( - (media: Media) => - (due ? media.status === 'RELEASING' : media.status === 'FINISHED') && - (media.mediaListEntry || { status: 'DROPPED' }).status !== - ($settings.displayPausedMedia ? '' : 'PAUSED') && - (media.mediaListEntry || { status: 'DROPPED' }).status !== 'DROPPED' && - (media.mediaListEntry || { progress: 0 }).progress >= - ($settings.displayNotStarted === true ? 0 : 1) - ); - let finalMedia = releasingMedia; - const progressStep = 100 / finalMedia.length / 2; - const chapterPromises = finalMedia.map((m: Media) => - database.chapters.get(m.id).then((c) => { - if (progress < 100) progress += progressStep; - - if (!due) return new Promise((resolve) => resolve(m.chapters)) as Promise<number | null>; - - if (c !== undefined) return chapterCount($identity, m, $settings.calculateGuessingDisabled); - else { - // A = On 1 second interval, - // B = a maximum of 5 requests per second are allowed. - // C = chapterCount makes 3 requests per call. - // F = A / (B / C) = 0.6 seconds - return new Promise((resolve) => setTimeout(resolve, 600)).then(() => - chapterCount($identity, m, $settings.calculateGuessingDisabled) - ); - } - }) - ); - const chapterCounts: (number | null)[] = []; - - for (let i = 0; i < chapterPromises.length; i++) { - const count = await chapterPromises[i]; - - if (count === -22) { - rateLimited = true; - - break; - } - - chapterCounts.push(count); - - if (progress < 100) progress += progressStep; - } - - 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) => - (a.episodes || 9999) - - (a.mediaListEntry || { progress: 0 }).progress - - ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress) - ); - - finalMedia = finalMedia.filter( - (item, index, array) => - array.findIndex((i) => i.id === item.id) === index && - (item.episodes === -1337 && displayUnresolved - ? true - : (item.mediaListEntry?.progress || 0) < - ($settings.calculateChaptersRoundedDown === true - ? Math.floor(item.episodes) - : item.episodes)) - ); - - if (!endTime || endTime === -1) endTime = performance.now() - startTime; - - return finalMedia; +const cleanMedia = async ( + manga: Media[], + displayUnresolved: boolean, + force: boolean, +) => { + progress = 0; + + if (manga && dummy) return manga; + + if (manga === undefined) return []; + + if (!authorised && (await database.chapters.toArray()).length <= 0 && !force) + return []; + + if (authorised) { + let refreshing = false; + + if ($lastPruneTimes.chapters === 1) { + refreshing = true; + + lastPruneTimes.setKey("chapters", new Date().getTime()); + } else { + const currentDate = new Date(); + + if ( + (currentDate.getTime() - $lastPruneTimes.chapters) / 1000 / 60 > + Math.max($settings.cacheMangaMinutes, 5) + ) { + refreshing = true; + + lastPruneTimes.setKey("chapters", currentDate.getTime()); + (async () => { + await database.chapters.bulkDelete( + (await database.chapters.toArray()).map((m) => m.id), + ); + })(); + } + } + + if (refreshing) { + addNotification( + options({ + heading: "Manga", + description: "Re-freshing manga data ...", + }), + ); + } + } + + const releasingMedia = manga.filter( + (media: Media) => + (due ? media.status === "RELEASING" : media.status === "FINISHED") && + (media.mediaListEntry || { status: "DROPPED" }).status !== + ($settings.displayPausedMedia ? "" : "PAUSED") && + (media.mediaListEntry || { status: "DROPPED" }).status !== "DROPPED" && + (media.mediaListEntry || { progress: 0 }).progress >= + ($settings.displayNotStarted === true ? 0 : 1), + ); + let finalMedia = releasingMedia; + const progressStep = 100 / finalMedia.length / 2; + const chapterPromises = finalMedia.map((m: Media) => + database.chapters.get(m.id).then((c) => { + if (progress < 100) progress += progressStep; + + if (!due) + return new Promise((resolve) => resolve(m.chapters)) as Promise< + number | null + >; + + if (c !== undefined) + return chapterCount($identity, m, $settings.calculateGuessingDisabled); + else { + // A = On 1 second interval, + // B = a maximum of 5 requests per second are allowed. + // C = chapterCount makes 3 requests per call. + // F = A / (B / C) = 0.6 seconds + return new Promise((resolve) => setTimeout(resolve, 600)).then(() => + chapterCount($identity, m, $settings.calculateGuessingDisabled), + ); + } + }), + ); + const chapterCounts: (number | null)[] = []; + + for (let i = 0; i < chapterPromises.length; i++) { + const count = await chapterPromises[i]; + + if (count === -22) { + rateLimited = true; + + break; + } + + chapterCounts.push(count); + + if (progress < 100) progress += progressStep; + } + + 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) => + (a.episodes || 9999) - + (a.mediaListEntry || { progress: 0 }).progress - + ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress), + ); + + finalMedia = finalMedia.filter( + (item, index, array) => + array.findIndex((i) => i.id === item.id) === index && + (item.episodes === -1337 && displayUnresolved + ? true + : (item.mediaListEntry?.progress || 0) < + ($settings.calculateChaptersRoundedDown === 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, media: Media[]) => { - pendingUpdate = id; - lastUpdatedMedia = id; - - await database.chapters.delete(id); - - incrementMediaProgress(id, progress, user, () => { - previousMangaList = media; - - const foundEntry = media.find((m) => m.id === id); - - if (foundEntry && foundEntry.mediaListEntry) - foundEntry.mediaListEntry.progress = (progress || 0) + 1; - - mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, { - forcePrune: true - }); - pendingUpdate = null; - }); +const updateMedia = async ( + id: number, + progress: number | undefined, + media: Media[], +) => { + pendingUpdate = id; + lastUpdatedMedia = id; + + await database.chapters.delete(id); + + incrementMediaProgress(id, progress, user, () => { + previousMangaList = media; + + const foundEntry = media.find((m) => m.id === id); + + if (foundEntry && foundEntry.mediaListEntry) + foundEntry.mediaListEntry.progress = (progress || 0) + 1; + + mangaLists = mediaListCollection( + user, + $identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + forcePrune: true, + }, + ); + pendingUpdate = null; + }); }; const cleanCache = () => { - startTime = performance.now(); - endTime = -1; - - pruneAllManga().then(() => { - mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, { - forcePrune: true - }); - }); + startTime = performance.now(); + endTime = -1; + + pruneAllManga().then(() => { + mangaLists = mediaListCollection( + user, + $identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + forcePrune: true, + }, + ); + }); }; </script> diff --git a/src/lib/List/MediaRoulette.svelte b/src/lib/List/MediaRoulette.svelte index b4a6d527..6196c669 100644 --- a/src/lib/List/MediaRoulette.svelte +++ b/src/lib/List/MediaRoulette.svelte @@ -1,15 +1,15 @@ <script lang="ts"> -import type { Media } from '$lib/Data/AniList/media'; -import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; -import { outboundLink } from '$lib/Media/links'; -import settings from '$stores/settings'; -import { mediaTitle } from './mediaTitle'; +import type { Media } from "$lib/Data/AniList/media"; +import ParallaxImage from "$lib/Image/ParallaxImage.svelte"; +import { outboundLink } from "$lib/Media/links"; +import settings from "$stores/settings"; +import { mediaTitle } from "./mediaTitle"; interface Props { - media: Media[]; - type: 'anime' | 'manga'; - onClose: () => void; - spinDuration?: number; + media: Media[]; + type: "anime" | "manga"; + onClose: () => void; + spinDuration?: number; } let { media, type, onClose, spinDuration = 2 }: Props = $props(); @@ -22,63 +22,63 @@ let isClosing = $state(false); let currentMedia = $derived(media[displayIndex]); const startRoulette = () => { - if (media.length === 0 || isSpinning) return; + if (media.length === 0 || isSpinning) return; - isSpinning = true; - showResult = false; - selectedIndex = Math.floor(Math.random() * media.length); + isSpinning = true; + showResult = false; + selectedIndex = Math.floor(Math.random() * media.length); - const startTime = Date.now(); - const durationMs = spinDuration * 1000; - const minSpeed = 50; - const maxSpeed = 350; - const slowdownStart = 0.8; + const startTime = Date.now(); + const durationMs = spinDuration * 1000; + const minSpeed = 50; + const maxSpeed = 350; + const slowdownStart = 0.8; - const spin = () => { - displayIndex = (displayIndex + 1) % media.length; + const spin = () => { + displayIndex = (displayIndex + 1) % media.length; - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / durationMs, 1); - let speed = minSpeed; + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / durationMs, 1); + let speed = minSpeed; - if (progress > slowdownStart) { - const slowdownProgress = (progress - slowdownStart) / (1 - slowdownStart); + if (progress > slowdownStart) { + const slowdownProgress = (progress - slowdownStart) / (1 - slowdownStart); - speed = minSpeed + slowdownProgress * (maxSpeed - minSpeed); - } + speed = minSpeed + slowdownProgress * (maxSpeed - minSpeed); + } - if (progress >= 1 && displayIndex === selectedIndex) { - spinTimeout = null; - isSpinning = false; - showResult = true; + if (progress >= 1 && displayIndex === selectedIndex) { + spinTimeout = null; + isSpinning = false; + showResult = true; - return; - } + return; + } - spinTimeout = setTimeout(spin, speed); - }; + spinTimeout = setTimeout(spin, speed); + }; - spinTimeout = setTimeout(spin, minSpeed); + spinTimeout = setTimeout(spin, minSpeed); }; const handleClose = () => { - if (isClosing) return; + if (isClosing) return; - if (spinTimeout) { - clearTimeout(spinTimeout); + if (spinTimeout) { + clearTimeout(spinTimeout); - spinTimeout = null; - } + spinTimeout = null; + } - isSpinning = false; - showResult = false; - isClosing = true; + isSpinning = false; + showResult = false; + isClosing = true; - setTimeout(() => onClose(), 200); + setTimeout(() => onClose(), 200); }; const handleOverlayClick = (e: MouseEvent) => { - if (e.target === e.currentTarget) handleClose(); + if (e.target === e.currentTarget) handleClose(); }; </script> diff --git a/src/lib/List/MediaTitleDisplay.svelte b/src/lib/List/MediaTitleDisplay.svelte index 4d379ee9..e7c26fb0 100644 --- a/src/lib/List/MediaTitleDisplay.svelte +++ b/src/lib/List/MediaTitleDisplay.svelte @@ -1,10 +1,10 @@ <script lang="ts"> -import type { MediaTitle } from '$lib/Data/AniList/media'; -import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; -import { abbreviate as abbreviated } from '$lib/Utility/string'; -import settings from '$stores/settings'; -import LZString from 'lz-string'; -import * as wanakana from 'wanakana'; +import type { MediaTitle } from "$lib/Data/AniList/media"; +import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte"; +import { abbreviate as abbreviated } from "$lib/Utility/string"; +import settings from "$stores/settings"; +import LZString from "lz-string"; +import * as wanakana from "wanakana"; export let title: MediaTitle; export let abbreviate = false; diff --git a/src/lib/List/covers.css b/src/lib/List/covers.css index f2bf45ca..94266257 100644 --- a/src/lib/List/covers.css +++ b/src/lib/List/covers.css @@ -1,77 +1,77 @@ .covers { - display: grid; - justify-content: center; - gap: 1em 0.5em; - margin-top: 0.5rem; + display: grid; + justify-content: center; + gap: 1em 0.5em; + margin-top: 0.5rem; } .cover { - background-size: cover; - background-position: center; - border-radius: 8px; - aspect-ratio: 46 / 65; - width: 100%; - height: auto; - object-fit: cover; + background-size: cover; + background-position: center; + border-radius: 8px; + aspect-ratio: 46 / 65; + width: 100%; + height: auto; + object-fit: cover; } .cover-title { - text-align: center; - margin: 0.25rem; + text-align: center; + margin: 0.25rem; } .cover-card { - display: inline-block; + display: inline-block; } .cover-card-image { - border-radius: 8px; - transition: - transform 0.45s ease, - box-shadow 0.45s ease; - margin-bottom: 0.5em; + border-radius: 8px; + transition: + transform 0.45s ease, + box-shadow 0.45s ease; + margin-bottom: 0.5em; } .cover-card-image:hover { - transform: scale(1.1); - position: relative; - transition: - transform 0.45s ease, - box-shadow 0.45s ease; + transform: scale(1.1); + position: relative; + transition: + transform 0.45s ease, + box-shadow 0.45s ease; } .entry::after { - content: ""; - display: table; - clear: both; + content: ""; + display: table; + clear: both; } .countdown { - white-space: nowrap; - float: right; + white-space: nowrap; + float: right; } .adult { - filter: blur(10px) grayscale(50%) brightness(0.5); - transition: filter 0.3s ease; + filter: blur(10px) grayscale(50%) brightness(0.5); + transition: filter 0.3s ease; } .adult:hover { - filter: blur(0) !important; - transition: filter 0.3s ease; + filter: blur(0) !important; + transition: filter 0.3s ease; } .cover-container { - display: flex; - justify-content: center; - align-items: center; - border-radius: 8px; - aspect-ratio: 46 / 65; - overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + aspect-ratio: 46 / 65; + overflow: hidden; } button { - margin-top: unset; - vertical-align: unset; - height: unset; + margin-top: unset; + vertical-align: unset; + height: unset; } diff --git a/src/lib/List/mediaTitle.ts b/src/lib/List/mediaTitle.ts index e59a3091..0e1d65cf 100644 --- a/src/lib/List/mediaTitle.ts +++ b/src/lib/List/mediaTitle.ts @@ -1,16 +1,21 @@ -import type { Media } from '$lib/Data/AniList/media'; -import settings from '$stores/settings'; -import { get } from 'svelte/store'; +import type { Media } from "$lib/Data/AniList/media"; +import settings from "$stores/settings"; +import { get } from "svelte/store"; export interface Title { - title: string; - hint: string; + title: string; + hint: string; } export const mediaTitle = (media: Media) => { - if (!media) return 'Loading ...'; + if (!media) return "Loading ..."; - const title = media.title; + const title = media.title; - return title[get(settings).displayTitleFormat] || title.english || title.romaji || title.native; + return ( + title[get(settings).displayTitleFormat] || + title.english || + title.romaji || + title.native + ); }; |