aboutsummaryrefslogtreecommitdiff
path: root/src/lib/List
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/List')
-rw-r--r--src/lib/List/Anime/AnimeListTemplate.svelte92
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte301
-rw-r--r--src/lib/List/Anime/CompletedAnimeList.svelte190
-rw-r--r--src/lib/List/Anime/DueAnimeList.svelte223
-rw-r--r--src/lib/List/Anime/DueIndexColumn.svelte24
-rw-r--r--src/lib/List/Anime/PlaceholderList.svelte12
-rw-r--r--src/lib/List/Anime/UpcomingAnimeList.svelte140
-rw-r--r--src/lib/List/CleanGrid.svelte34
-rw-r--r--src/lib/List/CleanList.svelte24
-rw-r--r--src/lib/List/ListTitle.svelte22
-rw-r--r--src/lib/List/Manga/CleanMangaList.svelte154
-rw-r--r--src/lib/List/Manga/MangaListTemplate.svelte449
-rw-r--r--src/lib/List/MediaRoulette.svelte142
-rw-r--r--src/lib/List/MediaTitleDisplay.svelte22
14 files changed, 901 insertions, 928 deletions
diff --git a/src/lib/List/Anime/AnimeListTemplate.svelte b/src/lib/List/Anime/AnimeListTemplate.svelte
index 185419de..cb1d0fcd 100644
--- a/src/lib/List/Anime/AnimeListTemplate.svelte
+++ b/src/lib/List/Anime/AnimeListTemplate.svelte
@@ -1,55 +1,55 @@
<script lang="ts">
- /* eslint svelte/no-at-html-tags: "off" */
+/* eslint svelte/no-at-html-tags: "off" */
- import type { AniListAuthorisation } from '$lib/Data/AniList/identity';
- import type { Media } from '$lib/Data/AniList/media';
- import Error 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 Error 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[];
- export let animeLists: Promise<Media[]>;
- export let user: AniListAuthorisation;
- export let title: Title;
- export let completed = false;
- export let plannedOnly = false;
- export let upcoming = false;
- export let notYetReleased = false;
- export let dummy = false;
- export let disableFilter = false;
- export let limit: number | undefined = undefined;
+export let endTime: number;
+export let cleanMedia: (
+ media: Media[],
+ displayUnresolved: boolean,
+ subsPlease: SubsPlease | null,
+ plannedOnly?: boolean
+) => Media[];
+export let animeLists: Promise<Media[]>;
+export let user: AniListAuthorisation;
+export let title: Title;
+export let completed = false;
+export let plannedOnly = false;
+export let upcoming = false;
+export let notYetReleased = false;
+export let dummy = false;
+export let disableFilter = false;
+export let limit: number | undefined = undefined;
- let lastUpdatedMedia = -1;
- let previousAnimeList: Media[];
- let pendingUpdate: number | null = null;
- let lastListSize = 8;
+let lastUpdatedMedia = -1;
+let previousAnimeList: Media[];
+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;
+onMount(async () => {
+ 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>
{#if !$subsPlease && !dummy}
diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte
index aee035a4..14406f04 100644
--- a/src/lib/List/Anime/CleanAnimeList.svelte
+++ b/src/lib/List/Anime/CleanAnimeList.svelte
@@ -1,177 +1,176 @@
<script lang="ts">
- 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';
-
- export let media: Media[];
- export let title: Title;
- export let animeLists: Promise<Media[]>;
- export let user: AniListAuthorisation;
- export let endTime: number;
- export let lastUpdatedMedia: number;
- export let completed = false;
- export let previousAnimeList: Media[];
- export let pendingUpdate: number | null;
- export let upcoming = false;
- export let notYetReleased = false;
- export let dummy = false;
- export let disableFilter = false;
- export let limit: number | undefined = undefined;
-
- 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);
- const lists = Array.from(
- 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';
- const filterKey = `${filterKind}AnimeListFilter`;
-
- $: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All';
-
- $: filteredMedia =
- selectedList === 'All' || !$settings.displayMediaListFilter
- ? media
- : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]);
-
- const clearAiringRefreshTimeout = () => {
- if (airingRefreshTimeout) clearTimeout(airingRefreshTimeout);
+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';
+
+export let media: Media[];
+export let title: Title;
+export let animeLists: Promise<Media[]>;
+export let user: AniListAuthorisation;
+export let endTime: number;
+export let lastUpdatedMedia: number;
+export let completed = false;
+export let previousAnimeList: Media[];
+export let pendingUpdate: number | null;
+export let upcoming = false;
+export let notYetReleased = false;
+export let dummy = false;
+export let disableFilter = false;
+export let limit: number | undefined = undefined;
+
+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);
+const lists = Array.from(
+ 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';
+const filterKey = `${filterKind}AnimeListFilter`;
+
+$: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All';
+
+$: filteredMedia =
+ selectedList === 'All' || !$settings.displayMediaListFilter
+ ? media
+ : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]);
+
+const clearAiringRefreshTimeout = () => {
+ if (airingRefreshTimeout) clearTimeout(airingRefreshTimeout);
+
+ airingRefreshTimeout = undefined;
+ scheduledAiringAt = null;
+};
+
+const scheduleAiringRefresh = () => {
+ if (!browser) return;
+
+ if (dummy || media.length === 0) {
+ clearAiringRefreshTimeout();
- airingRefreshTimeout = undefined;
- scheduledAiringAt = null;
- };
+ return;
+ }
- const scheduleAiringRefresh = () => {
- if (!browser) return;
+ const nextAiringAt = media.reduce<number | null>((closest, currentMedia) => {
+ if (currentMedia.status !== 'RELEASING' && currentMedia.status !== 'NOT_YET_RELEASED')
+ return closest;
- if (dummy || media.length === 0) {
- clearAiringRefreshTimeout();
+ const airingAt = currentMedia.nextAiringEpisode?.airingAt;
- return;
- }
+ if (!airingAt) return closest;
+ if (closest === null) return airingAt;
- const nextAiringAt = media.reduce<number | null>((closest, currentMedia) => {
- if (currentMedia.status !== 'RELEASING' && currentMedia.status !== 'NOT_YET_RELEASED')
- return closest;
+ return airingAt < closest ? airingAt : closest;
+ }, null);
- const airingAt = currentMedia.nextAiringEpisode?.airingAt;
+ if (!nextAiringAt) {
+ clearAiringRefreshTimeout();
- if (!airingAt) return closest;
- if (closest === null) return airingAt;
+ return;
+ }
- return airingAt < closest ? airingAt : closest;
- }, null);
+ if (airingRefreshTimeout && scheduledAiringAt === nextAiringAt) return;
- if (!nextAiringAt) {
- clearAiringRefreshTimeout();
+ clearAiringRefreshTimeout();
+ scheduledAiringAt = nextAiringAt;
+ airingRefreshTimeout = setTimeout(
+ () => {
+ const now = Date.now() / 1000;
- return;
- }
+ if (media.some((m) => m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < now))
+ animeLists = cleanCache(user, $identity);
- if (airingRefreshTimeout && scheduledAiringAt === nextAiringAt) return;
+ scheduleAiringRefresh();
+ },
+ Math.max(1000, nextAiringAt * 1000 - Date.now() + 250)
+ );
+};
- clearAiringRefreshTimeout();
- scheduledAiringAt = nextAiringAt;
- airingRefreshTimeout = setTimeout(
- () => {
- const now = Date.now() / 1000;
+onMount(async () => {
+ if (dummy) return;
- if (media.some((m) => m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < now))
- animeLists = cleanCache(user, $identity);
+ scheduleAiringRefresh();
- scheduleAiringRefresh();
- },
- Math.max(1000, nextAiringAt * 1000 - Date.now() + 250)
+ if (browser)
+ await localforage.setItem(
+ `last${
+ notYetReleased ? 'NotYetReleased' : upcoming ? 'Upcoming' : completed ? 'Completed' : ''
+ }AnimeListLength`,
+ media.length.toString()
);
- };
+});
- onMount(async () => {
- if (dummy) return;
+$: if (browser && !dummy) {
+ media;
- scheduleAiringRefresh();
+ scheduleAiringRefresh();
+}
- if (browser)
- await localforage.setItem(
- `last${
- notYetReleased ? 'NotYetReleased' : upcoming ? 'Upcoming' : completed ? 'Completed' : ''
- }AnimeListLength`,
- media.length.toString()
- );
- });
+onDestroy(() => clearAiringRefreshTimeout());
- $: if (browser && !dummy) {
- media;
-
- scheduleAiringRefresh();
- }
+const increment = (anime: Media, progress: number) => {
+ if (!dummy && pendingUpdate !== anime.id) {
+ $revalidateAnime = true;
+ lastUpdatedMedia = anime.id;
+ pendingUpdate = anime.id;
- onDestroy(() => clearAiringRefreshTimeout());
+ incrementMediaProgress(anime.id, anime.mediaListEntry?.progress, user, () => {
+ const mediaListEntry = media.find((m) => m.id === anime.id)?.mediaListEntry;
- const increment = (anime: Media, progress: number) => {
- if (!dummy && pendingUpdate !== anime.id) {
- $revalidateAnime = true;
- lastUpdatedMedia = anime.id;
- pendingUpdate = anime.id;
+ if (mediaListEntry) mediaListEntry.progress = progress + 1;
- 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;
- });
- }
- };
+ previousAnimeList = media;
+ animeLists = cleanCache(user, $identity);
+ pendingUpdate = null;
+ });
+ }
+};
</script>
<ListTitle
diff --git a/src/lib/List/Anime/CompletedAnimeList.svelte b/src/lib/List/Anime/CompletedAnimeList.svelte
index b9e86ba0..240d386d 100644
--- a/src/lib/List/Anime/CompletedAnimeList.svelte
+++ b/src/lib/List/Anime/CompletedAnimeList.svelte
@@ -1,112 +1,112 @@
<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: ''
- };
- export let dummy = false;
- export let dummyCount = 7;
- export let disableFilter = false;
- export let limit: number | undefined = undefined;
- let animeLists: Promise<Media[]>;
- let startTime: number;
- let endTime: number;
+export let user: AniListAuthorisation = {
+ accessToken: '',
+ refreshToken: '',
+ expiresIn: 0,
+ tokenType: ''
+};
+export let dummy = false;
+export let dummyCount = 7;
+export let disableFilter = false;
+export let limit: number | undefined = undefined;
+let animeLists: Promise<Media[]>;
+let startTime: number;
+let endTime: number;
- onMount(async () => {
- startTime = performance.now();
+onMount(async () => {
+ 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;
+const cleanMedia = (anime: Media[]) => {
+ 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);
+ }
+ });
- if (!endTime) endTime = performance.now() - startTime;
+ if (!endTime) endTime = performance.now() - startTime;
- return outdatedCompletedAnime;
- };
+ return outdatedCompletedAnime;
+};
</script>
<AnimeList
diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte
index da1f6c48..e19bcd0c 100644
--- a/src/lib/List/Anime/DueAnimeList.svelte
+++ b/src/lib/List/Anime/DueAnimeList.svelte
@@ -1,130 +1,119 @@
<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';
-
- export let user: AniListAuthorisation;
- let animeLists: Promise<Media[]>;
- let startTime: number;
- let endTime: number;
- 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
- );
- };
+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[]>;
+let startTime: number;
+let endTime: number;
+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
+ );
+};
- onMount(async () => {
- restartKeyCacher($settings.cacheMinutes);
+onMount(async () => {
+ restartKeyCacher($settings.cacheMinutes);
- startTime = performance.now();
- animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, {
- addNotification
- });
+ startTime = performance.now();
+ animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, {
+ addNotification
});
+});
+
+$: if (keyCacher && keyCacheMinutes !== $settings.cacheMinutes)
+ restartKeyCacher($settings.cacheMinutes);
+
+onDestroy(() => {
+ 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 (keyCacher && keyCacheMinutes !== $settings.cacheMinutes)
- restartKeyCacher($settings.cacheMinutes);
-
- onDestroy(() => {
- if (keyCacher) clearInterval(keyCacher);
- });
+ if (!displayUnresolved)
+ dueAnime = dueAnime.filter((media: Media) => media.nextAiringEpisode?.episode !== -1);
- 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;
- });
+ 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;
- 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);
+ return difference(a) - difference(b);
}
- });
- if (!endTime || endTime === -1) endTime = performance.now() - startTime;
+ 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);
+ }
+ });
+
+ if (!endTime || endTime === -1) endTime = performance.now() - startTime;
- return dueAnime;
- };
+ return dueAnime;
+};
</script>
<AnimeList {endTime} {cleanMedia} bind:animeLists {user} title={$locale().lists.due.episodes} />
diff --git a/src/lib/List/Anime/DueIndexColumn.svelte b/src/lib/List/Anime/DueIndexColumn.svelte
index 920035bc..6ffc17be 100644
--- a/src/lib/List/Anime/DueIndexColumn.svelte
+++ b/src/lib/List/Anime/DueIndexColumn.svelte
@@ -1,18 +1,18 @@
<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;
+export let userIdentity: { id: number };
+export let user: AniListAuthorisation;
- onMount(() => {
- $stateBin.dueAnimeListOpen ??= true;
- });
+onMount(() => {
+ $stateBin.dueAnimeListOpen ??= true;
+});
</script>
<details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
diff --git a/src/lib/List/Anime/PlaceholderList.svelte b/src/lib/List/Anime/PlaceholderList.svelte
index 1f701d79..4ac0d8ff 100644
--- a/src/lib/List/Anime/PlaceholderList.svelte
+++ b/src/lib/List/Anime/PlaceholderList.svelte
@@ -1,11 +1,11 @@
<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;
+export let title: Title;
+export let count = 8;
</script>
<ListTitle {title} />
diff --git a/src/lib/List/Anime/UpcomingAnimeList.svelte b/src/lib/List/Anime/UpcomingAnimeList.svelte
index a2cc963d..109584f0 100644
--- a/src/lib/List/Anime/UpcomingAnimeList.svelte
+++ b/src/lib/List/Anime/UpcomingAnimeList.svelte
@@ -1,88 +1,86 @@
<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[]>;
- let startTime: number;
- let endTime: number;
+export let user: AniListAuthorisation;
+let animeLists: Promise<Media[]>;
+let startTime: number;
+let endTime: number;
- onMount(async () => {
- startTime = performance.now();
- animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, {
- addNotification,
- notificationType: 'Upcoming Episodes'
- });
+onMount(async () => {
+ 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
- ) => {
- if (anime === undefined) return [];
+const cleanMedia = (
+ anime: Media[],
+ displayUnresolved: boolean,
+ subsPlease: SubsPlease | null,
+ plannedOnly = true
+) => {
+ 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)
+ 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)
)
- .map((media: Media) => {
- // Adjust for planned anime
- if (
- ($settings.displayPlannedAnime ? media.episodes !== 1 : true) &&
- hasNoAiredEpisodes(media)
- )
- media.nextAiringEpisode = { episode: -1 };
+ 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>
<AnimeList
diff --git a/src/lib/List/CleanGrid.svelte b/src/lib/List/CleanGrid.svelte
index 4f628c3c..08a7ef83 100644
--- a/src/lib/List/CleanGrid.svelte
+++ b/src/lib/List/CleanGrid.svelte
@@ -1,24 +1,24 @@
<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 upcoming = false;
- export let notYetReleased = false;
- export let reverseSort = false;
- export let limit: number | undefined = undefined;
+export let media: Media[];
+export let dummy = false;
+export let type: 'anime' | 'manga';
+export let upcoming = false;
+export let notYetReleased = false;
+export let reverseSort = false;
+export let limit: number | undefined = undefined;
- let uniqueID = new Date().getTime();
+let uniqueID = new Date().getTime();
- $: sortedMedia = reverseSort ? [...media].reverse() : media;
- $: processedMedia = limit !== undefined ? sortedMedia.slice(0, limit) : sortedMedia;
+$: sortedMedia = reverseSort ? [...media].reverse() : media;
+$: 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 bf8c44ff..377b9302 100644
--- a/src/lib/List/CleanList.svelte
+++ b/src/lib/List/CleanList.svelte
@@ -1,18 +1,18 @@
<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 upcoming = false;
- export let notYetReleased = false;
- export let lastUpdatedMedia: number;
- export let reverseSort = false;
+export let media: Media[];
+export let type: 'anime' | 'manga';
+export let upcoming = false;
+export let notYetReleased = false;
+export let lastUpdatedMedia: number;
+export let reverseSort = false;
- $: processedMedia = reverseSort ? [...media].reverse() : media;
+$: processedMedia = reverseSort ? [...media].reverse() : media;
</script>
<ul>
diff --git a/src/lib/List/ListTitle.svelte b/src/lib/List/ListTitle.svelte
index 21013b52..71b5e649 100644
--- a/src/lib/List/ListTitle.svelte
+++ b/src/lib/List/ListTitle.svelte
@@ -1,16 +1,16 @@
<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.'
- };
- export let progress: undefined | number = undefined;
- export let hideTime = false;
- export let hideCount = false;
+export let time: number | undefined = undefined;
+export let count = -1337;
+export let title: Title = {
+ title: 'Media List',
+ hint: 'This is a media list.'
+};
+export let progress: undefined | number = undefined;
+export let hideTime = false;
+export let hideCount = false;
</script>
<summary>
diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte
index bba0fb08..8964c15e 100644
--- a/src/lib/List/Manga/CleanMangaList.svelte
+++ b/src/lib/List/Manga/CleanMangaList.svelte
@@ -1,83 +1,79 @@
<script lang="ts">
- import Spacer from '$lib/Layout/Spacer.svelte';
- import type { Media } from '$lib/Data/AniList/media';
- import Error 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 pendingUpdate: number | null;
- export let due: boolean;
- export let rateLimited: boolean;
- export let authorised: boolean;
- export let dummy = false;
- export let disableFilter = false;
- 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;
-
- if (!due && !$settings.displayTotalEpisodes) return 1;
-
- 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)
- )
- );
- const filterKind = due ? 'due' : 'completed';
- const filterKey = `${filterKind}MangaListFilter`;
-
- $: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All';
-
- $: filteredMedia =
- selectedList === 'All' || !$settings.displayMediaListFilter
- ? media
- : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]);
-
- onMount(async () => {
- serviceStatusResponse = fetch(proxy('https://api.mangadex.org/ping'));
-
- 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);
- };
+import Spacer from '$lib/Layout/Spacer.svelte';
+import type { Media } from '$lib/Data/AniList/media';
+import Error 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 pendingUpdate: number | null;
+export let due: boolean;
+export let rateLimited: boolean;
+export let authorised: boolean;
+export let dummy = false;
+export let disableFilter = false;
+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;
+
+ if (!due && !$settings.displayTotalEpisodes) return 1;
+
+ 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)
+ )
+);
+const filterKind = due ? 'due' : 'completed';
+const filterKey = `${filterKind}MangaListFilter`;
+
+$: selectedList = disableFilter ? 'All' : ($stateBin[filterKey] as string) || 'All';
+
+$: filteredMedia =
+ selectedList === 'All' || !$settings.displayMediaListFilter
+ ? media
+ : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]);
+
+onMount(async () => {
+ serviceStatusResponse = fetch(proxy('https://api.mangadex.org/ping'));
+
+ 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);
+};
</script>
{#if authorised}
diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte
index f549496d..740a1341 100644
--- a/src/lib/List/Manga/MangaListTemplate.svelte
+++ b/src/lib/List/Manga/MangaListTemplate.svelte
@@ -1,271 +1,262 @@
<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 Error 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: ''
- };
- export let displayUnresolved: boolean;
- export let due: boolean;
- export let dummy = $settings.debugDummyLists || false;
- export let dummyCount = 7;
- export let disableFilter = false;
- export let limit: number | undefined = undefined;
- const authorised = privilegedUser($identity.id);
- let mangaLists: Promise<Media[]>;
- let startTime: number;
- let endTime: number;
- let lastUpdatedMedia = -1;
- let previousMangaList: Media[];
- let pendingUpdate: number | null = null;
- let progress = 0;
- let rateLimited = false;
- let forceFlag = false;
- let lastListSize = 5;
- 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
- );
- };
-
- 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 {
+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 Error 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: ''
+};
+export let displayUnresolved: boolean;
+export let due: boolean;
+export let dummy = $settings.debugDummyLists || false;
+export let dummyCount = 7;
+export let disableFilter = false;
+export let limit: number | undefined = undefined;
+const authorised = privilegedUser($identity.id);
+let mangaLists: Promise<Media[]>;
+let startTime: number;
+let endTime: number;
+let lastUpdatedMedia = -1;
+let previousMangaList: Media[];
+let pendingUpdate: number | null = null;
+let progress = 0;
+let rateLimited = false;
+let forceFlag = false;
+let lastListSize = 5;
+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
+ );
+};
+
+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
+ });
+ }
+});
- $: 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);
- });
+onDestroy(() => {
+ if (keyCacher) clearInterval(keyCacher);
+});
- const cleanMedia = async (manga: Media[], displayUnresolved: boolean, force: boolean) => {
- progress = 0;
+const cleanMedia = async (manga: Media[], displayUnresolved: boolean, force: boolean) => {
+ progress = 0;
- if (manga && dummy) return manga;
+ if (manga && dummy) return manga;
- if (manga === undefined) return [];
+ if (manga === undefined) return [];
- if (!authorised && (await database.chapters.toArray()).length <= 0 && !force) return [];
+ if (!authorised && (await database.chapters.toArray()).length <= 0 && !force) return [];
- if (authorised) {
- let refreshing = false;
+ if (authorised) {
+ let refreshing = false;
- if ($lastPruneTimes.chapters === 1) {
- refreshing = true;
+ 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)
- );
- })();
- }
- }
+ lastPruneTimes.setKey('chapters', new Date().getTime());
+ } else {
+ const currentDate = new Date();
- if (refreshing) {
- addNotification(
- options({
- heading: 'Manga',
- description: 'Re-freshing manga data ...'
- })
- );
+ 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));
+ })();
}
}
- 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 (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 (count === -22) {
- rateLimited = true;
+ if (!due) return new Promise((resolve) => resolve(m.chapters)) as Promise<number | null>;
- break;
+ 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)[] = [];
- chapterCounts.push(count);
+ for (let i = 0; i < chapterPromises.length; i++) {
+ const count = await chapterPromises[i];
- if (progress < 100) progress += progressStep;
+ if (count === -22) {
+ rateLimited = true;
+
+ break;
}
- finalMedia.forEach((m: Media, i) => (m.episodes = chapterCounts[i] || -1337));
+ chapterCounts.push(count);
- if (!displayUnresolved) finalMedia = finalMedia.filter((m: Media) => m.episodes !== -1337);
+ if (progress < 100) progress += progressStep;
+ }
- finalMedia.sort(
- (a: Media, b: Media) =>
- (a.episodes || 9999) -
- (a.mediaListEntry || { progress: 0 }).progress -
- ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress)
- );
+ finalMedia.forEach((m: Media, i) => (m.episodes = chapterCounts[i] || -1337));
- 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 (!displayUnresolved) finalMedia = finalMedia.filter((m: Media) => m.episodes !== -1337);
- if (!endTime || endTime === -1) endTime = performance.now() - startTime;
+ finalMedia.sort(
+ (a: Media, b: Media) =>
+ (a.episodes || 9999) -
+ (a.mediaListEntry || { progress: 0 }).progress -
+ ((b.episodes || 9999) - (b.mediaListEntry || { progress: 0 }).progress)
+ );
- return finalMedia;
- };
+ 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))
+ );
- const updateMedia = async (id: number, progress: number | undefined, media: Media[]) => {
- pendingUpdate = id;
- lastUpdatedMedia = id;
+ if (!endTime || endTime === -1) endTime = performance.now() - startTime;
- await database.chapters.delete(id);
+ return finalMedia;
+};
- incrementMediaProgress(id, progress, user, () => {
- previousMangaList = media;
+const updateMedia = async (id: number, progress: number | undefined, media: Media[]) => {
+ pendingUpdate = id;
+ lastUpdatedMedia = id;
- const foundEntry = media.find((m) => m.id === id);
+ await database.chapters.delete(id);
- if (foundEntry && foundEntry.mediaListEntry)
- foundEntry.mediaListEntry.progress = (progress || 0) + 1;
+ incrementMediaProgress(id, progress, user, () => {
+ previousMangaList = media;
- mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, {
- forcePrune: true
- });
- pendingUpdate = null;
+ 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;
+const cleanCache = () => {
+ startTime = performance.now();
+ endTime = -1;
- pruneAllManga().then(() => {
- mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, {
- forcePrune: true
- });
+ pruneAllManga().then(() => {
+ mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, {
+ forcePrune: true
});
- };
+ });
+};
</script>
{#await mangaLists}
diff --git a/src/lib/List/MediaRoulette.svelte b/src/lib/List/MediaRoulette.svelte
index 4b498d4f..b4a6d527 100644
--- a/src/lib/List/MediaRoulette.svelte
+++ b/src/lib/List/MediaRoulette.svelte
@@ -1,85 +1,85 @@
<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';
-
- interface Props {
- media: Media[];
- type: 'anime' | 'manga';
- onClose: () => void;
- spinDuration?: number;
- }
-
- let { media, type, onClose, spinDuration = 2 }: Props = $props();
- let isSpinning = $state(false);
- let selectedIndex = $state(0);
- let displayIndex = $state(0);
- let spinTimeout: ReturnType<typeof setTimeout> | null = $state(null);
- let showResult = $state(false);
- let isClosing = $state(false);
- let currentMedia = $derived(media[displayIndex]);
-
- const startRoulette = () => {
- if (media.length === 0 || isSpinning) return;
-
- 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 spin = () => {
- displayIndex = (displayIndex + 1) % media.length;
-
- const elapsed = Date.now() - startTime;
- const progress = Math.min(elapsed / durationMs, 1);
- let speed = minSpeed;
-
- if (progress > slowdownStart) {
- const slowdownProgress = (progress - slowdownStart) / (1 - slowdownStart);
-
- speed = minSpeed + slowdownProgress * (maxSpeed - minSpeed);
- }
-
- if (progress >= 1 && displayIndex === selectedIndex) {
- spinTimeout = null;
- isSpinning = false;
- showResult = true;
+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;
+}
+
+let { media, type, onClose, spinDuration = 2 }: Props = $props();
+let isSpinning = $state(false);
+let selectedIndex = $state(0);
+let displayIndex = $state(0);
+let spinTimeout: ReturnType<typeof setTimeout> | null = $state(null);
+let showResult = $state(false);
+let isClosing = $state(false);
+let currentMedia = $derived(media[displayIndex]);
+
+const startRoulette = () => {
+ if (media.length === 0 || isSpinning) return;
+
+ 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 spin = () => {
+ displayIndex = (displayIndex + 1) % media.length;
+
+ const elapsed = Date.now() - startTime;
+ const progress = Math.min(elapsed / durationMs, 1);
+ let speed = minSpeed;
+
+ if (progress > slowdownStart) {
+ const slowdownProgress = (progress - slowdownStart) / (1 - slowdownStart);
+
+ speed = minSpeed + slowdownProgress * (maxSpeed - minSpeed);
+ }
- return;
- }
+ if (progress >= 1 && displayIndex === selectedIndex) {
+ spinTimeout = null;
+ isSpinning = false;
+ showResult = true;
- spinTimeout = setTimeout(spin, speed);
- };
+ return;
+ }
- spinTimeout = setTimeout(spin, minSpeed);
+ spinTimeout = setTimeout(spin, speed);
};
- const handleClose = () => {
- if (isClosing) return;
+ spinTimeout = setTimeout(spin, minSpeed);
+};
- if (spinTimeout) {
- clearTimeout(spinTimeout);
+const handleClose = () => {
+ if (isClosing) return;
- spinTimeout = null;
- }
+ if (spinTimeout) {
+ clearTimeout(spinTimeout);
- isSpinning = false;
- showResult = false;
- isClosing = true;
+ spinTimeout = null;
+ }
- setTimeout(() => onClose(), 200);
- };
+ isSpinning = false;
+ showResult = false;
+ isClosing = true;
- const handleOverlayClick = (e: MouseEvent) => {
- if (e.target === e.currentTarget) handleClose();
- };
+ setTimeout(() => onClose(), 200);
+};
+
+const handleOverlayClick = (e: MouseEvent) => {
+ if (e.target === e.currentTarget) handleClose();
+};
</script>
<svelte:window onkeydown={(e) => e.key === 'Escape' && handleClose()} />
diff --git a/src/lib/List/MediaTitleDisplay.svelte b/src/lib/List/MediaTitleDisplay.svelte
index 6a886704..4d379ee9 100644
--- a/src/lib/List/MediaTitleDisplay.svelte
+++ b/src/lib/List/MediaTitleDisplay.svelte
@@ -1,17 +1,17 @@
<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;
- export let abbreviateTo = 20;
- export let tooltip = false;
+export let title: MediaTitle;
+export let abbreviate = false;
+export let abbreviateTo = 20;
+export let tooltip = false;
- const compressToBase64 = (string: string) => LZString.compressToBase64(string);
+const compressToBase64 = (string: string) => LZString.compressToBase64(string);
</script>
<span id={`title-display-${compressToBase64(title.native)}`}>