diff options
Diffstat (limited to 'src/lib/List')
| -rw-r--r-- | src/lib/List/Anime/AnimeListTemplate.svelte | 92 | ||||
| -rw-r--r-- | src/lib/List/Anime/CleanAnimeList.svelte | 301 | ||||
| -rw-r--r-- | src/lib/List/Anime/CompletedAnimeList.svelte | 190 | ||||
| -rw-r--r-- | src/lib/List/Anime/DueAnimeList.svelte | 223 | ||||
| -rw-r--r-- | src/lib/List/Anime/DueIndexColumn.svelte | 24 | ||||
| -rw-r--r-- | src/lib/List/Anime/PlaceholderList.svelte | 12 | ||||
| -rw-r--r-- | src/lib/List/Anime/UpcomingAnimeList.svelte | 140 | ||||
| -rw-r--r-- | src/lib/List/CleanGrid.svelte | 34 | ||||
| -rw-r--r-- | src/lib/List/CleanList.svelte | 24 | ||||
| -rw-r--r-- | src/lib/List/ListTitle.svelte | 22 | ||||
| -rw-r--r-- | src/lib/List/Manga/CleanMangaList.svelte | 154 | ||||
| -rw-r--r-- | src/lib/List/Manga/MangaListTemplate.svelte | 449 | ||||
| -rw-r--r-- | src/lib/List/MediaRoulette.svelte | 142 | ||||
| -rw-r--r-- | src/lib/List/MediaTitleDisplay.svelte | 22 |
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)}`}> |