diff options
| author | Fuwn <[email protected]> | 2026-03-01 16:04:11 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-01 16:04:11 -0800 |
| commit | 48f0c30d47d62e4f35706edb93a1bb2f97eba14c (patch) | |
| tree | 44866d7a61adfdf01a780e0108c370294d3db78b /src/lib | |
| parent | chore(biome): re-enable useAltText rule (diff) | |
| download | due.moe-48f0c30d47d62e4f35706edb93a1bb2f97eba14c.tar.xz due.moe-48f0c30d47d62e4f35706edb93a1bb2f97eba14c.zip | |
chore(biome): enable svelte formatting
Diffstat (limited to 'src/lib')
101 files changed, 3658 insertions, 3721 deletions
diff --git a/src/lib/Announcement.svelte b/src/lib/Announcement.svelte index 4fbe9de0..1e6f428f 100644 --- a/src/lib/Announcement.svelte +++ b/src/lib/Announcement.svelte @@ -1,38 +1,38 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import Popup from './Layout/Popup.svelte'; - import announcementHash from '$stores/announcementHash'; - import { env } from '$env/dynamic/public'; - import identity from '$stores/identity'; - - const announcement = env.PUBLIC_ANNOUNCEMENT; - const dismissButton = env.PUBLIC_ANNOUNCEMENT_DISMISS; - const loggedIn = $identity !== undefined && $identity.id !== -2; - - const hash = (s: string) => - s - .split('') - .reduce((previous, current) => ((previous << 5) - previous + current.charCodeAt(0)) | 0, 0); - - const dismiss = () => { - if (announcement) announcementHash.set(hash(announcement)); - }; - - const maxWidth = (input: string, max = 100) => { - let output = ''; - let line = ''; - - for (const word of input.split(' ')) { - if (line.length + word.length > max) { - output += line + '\n'; - line = ''; - } - - line += word + ' '; +import Spacer from '$lib/Layout/Spacer.svelte'; +import Popup from './Layout/Popup.svelte'; +import announcementHash from '$stores/announcementHash'; +import { env } from '$env/dynamic/public'; +import identity from '$stores/identity'; + +const announcement = env.PUBLIC_ANNOUNCEMENT; +const dismissButton = env.PUBLIC_ANNOUNCEMENT_DISMISS; +const loggedIn = $identity !== undefined && $identity.id !== -2; + +const hash = (s: string) => + s + .split('') + .reduce((previous, current) => ((previous << 5) - previous + current.charCodeAt(0)) | 0, 0); + +const dismiss = () => { + if (announcement) announcementHash.set(hash(announcement)); +}; + +const maxWidth = (input: string, max = 100) => { + let output = ''; + let line = ''; + + for (const word of input.split(' ')) { + if (line.length + word.length > max) { + output += line + '\n'; + line = ''; } - return output + line; - }; + line += word + ' '; + } + + return output + line; +}; </script> {#if loggedIn && announcement && $announcementHash !== hash(announcement) && $announcementHash !== 0} diff --git a/src/lib/CommandPalette/CommandPalette.svelte b/src/lib/CommandPalette/CommandPalette.svelte index 735941fb..a42fab1a 100644 --- a/src/lib/CommandPalette/CommandPalette.svelte +++ b/src/lib/CommandPalette/CommandPalette.svelte @@ -1,137 +1,137 @@ <script lang="ts"> - import { onMount } from 'svelte'; - import { fly, fade } from 'svelte/transition'; - import { flip } from 'svelte/animate'; - import type { CommandPaletteAction } from './actions'; - - export let items: CommandPaletteAction[] = []; - export let open = false; - - let search = ''; - let filtered: (CommandPaletteAction & { id?: string })[] = []; - let selectedIndex = -1; - let inputRef: HTMLInputElement; - let isVisible = false; - let timeoutID: ReturnType<typeof setTimeout> | null = null; - let itemIDs = new Map<string, number>(); - - $: { - items.forEach((item, index) => { - if (!itemIDs.has(item.url)) itemIDs.set(item.url, index); - }); - - const doesActionMatch = (action: CommandPaletteAction) => { - const doesActionIncludePattern = (query: string, action: string) => { - const normalise = (input: string) => input.toLowerCase().replace(/\s+/g, ''); - - return normalise(query).includes(normalise(action)); - }; - - return ( - doesActionIncludePattern(action.name, search) || - action.tags?.some((tag) => doesActionIncludePattern(tag, search)) - ); +import { onMount } from 'svelte'; +import { fly, fade } from 'svelte/transition'; +import { flip } from 'svelte/animate'; +import type { CommandPaletteAction } from './actions'; + +export let items: CommandPaletteAction[] = []; +export let open = false; + +let search = ''; +let filtered: (CommandPaletteAction & { id?: string })[] = []; +let selectedIndex = -1; +let inputRef: HTMLInputElement; +let isVisible = false; +let timeoutID: ReturnType<typeof setTimeout> | null = null; +let itemIDs = new Map<string, number>(); + +$: { + items.forEach((item, index) => { + if (!itemIDs.has(item.url)) itemIDs.set(item.url, index); + }); + + const doesActionMatch = (action: CommandPaletteAction) => { + const doesActionIncludePattern = (query: string, action: string) => { + const normalise = (input: string) => input.toLowerCase().replace(/\s+/g, ''); + + return normalise(query).includes(normalise(action)); }; - filtered = []; + return ( + doesActionIncludePattern(action.name, search) || + action.tags?.some((tag) => doesActionIncludePattern(tag, search)) + ); + }; - items.forEach((action, idx) => { - const actionMatches = doesActionMatch(action); - let matchedParent = false; + filtered = []; - if (actionMatches) { - filtered.push({ ...action, id: `action-${idx}` }); + items.forEach((action, idx) => { + const actionMatches = doesActionMatch(action); + let matchedParent = false; - matchedParent = true; - } + if (actionMatches) { + filtered.push({ ...action, id: `action-${idx}` }); - if (action.actions) - action.actions.forEach((nestedAction, nestedIdx) => { - if (doesActionMatch(nestedAction)) - filtered.push({ - ...nestedAction, - id: `action-${idx}-nested-${nestedIdx}`, - name: `${matchedParent ? '↳' : `${action.name} >`} ${nestedAction.name}` - }); - }); - }); + matchedParent = true; + } - filtered = filtered.slice(0, 10); - } + if (action.actions) + action.actions.forEach((nestedAction, nestedIdx) => { + if (doesActionMatch(nestedAction)) + filtered.push({ + ...nestedAction, + id: `action-${idx}-nested-${nestedIdx}`, + name: `${matchedParent ? '↳' : `${action.name} >`} ${nestedAction.name}` + }); + }); + }); - $: if (selectedIndex >= filtered.length) selectedIndex = filtered.length - 1; - $: if (selectedIndex < 0 && filtered.length > 0) selectedIndex = 0; + filtered = filtered.slice(0, 10); +} - $: if (open && !isVisible) { - isVisible = true; +$: if (selectedIndex >= filtered.length) selectedIndex = filtered.length - 1; +$: if (selectedIndex < 0 && filtered.length > 0) selectedIndex = 0; - if (timeoutID !== null) { - clearTimeout(timeoutID); +$: if (open && !isVisible) { + isVisible = true; + if (timeoutID !== null) { + clearTimeout(timeoutID); + + timeoutID = null; + } +} else if (!open && isVisible) { + if (timeoutID === null) { + timeoutID = setTimeout(() => { + isVisible = false; timeoutID = null; - } - } else if (!open && isVisible) { - if (timeoutID === null) { - timeoutID = setTimeout(() => { - isVisible = false; - timeoutID = null; - }, 200); - } + }, 200); } +} - const executeItem = (item: CommandPaletteAction) => { - if (item.onClick) item.onClick(); - if (!item.preventDefault) window.location.href = item.url; +const executeItem = (item: CommandPaletteAction) => { + if (item.onClick) item.onClick(); + if (!item.preventDefault) window.location.href = item.url; - open = false; - }; + open = false; +}; - const handleKey = (e: KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - - selectedIndex = (selectedIndex + 1) % filtered.length; - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - - selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length; - } else if (e.key === 'Enter') { - if (filtered.length === 1) { - executeItem(filtered[0]); - } else if (filtered.length > 1 && selectedIndex >= 0) { - executeItem(filtered[selectedIndex]); - } - } else if (e.key === 'Escape') { - open = false; - } - }; +const handleKey = (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); - onMount(() => { - window.addEventListener('keydown', handleGlobalKey); + selectedIndex = (selectedIndex + 1) % filtered.length; + } else if (e.key === 'ArrowUp') { + e.preventDefault(); - return () => { - window.removeEventListener('keydown', handleGlobalKey); + selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length; + } else if (e.key === 'Enter') { + if (filtered.length === 1) { + executeItem(filtered[0]); + } else if (filtered.length > 1 && selectedIndex >= 0) { + executeItem(filtered[selectedIndex]); + } + } else if (e.key === 'Escape') { + open = false; + } +}; - if (timeoutID !== null) clearTimeout(timeoutID); - }; - }); +onMount(() => { + window.addEventListener('keydown', handleGlobalKey); - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; + return () => { + window.removeEventListener('keydown', handleGlobalKey); - if (target.classList.contains('command-palette-overlay') || !target.closest('.dropdown')) - open = false; + if (timeoutID !== null) clearTimeout(timeoutID); }; +}); - const handleGlobalKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); +const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; - open = !open; + if (target.classList.contains('command-palette-overlay') || !target.closest('.dropdown')) + open = false; +}; - if (open) requestAnimationFrame(() => inputRef?.focus()); - } - }; +const handleGlobalKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + + open = !open; + + if (open) requestAnimationFrame(() => inputRef?.focus()); + } +}; </script> <svelte:window onclick={handleClickOutside} /> diff --git a/src/lib/Error/AnimeRateLimited.svelte b/src/lib/Error/AnimeRateLimited.svelte index 5813ce51..752b8aed 100644 --- a/src/lib/Error/AnimeRateLimited.svelte +++ b/src/lib/Error/AnimeRateLimited.svelte @@ -1,6 +1,6 @@ <script> - import Spacer from '$lib/Layout/Spacer.svelte'; - import Popup from '$lib/Layout/Popup.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import Popup from '$lib/Layout/Popup.svelte'; </script> <Popup locked fullscreen> diff --git a/src/lib/Error/DotDotDot.svelte b/src/lib/Error/DotDotDot.svelte index 88b46374..cb8f95f2 100644 --- a/src/lib/Error/DotDotDot.svelte +++ b/src/lib/Error/DotDotDot.svelte @@ -1,22 +1,22 @@ <script lang="ts"> - import { onDestroy, onMount } from 'svelte'; +import { onDestroy, onMount } from 'svelte'; - export let max: number | undefined = undefined; - export let perMs = 1000; - export let start = ''; +export let max: number | undefined = undefined; +export let perMs = 1000; +export let start = ''; - let dots = start; - let interval: ReturnType<typeof setInterval>; +let dots = start; +let interval: ReturnType<typeof setInterval>; - onMount(() => { - interval = setInterval(() => { - dots += '.'; +onMount(() => { + interval = setInterval(() => { + dots += '.'; - if (max && dots.length > max) dots = ''; - }, perMs); - }); + if (max && dots.length > max) dots = ''; + }, perMs); +}); - onDestroy(() => clearInterval(interval)); +onDestroy(() => clearInterval(interval)); </script> {dots} diff --git a/src/lib/Error/LogInRestricted.svelte b/src/lib/Error/LogInRestricted.svelte index ab48f180..17c9d62b 100644 --- a/src/lib/Error/LogInRestricted.svelte +++ b/src/lib/Error/LogInRestricted.svelte @@ -1,7 +1,7 @@ <script> - import Popup from '$lib/Layout/Popup.svelte'; - import { env } from '$env/dynamic/public'; - import localforage from 'localforage'; +import Popup from '$lib/Layout/Popup.svelte'; +import { env } from '$env/dynamic/public'; +import localforage from 'localforage'; </script> <Popup fullscreen locked> diff --git a/src/lib/Error/RateLimited.svelte b/src/lib/Error/RateLimited.svelte index 5a729c87..9c62759d 100644 --- a/src/lib/Error/RateLimited.svelte +++ b/src/lib/Error/RateLimited.svelte @@ -1,11 +1,11 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - export let type = 'Media'; - export let loginSessionError = true; - export let contact = true; - export let list = true; - export let card = false; - export let might = true; +import Spacer from '$lib/Layout/Spacer.svelte'; +export let type = 'Media'; +export let loginSessionError = true; +export let contact = true; +export let list = true; +export let card = false; +export let might = true; </script> <div class:card> diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte index 494eb6be..976ebdfa 100644 --- a/src/lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte +++ b/src/lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte @@ -1,16 +1,16 @@ <script lang="ts"> - export let prompt: string; - export let images: string[] = []; - export let correctIndex: number; - export let onComplete: () => void; +export let prompt: string; +export let images: string[] = []; +export let correctIndex: number; +export let onComplete: () => void; - let selectedIndex = -1; +let selectedIndex = -1; - const handleClick = (index: number) => { - selectedIndex = index; +const handleClick = (index: number) => { + selectedIndex = index; - if (index === correctIndex) setTimeout(onComplete, 500); - }; + if (index === correctIndex) setTimeout(onComplete, 500); +}; </script> <div class="container"> diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte index 0d7ab010..1c7d4545 100644 --- a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte +++ b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte @@ -1,100 +1,100 @@ <script lang="ts"> - import { onMount, tick } from 'svelte'; - import { browser } from '$app/environment'; - import Popup from '$lib/Layout/Popup.svelte'; +import { onMount, tick } from 'svelte'; +import { browser } from '$app/environment'; +import Popup from '$lib/Layout/Popup.svelte'; - export let targetID = 'easter-target'; - export let id: number; +export let targetID = 'easter-target'; +export let id: number; - let visible = false; - let showPopup = false; +let visible = false; +let showPopup = false; - $: eggCount = browser - ? JSON.parse(localStorage.getItem('easter2025ClickedEggs') || '[]').length - : 0; +$: eggCount = browser + ? JSON.parse(localStorage.getItem('easter2025ClickedEggs') || '[]').length + : 0; - onMount(() => { - let intervalId: number | undefined; +onMount(() => { + let intervalId: number | undefined; - const updatePosition = async () => { - await tick(); + const updatePosition = async () => { + await tick(); - const targetElement = document.getElementById(targetID); + const targetElement = document.getElementById(targetID); - if (!targetElement) return; + if (!targetElement) return; - const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; - const eggVisual = document.getElementById(`egg-visual-${targetID}-${id}`); - const eggClick = document.getElementById(`egg-click-${targetID}-${id}`); - const pageWidth = document.documentElement.clientWidth; - - visible = !clickedEggs.includes(id) && clickedEggs.length < 4; - - if (eggVisual && eggClick) { - const verticalPosition = targetElement.offsetHeight * 0.9; - const eggWidthPercent = (100 / pageWidth) * 100; - const horizontalPosition = targetElement.offsetWidth - eggWidthPercent / 2; - - eggVisual.style.top = `${verticalPosition}px`; - eggVisual.style.left = `${horizontalPosition}px`; - eggVisual.style.zIndex = '-1'; - eggClick.style.top = `${verticalPosition}px`; - eggClick.style.left = `${horizontalPosition}px`; - eggClick.style.zIndex = '9999'; - } - }; + const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); + const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; + const eggVisual = document.getElementById(`egg-visual-${targetID}-${id}`); + const eggClick = document.getElementById(`egg-click-${targetID}-${id}`); + const pageWidth = document.documentElement.clientWidth; + + visible = !clickedEggs.includes(id) && clickedEggs.length < 4; + + if (eggVisual && eggClick) { + const verticalPosition = targetElement.offsetHeight * 0.9; + const eggWidthPercent = (100 / pageWidth) * 100; + const horizontalPosition = targetElement.offsetWidth - eggWidthPercent / 2; + + eggVisual.style.top = `${verticalPosition}px`; + eggVisual.style.left = `${horizontalPosition}px`; + eggVisual.style.zIndex = '-1'; + eggClick.style.top = `${verticalPosition}px`; + eggClick.style.left = `${horizontalPosition}px`; + eggClick.style.zIndex = '9999'; + } + }; - intervalId = setInterval(updatePosition, 100) as unknown as number; + intervalId = setInterval(updatePosition, 100) as unknown as number; - updatePosition(); + updatePosition(); - return () => { - if (intervalId) clearInterval(intervalId); + return () => { + if (intervalId) clearInterval(intervalId); - window.removeEventListener('resize', updatePosition); - window.removeEventListener('scroll', updatePosition); - }; - }); + window.removeEventListener('resize', updatePosition); + window.removeEventListener('scroll', updatePosition); + }; +}); - const handleClick = (event: MouseEvent) => { - if (event.button === 0) { - const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; +const handleClick = (event: MouseEvent) => { + if (event.button === 0) { + const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); + const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; - if (!clickedEggs.includes(id)) { - clickedEggs.push(id); - localStorage.setItem('easter2025ClickedEggs', JSON.stringify(clickedEggs)); - } + if (!clickedEggs.includes(id)) { + clickedEggs.push(id); + localStorage.setItem('easter2025ClickedEggs', JSON.stringify(clickedEggs)); + } - visible = false; + visible = false; - if (clickedEggs.length >= 3) showPopup = true; + if (clickedEggs.length >= 3) showPopup = true; - // eslint-disable-next-line no-undef - umami.track('Easter Egg Clicked', { id }); - } else if (event.button === 1) { - visible = true; + // eslint-disable-next-line no-undef + umami.track('Easter Egg Clicked', { id }); + } else if (event.button === 1) { + visible = true; - localStorage.setItem('easter2025ClickedEggs', '[]'); - } - }; + localStorage.setItem('easter2025ClickedEggs', '[]'); + } +}; - const copyCode = (source: string) => { - navigator.clipboard.writeText( - `<img src="${source}" alt="due.moe × AniList Badges Badge Prize" width="200px">` - ); - }; +const copyCode = (source: string) => { + navigator.clipboard.writeText( + `<img src="${source}" alt="due.moe × AniList Badges Badge Prize" width="200px">` + ); +}; - const onLeavePopup = () => { - showPopup = false; +const onLeavePopup = () => { + showPopup = false; - const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); - const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; + const storedClickedEggs = localStorage.getItem('easter2025ClickedEggs'); + const clickedEggs = storedClickedEggs ? JSON.parse(storedClickedEggs) : []; - clickedEggs.push(-1); - localStorage.setItem('easter2025ClickedEggs', JSON.stringify(clickedEggs)); - }; + clickedEggs.push(-1); + localStorage.setItem('easter2025ClickedEggs', JSON.stringify(clickedEggs)); +}; </script> {#if visible} diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte index 6ba477be..ebcc9678 100644 --- a/src/lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte +++ b/src/lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte @@ -1,20 +1,20 @@ <script lang="ts"> - export let prompt: string; - export let answers: string[] = []; - export let correctIndex: number; - export let onComplete: () => void; +export let prompt: string; +export let answers: string[] = []; +export let correctIndex: number; +export let onComplete: () => void; - let selected = -1; +let selected = -1; - const handleChoice = (index: number) => { - if (index === correctIndex) { - selected = index; +const handleChoice = (index: number) => { + if (index === correctIndex) { + selected = index; - setTimeout(onComplete, 500); - } else { - selected = index; - } - }; + setTimeout(onComplete, 500); + } else { + selected = index; + } +}; </script> <div class="container"> diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte index 664d5e27..da01e5db 100644 --- a/src/lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte +++ b/src/lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte @@ -1,18 +1,18 @@ <script lang="ts"> - export let riddle: string; - export let answer: string; - export let onComplete: () => void; - export let hint: string | undefined = undefined; +export let riddle: string; +export let answer: string; +export let onComplete: () => void; +export let hint: string | undefined = undefined; - let userInput = ''; +let userInput = ''; - const checkAnswer = () => { - if (userInput.toLowerCase() === answer.toLowerCase()) { - setTimeout(onComplete, 500); - } else { - setTimeout(() => (userInput = ''), 500); - } - }; +const checkAnswer = () => { + if (userInput.toLowerCase() === answer.toLowerCase()) { + setTimeout(onComplete, 500); + } else { + setTimeout(() => (userInput = ''), 500); + } +}; </script> <div class="container"> diff --git a/src/lib/Events/Event.svelte b/src/lib/Events/Event.svelte index e6e16586..e8a8d606 100644 --- a/src/lib/Events/Event.svelte +++ b/src/lib/Events/Event.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import type { Event } from '$lib/Database/SB/events'; - import root from '$lib/Utility/root'; - import locale from '$stores/locale'; +import type { Event } from '$lib/Database/SB/events'; +import root from '$lib/Utility/root'; +import locale from '$stores/locale'; - let { event, avatar = false }: { event: Event; avatar?: boolean } = $props(); +let { event, avatar = false }: { event: Event; avatar?: boolean } = $props(); </script> <div diff --git a/src/lib/Events/Group.svelte b/src/lib/Events/Group.svelte index 43eb4f26..a891060f 100644 --- a/src/lib/Events/Group.svelte +++ b/src/lib/Events/Group.svelte @@ -1,8 +1,8 @@ <script lang="ts"> - import type { Group } from '$lib/Database/SB/groups'; - import tooltip from '$lib/Tooltip/tooltip'; +import type { Group } from '$lib/Database/SB/groups'; +import tooltip from '$lib/Tooltip/tooltip'; - let { group }: { group: Group } = $props(); +let { group }: { group: Group } = $props(); </script> <div diff --git a/src/lib/Hololive/Lives.svelte b/src/lib/Hololive/Lives.svelte index 877dc316..9fb5585f 100644 --- a/src/lib/Hololive/Lives.svelte +++ b/src/lib/Hololive/Lives.svelte @@ -1,62 +1,62 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import root from '$lib/Utility/root'; - import type { Live, ParseResult } from './hololive'; - import Stream from './Stream.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import Message from '$lib/Loading/Message.svelte'; +import root from '$lib/Utility/root'; +import type { Live, ParseResult } from './hololive'; +import Stream from './Stream.svelte'; - export let schedule: ParseResult; - export let pinnedStreams: string[]; - export let getPinnedStreams: () => void; - export let filter: string | undefined; +export let schedule: ParseResult; +export let pinnedStreams: string[]; +export let getPinnedStreams: () => void; +export let filter: string | undefined; - const pinStream = (streamer: string) => - fetch(root(`/api/preferences/pin?stream=${encodeURIComponent(streamer)}`), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - } - }).then(getPinnedStreams); +const pinStream = (streamer: string) => + fetch(root(`/api/preferences/pin?stream=${encodeURIComponent(streamer)}`), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + } + }).then(getPinnedStreams); - $: categorisedStreams = schedule.lives - .filter((live) => (filter ? live.streamer === filter : true)) - .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) - .sort((a, b) => { - const aPinned = pinnedStreams.includes(a.streamer); - const bPinned = pinnedStreams.includes(b.streamer); +$: categorisedStreams = schedule.lives + .filter((live) => (filter ? live.streamer === filter : true)) + .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()) + .sort((a, b) => { + const aPinned = pinnedStreams.includes(a.streamer); + const bPinned = pinnedStreams.includes(b.streamer); - if (aPinned && !bPinned) return -1; - if (!aPinned && bPinned) return 1; + if (aPinned && !bPinned) return -1; + if (!aPinned && bPinned) return 1; - return 0; - }) - .reduce( - ( - acc: { - live: Live[]; - upcoming: Live[]; - ended: Live[]; - }, - live - ) => { - const now = Date.now(); - const time = new Date(live.time).getTime(); - const isLive = live.streaming; - const isUpcoming = time > now && !isLive; - const isEnded = time < now && !isLive; + return 0; + }) + .reduce( + ( + acc: { + live: Live[]; + upcoming: Live[]; + ended: Live[]; + }, + live + ) => { + const now = Date.now(); + const time = new Date(live.time).getTime(); + const isLive = live.streaming; + const isUpcoming = time > now && !isLive; + const isEnded = time < now && !isLive; - if (isLive) { - acc.live.push(live); - } else if (isUpcoming) { - acc.upcoming.push(live); - } else if (isEnded) { - acc.ended.push(live); - } + if (isLive) { + acc.live.push(live); + } else if (isUpcoming) { + acc.upcoming.push(live); + } else if (isEnded) { + acc.ended.push(live); + } - return acc; - }, - { live: [], upcoming: [], ended: [] } - ); + return acc; + }, + { live: [], upcoming: [], ended: [] } + ); </script> {#if schedule.lives.length === 0} diff --git a/src/lib/Hololive/Stream.svelte b/src/lib/Hololive/Stream.svelte index afe400b7..5c833a50 100644 --- a/src/lib/Hololive/Stream.svelte +++ b/src/lib/Hololive/Stream.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; - import root from '$lib/Utility/root'; - import identity from '$stores/identity'; - import locale from '$stores/locale'; - import Icon from '@iconify/svelte'; - import type { LiveInfo } from '$lib/Data/hololive'; - - export let live: LiveInfo; - export let pinStream: (streamer: string) => void; - export let pinnedStreams: string[]; - export let icon: string; +import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; +import root from '$lib/Utility/root'; +import identity from '$stores/identity'; +import locale from '$stores/locale'; +import Icon from '@iconify/svelte'; +import type { LiveInfo } from '$lib/Data/hololive'; + +export let live: LiveInfo; +export let pinStream: (streamer: string) => void; +export let pinnedStreams: string[]; +export let icon: string; </script> <div class="stream card"> diff --git a/src/lib/Home/HeadTitle.svelte b/src/lib/Home/HeadTitle.svelte index 1e86cab2..dbda7afe 100644 --- a/src/lib/Home/HeadTitle.svelte +++ b/src/lib/Home/HeadTitle.svelte @@ -1,6 +1,6 @@ <script lang="ts"> - let { route = undefined, path = '/' }: { route?: string; path?: string } = $props(); - const title = $derived((route ? `${route} • ` : '') + 'due.moe'); +let { route = undefined, path = '/' }: { route?: string; path?: string } = $props(); +const title = $derived((route ? `${route} • ` : '') + 'due.moe'); </script> <svelte:head> diff --git a/src/lib/Home/LastActivity.svelte b/src/lib/Home/LastActivity.svelte index 61cf6c7d..5c9ee4c2 100644 --- a/src/lib/Home/LastActivity.svelte +++ b/src/lib/Home/LastActivity.svelte @@ -1,53 +1,53 @@ <script lang="ts"> - import userIdentity from '$stores/identity'; - import { onMount } from 'svelte'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { lastActivityDate } from '../Data/AniList/activity'; - import settings from '$stores/settings'; - - let { user }: { user: AniListAuthorisation } = $props(); - let lastActivityWasToday = $state(true); - - onMount(async () => { - if (!$settings.displayDisableLastActivityWarning && user !== undefined) { - lastActivityWasToday = - (await lastActivityDate($userIdentity, user)).date.toDateString() >= - new Date().toDateString(); - - if (!lastActivityWasToday) { - if ($settings.displayLimitListHeight) { - document.querySelectorAll('.list').forEach((list) => { - (list as HTMLElement).style.maxHeight = `calc((100vh - ${ - document.querySelector('#list-container')?.getBoundingClientRect().top - }px) - 5rem)`; - }); - } +import userIdentity from '$stores/identity'; +import { onMount } from 'svelte'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import { lastActivityDate } from '../Data/AniList/activity'; +import settings from '$stores/settings'; + +let { user }: { user: AniListAuthorisation } = $props(); +let lastActivityWasToday = $state(true); + +onMount(async () => { + if (!$settings.displayDisableLastActivityWarning && user !== undefined) { + lastActivityWasToday = + (await lastActivityDate($userIdentity, user)).date.toDateString() >= + new Date().toDateString(); + + if (!lastActivityWasToday) { + if ($settings.displayLimitListHeight) { + document.querySelectorAll('.list').forEach((list) => { + (list as HTMLElement).style.maxHeight = `calc((100vh - ${ + document.querySelector('#list-container')?.getBoundingClientRect().top + }px) - 5rem)`; + }); } } - }); - - const timeLeftToday = () => { - const now = new Date(); - const currentHour = now.getHours(); - const currentMinute = now.getMinutes(); - const hoursLeft = 24 - currentHour; - let minutesLeft = 0; - let timeLeft = ''; - - if (hoursLeft > 0) { - minutesLeft = hoursLeft * 60 - currentMinute; - } else { - minutesLeft = 24 * 60 - (currentHour * 60 + currentMinute); - } - - if (minutesLeft > 60) { - timeLeft = `${Math.round(minutesLeft / 60)} hours`; - } else { - timeLeft = `${minutesLeft} minutes`; - } - - return timeLeft; - }; + } +}); + +const timeLeftToday = () => { + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + const hoursLeft = 24 - currentHour; + let minutesLeft = 0; + let timeLeft = ''; + + if (hoursLeft > 0) { + minutesLeft = hoursLeft * 60 - currentMinute; + } else { + minutesLeft = 24 * 60 - (currentHour * 60 + currentMinute); + } + + if (minutesLeft > 60) { + timeLeft = `${Math.round(minutesLeft / 60)} hours`; + } else { + timeLeft = `${minutesLeft} minutes`; + } + + return timeLeft; +}; </script> {#if !$settings.displayDisableLastActivityWarning && !lastActivityWasToday} diff --git a/src/lib/Home/Root.svelte b/src/lib/Home/Root.svelte index eb174471..b3c2279e 100644 --- a/src/lib/Home/Root.svelte +++ b/src/lib/Home/Root.svelte @@ -1,11 +1,11 @@ <script lang="ts"> - import settings from '$stores/settings'; - import { fly } from 'svelte/transition'; +import settings from '$stores/settings'; +import { fly } from 'svelte/transition'; - export let data: { url: string }; - export let way: number; +export let data: { url: string }; +export let way: number; - const animationDelay = 100; +const animationDelay = 100; </script> {#if $settings.displayDisableAnimations} diff --git a/src/lib/Image/FallbackImage.svelte b/src/lib/Image/FallbackImage.svelte index 7e458339..27c427be 100644 --- a/src/lib/Image/FallbackImage.svelte +++ b/src/lib/Image/FallbackImage.svelte @@ -1,24 +1,24 @@ <script lang="ts"> - export let source: string | undefined | null; - export let alternative: string | undefined | null; - export let fallback: string | undefined | null; - export let maxReplaceCount = 1; - export let replaceDelay = 1000; - export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; - export let hideOnError = false; - export let style = ''; +export let source: string | undefined | null; +export let alternative: string | undefined | null; +export let fallback: string | undefined | null; +export let maxReplaceCount = 1; +export let replaceDelay = 1000; +export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; +export let hideOnError = false; +export let style = ''; - let replaceCount = 0; +let replaceCount = 0; - const delayedReplace = (event: Event, image: string | undefined | null) => { - if (replaceCount >= maxReplaceCount) return; +const delayedReplace = (event: Event, image: string | undefined | null) => { + if (replaceCount >= maxReplaceCount) return; - setTimeout(() => { - (event.target as HTMLImageElement).src = image || ''; + setTimeout(() => { + (event.target as HTMLImageElement).src = image || ''; - replaceCount += 1; - }, replaceDelay); - }; + replaceCount += 1; + }, replaceDelay); +}; </script> {#if replaceCount < maxReplaceCount} diff --git a/src/lib/Image/ParallaxImage.svelte b/src/lib/Image/ParallaxImage.svelte index eb0383ef..66906cbc 100644 --- a/src/lib/Image/ParallaxImage.svelte +++ b/src/lib/Image/ParallaxImage.svelte @@ -1,40 +1,40 @@ <script lang="ts"> - import { cubicOut } from 'svelte/easing'; - import { tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; +import { tweened } from 'svelte/motion'; - export let source: string; - export let duration = 300 * 1.75; - export let easing = cubicOut; - export let alternativeText: string; - export let classList = ''; - export let style = ''; - export let factor = 1.25; - export let limit = 300 * 1.75; - export let borderRadius = '8px'; - export let loading: 'lazy' | 'eager' = 'lazy'; - export let fetchpriority: 'high' | 'low' | 'auto' | undefined = undefined; - export let width: number | undefined = undefined; - export let height: number | undefined = undefined; +export let source: string; +export let duration = 300 * 1.75; +export let easing = cubicOut; +export let alternativeText: string; +export let classList = ''; +export let style = ''; +export let factor = 1.25; +export let limit = 300 * 1.75; +export let borderRadius = '8px'; +export let loading: 'lazy' | 'eager' = 'lazy'; +export let fetchpriority: 'high' | 'low' | 'auto' | undefined = undefined; +export let width: number | undefined = undefined; +export let height: number | undefined = undefined; - let badgeReference: HTMLImageElement; - const mouse = tweened({ x: 0, y: 0 }, { duration, easing }); +let badgeReference: HTMLImageElement; +const mouse = tweened({ x: 0, y: 0 }, { duration, easing }); - const handleMouseMove = (event: MouseEvent) => { - const boundingRectangle = badgeReference.getBoundingClientRect(); +const handleMouseMove = (event: MouseEvent) => { + const boundingRectangle = badgeReference.getBoundingClientRect(); - if ($mouse.x === 0 && $mouse.y === 0) $mouse = { x: event.clientX, y: event.clientY }; + if ($mouse.x === 0 && $mouse.y === 0) $mouse = { x: event.clientX, y: event.clientY }; - $mouse.x += - (-(event.clientX - boundingRectangle.left - boundingRectangle.width / 2) - $mouse.x) * factor; - $mouse.y += - (-(event.clientY - boundingRectangle.top - boundingRectangle.height / 2) - $mouse.y) * factor; - $mouse.x = Math.max(Math.min($mouse.x, limit), -limit); - $mouse.y = Math.max(Math.min($mouse.y, limit), -limit); - }; + $mouse.x += + (-(event.clientX - boundingRectangle.left - boundingRectangle.width / 2) - $mouse.x) * factor; + $mouse.y += + (-(event.clientY - boundingRectangle.top - boundingRectangle.height / 2) - $mouse.y) * factor; + $mouse.x = Math.max(Math.min($mouse.x, limit), -limit); + $mouse.y = Math.max(Math.min($mouse.y, limit), -limit); +}; - const handleMouseLeave = () => { - $mouse = { x: 0, y: 0 }; - }; +const handleMouseLeave = () => { + $mouse = { x: 0, y: 0 }; +}; </script> <img diff --git a/src/lib/Landing.svelte b/src/lib/Landing.svelte index 4a5c7df0..bddd16df 100644 --- a/src/lib/Landing.svelte +++ b/src/lib/Landing.svelte @@ -1,67 +1,67 @@ <script lang="ts"> - import root from './Utility/root'; - import { env } from '$env/dynamic/public'; - import CompletedAnimeList from './List/Anime/CompletedAnimeList.svelte'; - import MangaListTemplate from './List/Manga/MangaListTemplate.svelte'; - import localforage from 'localforage'; - import { onMount } from 'svelte'; - - let sectionsVisible = $state<boolean[]>([false, false, false]); - let mangaContainer: HTMLElement; - let animeContainer: HTMLElement; - let gridLimit = $state<number | undefined>(undefined); - let demoFocused = $state(false); - const COVER_WIDTH = 100; - const COVER_GAP = 12; - const dummyCount = 20; - - const calculateLimit = () => { - const container = mangaContainer || animeContainer; - - if (!container) return; - - const containerWidth = container.clientWidth; - const itemWidth = COVER_WIDTH + COVER_GAP; - const padding = containerWidth < 500 ? 48 : 36; - - gridLimit = Math.max(2, Math.floor((containerWidth - padding) / itemWidth)); +import root from './Utility/root'; +import { env } from '$env/dynamic/public'; +import CompletedAnimeList from './List/Anime/CompletedAnimeList.svelte'; +import MangaListTemplate from './List/Manga/MangaListTemplate.svelte'; +import localforage from 'localforage'; +import { onMount } from 'svelte'; + +let sectionsVisible = $state<boolean[]>([false, false, false]); +let mangaContainer: HTMLElement; +let animeContainer: HTMLElement; +let gridLimit = $state<number | undefined>(undefined); +let demoFocused = $state(false); +const COVER_WIDTH = 100; +const COVER_GAP = 12; +const dummyCount = 20; + +const calculateLimit = () => { + const container = mangaContainer || animeContainer; + + if (!container) return; + + const containerWidth = container.clientWidth; + const itemWidth = COVER_WIDTH + COVER_GAP; + const padding = containerWidth < 500 ? 48 : 36; + + gridLimit = Math.max(2, Math.floor((containerWidth - padding) / itemWidth)); +}; + +onMount(() => { + const intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const index = Number(entry.target.getAttribute('data-section')); + + sectionsVisible[index] = true; + } + }); + }, + { threshold: 0.1 } + ); + + document.querySelectorAll('.landing-section').forEach((el) => intersectionObserver.observe(el)); + + calculateLimit(); + + const resizeObserver = new ResizeObserver(calculateLimit); + + if (mangaContainer) resizeObserver.observe(mangaContainer); + if (animeContainer) resizeObserver.observe(animeContainer); + + const handleKeydown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && demoFocused) demoFocused = false; }; - onMount(() => { - const intersectionObserver = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const index = Number(entry.target.getAttribute('data-section')); + document.addEventListener('keydown', handleKeydown); - sectionsVisible[index] = true; - } - }); - }, - { threshold: 0.1 } - ); - - document.querySelectorAll('.landing-section').forEach((el) => intersectionObserver.observe(el)); - - calculateLimit(); - - const resizeObserver = new ResizeObserver(calculateLimit); - - if (mangaContainer) resizeObserver.observe(mangaContainer); - if (animeContainer) resizeObserver.observe(animeContainer); - - const handleKeydown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && demoFocused) demoFocused = false; - }; - - document.addEventListener('keydown', handleKeydown); - - return () => { - intersectionObserver.disconnect(); - resizeObserver.disconnect(); - document.removeEventListener('keydown', handleKeydown); - }; - }); + return () => { + intersectionObserver.disconnect(); + resizeObserver.disconnect(); + document.removeEventListener('keydown', handleKeydown); + }; +}); </script> <div class="landing"> diff --git a/src/lib/LandingHero.svelte b/src/lib/LandingHero.svelte index 29f9f556..fc87b5fe 100644 --- a/src/lib/LandingHero.svelte +++ b/src/lib/LandingHero.svelte @@ -1,16 +1,16 @@ <script lang="ts"> - import { env } from '$env/dynamic/public'; - import localforage from 'localforage'; +import { env } from '$env/dynamic/public'; +import localforage from 'localforage'; - let heroSection = $state<HTMLElement>(); +let heroSection = $state<HTMLElement>(); - const scrollPastHero = () => { - if (!heroSection) return; +const scrollPastHero = () => { + if (!heroSection) return; - const heroBottom = heroSection.getBoundingClientRect().bottom + window.scrollY; + const heroBottom = heroSection.getBoundingClientRect().bottom + window.scrollY; - window.scrollTo({ top: heroBottom, behavior: 'smooth' }); - }; + window.scrollTo({ top: heroBottom, behavior: 'smooth' }); +}; </script> <section class="hero" bind:this={heroSection}> diff --git a/src/lib/Layout/Dropdown.svelte b/src/lib/Layout/Dropdown.svelte index e7e00272..324c9498 100644 --- a/src/lib/Layout/Dropdown.svelte +++ b/src/lib/Layout/Dropdown.svelte @@ -1,21 +1,21 @@ <script lang="ts"> - interface Item { - name: string; - url: string; - onClick?: () => void; - preventDefault?: boolean; - } - - export let items: Item[] = []; - export let title: string | undefined = undefined; - export let header = true; - export let center = false; - - let open = false; - - const handleClickOutside = (event: MouseEvent) => { - if (!(event.target as HTMLElement).closest('.dropdown')) open = false; - }; +interface Item { + name: string; + url: string; + onClick?: () => void; + preventDefault?: boolean; +} + +export let items: Item[] = []; +export let title: string | undefined = undefined; +export let header = true; +export let center = false; + +let open = false; + +const handleClickOutside = (event: MouseEvent) => { + if (!(event.target as HTMLElement).closest('.dropdown')) open = false; +}; </script> <svelte:window onclick={handleClickOutside} /> diff --git a/src/lib/Layout/NumberTicker.svelte b/src/lib/Layout/NumberTicker.svelte index 226e89c9..61e1757d 100644 --- a/src/lib/Layout/NumberTicker.svelte +++ b/src/lib/Layout/NumberTicker.svelte @@ -1,21 +1,21 @@ <script> - import { tweened } from 'svelte/motion'; - import { cubicOut } from 'svelte/easing'; +import { tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; - export let start = 0; - export let end = 100; - export let duration = 2000; - export let delay = 0; +export let start = 0; +export let end = 100; +export let duration = 2000; +export let delay = 0; - const count = tweened(start, { - duration: duration, - easing: cubicOut, - delay: delay - }); +const count = tweened(start, { + duration: duration, + easing: cubicOut, + delay: delay +}); - $: { - count.set(end); - } +$: { + count.set(end); +} </script> <span class="counter" class:visible={$count !== start}> diff --git a/src/lib/Layout/Popup.svelte b/src/lib/Layout/Popup.svelte index dc1557e3..92261a5b 100644 --- a/src/lib/Layout/Popup.svelte +++ b/src/lib/Layout/Popup.svelte @@ -1,37 +1,37 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { onMount } from 'svelte'; - - export let onLeave = () => { - return; - }; - export let card = true; - export let smallCard = false; - export let fullscreen = false; - export let show = true; - export let locked = false; - export let center = false; - - const handleClickOutside = (event: MouseEvent) => { - if (!locked && (event.target as HTMLElement).classList.contains('popup')) { - show = false; - - onLeave(); - } - }; - - onMount(() => { - if (browser) document.body.style.overflow = 'auto'; - }); - - $: { - if (browser) { - document.body.style.overflow = 'auto'; - - if (show) document.body.style.overflow = 'hidden'; - else document.body.style.overflow = 'auto'; - } +import { browser } from '$app/environment'; +import { onMount } from 'svelte'; + +export let onLeave = () => { + return; +}; +export let card = true; +export let smallCard = false; +export let fullscreen = false; +export let show = true; +export let locked = false; +export let center = false; + +const handleClickOutside = (event: MouseEvent) => { + if (!locked && (event.target as HTMLElement).classList.contains('popup')) { + show = false; + + onLeave(); } +}; + +onMount(() => { + if (browser) document.body.style.overflow = 'auto'; +}); + +$: { + if (browser) { + document.body.style.overflow = 'auto'; + + if (show) document.body.style.overflow = 'hidden'; + else document.body.style.overflow = 'auto'; + } +} </script> <svelte:window onclick={handleClickOutside} /> diff --git a/src/lib/Layout/Spacer.svelte b/src/lib/Layout/Spacer.svelte index a26796df..a4ecef18 100644 --- a/src/lib/Layout/Spacer.svelte +++ b/src/lib/Layout/Spacer.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let size: 'sm' | 'md' | 'lg' = 'md'; +export let size: 'sm' | 'md' | 'lg' = 'md'; </script> <div class="spacer {size}"></div> diff --git a/src/lib/Layout/TextTransition.svelte b/src/lib/Layout/TextTransition.svelte index 35cfe1ea..13a4ed9b 100644 --- a/src/lib/Layout/TextTransition.svelte +++ b/src/lib/Layout/TextTransition.svelte @@ -1,24 +1,24 @@ <script> - import { tweened } from 'svelte/motion'; - import { cubicOut } from 'svelte/easing'; +import { tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; - export let text = ''; - export let opacityTransitionDuration = 50; - export let blurTransitionDuration = opacityTransitionDuration; - export let easing = cubicOut; +export let text = ''; +export let opacityTransitionDuration = 50; +export let blurTransitionDuration = opacityTransitionDuration; +export let easing = cubicOut; - let previousValue = ''; - let opacity = tweened(1, { duration: opacityTransitionDuration, easing }); - let blur = tweened(0, { duration: blurTransitionDuration, easing }); +let previousValue = ''; +let opacity = tweened(1, { duration: opacityTransitionDuration, easing }); +let blur = tweened(0, { duration: blurTransitionDuration, easing }); - $: { - if (text !== previousValue) - Promise.all([opacity.set(0), blur.set(10)]).then(() => { - previousValue = text; +$: { + if (text !== previousValue) + Promise.all([opacity.set(0), blur.set(10)]).then(() => { + previousValue = text; - Promise.all([opacity.set(1), blur.set(0)]); - }); - } + Promise.all([opacity.set(1), blur.set(0)]); + }); +} </script> <span diff --git a/src/lib/Layout/Username.svelte b/src/lib/Layout/Username.svelte index 8b89b708..bd902cda 100644 --- a/src/lib/Layout/Username.svelte +++ b/src/lib/Layout/Username.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let username: string; +export let username: string; </script> <a href={`https://anilist.co/user/${username}/`}>@{username}</a> diff --git a/src/lib/Lazy.svelte b/src/lib/Lazy.svelte index fd08ed07..6386177b 100644 --- a/src/lib/Lazy.svelte +++ b/src/lib/Lazy.svelte @@ -1,47 +1,44 @@ <script lang="ts"> - import { onMount, onDestroy } from 'svelte'; - - export let top = 0; - export let bottom = 0; - export let left = 0; - export let right = 0; - export let steps = 100; - export let threshold = 0.01; - export let once = false; - - let element: Element; - let percent = 0; - let visible = false; - let observer: IntersectionObserver; - - const intersectPercent = ( - entries: IntersectionObserverEntry[], - observer: IntersectionObserver - ) => { - for (let entry of entries) - if (entry.target === element) { - percent = Math.round(entry.intersectionRatio * 100); - visible = entry.intersectionRatio >= threshold; - - if (visible && once) observer.unobserve(element); - - break; - } - }; - - onMount(() => { - if ('IntersectionObserver' in window) { - let options = { - rootMargin: `${top}px ${right}px ${bottom}px ${left}px`, - threshold: Array.from({ length: steps }, (_, i) => i / (steps - 1)) - }; - - observer = new IntersectionObserver(intersectPercent, options); - observer.observe(element); +import { onMount, onDestroy } from 'svelte'; + +export let top = 0; +export let bottom = 0; +export let left = 0; +export let right = 0; +export let steps = 100; +export let threshold = 0.01; +export let once = false; + +let element: Element; +let percent = 0; +let visible = false; +let observer: IntersectionObserver; + +const intersectPercent = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { + for (let entry of entries) + if (entry.target === element) { + percent = Math.round(entry.intersectionRatio * 100); + visible = entry.intersectionRatio >= threshold; + + if (visible && once) observer.unobserve(element); + + break; } - }); +}; - onDestroy(() => observer?.unobserve(element)); +onMount(() => { + if ('IntersectionObserver' in window) { + let options = { + rootMargin: `${top}px ${right}px ${bottom}px ${left}px`, + threshold: Array.from({ length: steps }, (_, i) => i / (steps - 1)) + }; + + observer = new IntersectionObserver(intersectPercent, options); + observer.observe(element); + } +}); + +onDestroy(() => observer?.unobserve(element)); </script> <div bind:this={element}> 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)}`}> diff --git a/src/lib/Loading/Ellipsis.svelte b/src/lib/Loading/Ellipsis.svelte index dfc8bcf4..674404fb 100644 --- a/src/lib/Loading/Ellipsis.svelte +++ b/src/lib/Loading/Ellipsis.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let colour = 'var(--fg)'; +export let colour = 'var(--fg)'; </script> <div class="ellipsis" style={`--loader-colour: ${colour};`}> diff --git a/src/lib/Loading/Grid.svelte b/src/lib/Loading/Grid.svelte index 543c5e5f..51fbd67e 100644 --- a/src/lib/Loading/Grid.svelte +++ b/src/lib/Loading/Grid.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let colour = 'var(--fg)'; +export let colour = 'var(--fg)'; </script> <div class="grid" style={`--loader-colour: ${colour};`}> diff --git a/src/lib/Loading/Message.svelte b/src/lib/Loading/Message.svelte index 53c993d3..9ae91704 100644 --- a/src/lib/Loading/Message.svelte +++ b/src/lib/Loading/Message.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import Ellipsis from './Ellipsis.svelte'; - import Ripple from './Ripple.svelte'; - import Grid from './Grid.svelte'; - import Popup from '$lib/Layout/Popup.svelte'; +import Ellipsis from './Ellipsis.svelte'; +import Ripple from './Ripple.svelte'; +import Grid from './Grid.svelte'; +import Popup from '$lib/Layout/Popup.svelte'; - export let message: string | undefined = undefined; - export let loader: 'ellipsis' | 'ripple' | 'grid' = 'ellipsis'; - export let colour = 'var(--fg)'; - export let slot = false; - export let withReload = false; - export let fullscreen = true; +export let message: string | undefined = undefined; +export let loader: 'ellipsis' | 'ripple' | 'grid' = 'ellipsis'; +export let colour = 'var(--fg)'; +export let slot = false; +export let withReload = false; +export let fullscreen = true; </script> <Popup {fullscreen} locked> diff --git a/src/lib/Loading/Ripple.svelte b/src/lib/Loading/Ripple.svelte index 0ce3d23c..74cf11b1 100644 --- a/src/lib/Loading/Ripple.svelte +++ b/src/lib/Loading/Ripple.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let colour = 'var(--fg)'; +export let colour = 'var(--fg)'; </script> <div class="ripple" style={`--loader-colour: ${colour};`}> diff --git a/src/lib/Loading/Skeleton.svelte b/src/lib/Loading/Skeleton.svelte index a0e1ab0e..03637c28 100644 --- a/src/lib/Loading/Skeleton.svelte +++ b/src/lib/Loading/Skeleton.svelte @@ -1,13 +1,13 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - export let count = 3; - export let width = '100%'; - export let height = '100px'; - export let card = true; - export let grid = false; - export let list = false; - export let pad = false; - export let bigCard = false; +import Spacer from '$lib/Layout/Spacer.svelte'; +export let count = 3; +export let width = '100%'; +export let height = '100px'; +export let card = true; +export let grid = false; +export let list = false; +export let pad = false; +export let bigCard = false; </script> {#if grid} diff --git a/src/lib/MarkdownLink.svelte b/src/lib/MarkdownLink.svelte index ee8c4900..0e01db41 100644 --- a/src/lib/MarkdownLink.svelte +++ b/src/lib/MarkdownLink.svelte @@ -1,20 +1,20 @@ <script lang="ts"> - let { href, text }: { href: string; text: string } = $props(); - let safeHref = $derived.by(() => { - try { - let url = new URL(href); +let { href, text }: { href: string; text: string } = $props(); +let safeHref = $derived.by(() => { + try { + let url = new URL(href); - switch (url.protocol) { - case 'javascript:': - return '#'; + switch (url.protocol) { + case 'javascript:': + return '#'; - default: - return href; - } - } catch (error) { - return '#'; + default: + return href; } - }); + } catch (error) { + return '#'; + } +}); </script> <a href={safeHref} target="_blank">{text}</a> diff --git a/src/lib/Media/Anime/Airing/AiringTime.svelte b/src/lib/Media/Anime/Airing/AiringTime.svelte index ff888629..0278de1c 100644 --- a/src/lib/Media/Anime/Airing/AiringTime.svelte +++ b/src/lib/Media/Anime/Airing/AiringTime.svelte @@ -1,120 +1,118 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; - import settings from '$stores/settings'; - import type { MediaPrequel } from '$lib/Data/AniList/prequels'; - import tooltip from '$lib/Tooltip/tooltip'; - import locale from '$stores/locale'; - import airingNow from '$stores/airingNow'; - - export let originalAnime: Media; - export let upcoming = false; - - const anime = originalAnime; - let opacity = 100; - let timeFrame = ''; - let time = ''; - let nextEpisode = anime.nextAiringEpisode?.episode || 0; - let few = true; - let dateString = ''; - const setAiringTime = () => { - time = ''; - timeFrame = ''; - dateString = ''; - - const airingAt = anime.nextAiringEpisode?.airingAt; - const untilAiring = airingAt - ? Math.round((airingAt - $airingNow / 1000) * 100) / 100 - : undefined; - let hours = null; - const shortenCountdown = $settings.displayShortCountdown; - - time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { - hour12: !$settings.display24HourTime, - hour: 'numeric', - minute: '2-digit' - }); - - if ( - (anime as unknown as MediaPrequel).startDate && - new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ) < new Date() - ) - return `<span class="opaque">on ${new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ).toLocaleDateString()}</span>`; - - if (untilAiring !== undefined) { - let minutes = Math.round(untilAiring / 60); - - few = true; - - if (minutes > 60) { - hours = minutes / 60; - - if (hours > 24) { - const days = Math.floor(hours / 24); - const weeks = Math.floor(days / 7); - - few = false; - - if (weeks >= 1.5) { - timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ - weeks === 1 || shortenCountdown ? '' : 's' - }`; - - const residualDays = days % 7; - - if (residualDays > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualDays}${ - shortenCountdown ? 'd' : ' day' - }${residualDays === 1 || shortenCountdown ? '' : 's'}`; - } else { - timeFrame += `${days}${shortenCountdown ? 'd' : ' day'}${ - days === 1 || shortenCountdown ? '' : 's' - }`; - } - - const residualHours = Math.floor(hours - days * 24); - - if (residualHours > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ - shortenCountdown ? 'h' : ' hour' - }${residualHours === 1 || shortenCountdown ? '' : 's'}`; - } else { - const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); - - timeFrame += `${Math.floor(hours).toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ - Math.floor(hours) === 1 || shortenCountdown ? '' : 's' +import type { Media } from '$lib/Data/AniList/media'; +import settings from '$stores/settings'; +import type { MediaPrequel } from '$lib/Data/AniList/prequels'; +import tooltip from '$lib/Tooltip/tooltip'; +import locale from '$stores/locale'; +import airingNow from '$stores/airingNow'; + +export let originalAnime: Media; +export let upcoming = false; + +const anime = originalAnime; +let opacity = 100; +let timeFrame = ''; +let time = ''; +let nextEpisode = anime.nextAiringEpisode?.episode || 0; +let few = true; +let dateString = ''; +const setAiringTime = () => { + time = ''; + timeFrame = ''; + dateString = ''; + + const airingAt = anime.nextAiringEpisode?.airingAt; + const untilAiring = airingAt ? Math.round((airingAt - $airingNow / 1000) * 100) / 100 : undefined; + let hours = null; + const shortenCountdown = $settings.displayShortCountdown; + + time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { + hour12: !$settings.display24HourTime, + hour: 'numeric', + minute: '2-digit' + }); + + if ( + (anime as unknown as MediaPrequel).startDate && + new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ) < new Date() + ) + return `<span class="opaque">on ${new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ).toLocaleDateString()}</span>`; + + if (untilAiring !== undefined) { + let minutes = Math.round(untilAiring / 60); + + few = true; + + if (minutes > 60) { + hours = minutes / 60; + + if (hours > 24) { + const days = Math.floor(hours / 24); + const weeks = Math.floor(days / 7); + + few = false; + + if (weeks >= 1.5) { + timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ + weeks === 1 || shortenCountdown ? '' : 's' }`; - if (residualMinutes > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ - shortenCountdown ? 'm' : ' minute' - }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; + const residualDays = days % 7; + + if (residualDays > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualDays}${ + shortenCountdown ? 'd' : ' day' + }${residualDays === 1 || shortenCountdown ? '' : 's'}`; + } else { + timeFrame += `${days}${shortenCountdown ? 'd' : ' day'}${ + days === 1 || shortenCountdown ? '' : 's' + }`; } + + const residualHours = Math.floor(hours - days * 24); + + if (residualHours > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ + shortenCountdown ? 'h' : ' hour' + }${residualHours === 1 || shortenCountdown ? '' : 's'}`; } else { - minutes = Math.round(minutes); + const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); - timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ - minutes === 1 || shortenCountdown ? '' : 's' + timeFrame += `${Math.floor(hours).toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ + Math.floor(hours) === 1 || shortenCountdown ? '' : 's' }`; + + if (residualMinutes > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ + shortenCountdown ? 'm' : ' minute' + }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; } + } else { + minutes = Math.round(minutes); - opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); - dateString = $locale().dateFormatter(new Date(airingAt ? airingAt * 1000 : 0)); + timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ + minutes === 1 || shortenCountdown ? '' : 's' + }`; } - }; - - $: { - $airingNow; - setAiringTime(); + opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); + dateString = $locale().dateFormatter(new Date(airingAt ? airingAt * 1000 : 0)); } +}; + +$: { + $airingNow; + + setAiringTime(); +} </script> {#if upcoming} diff --git a/src/lib/Media/Cover/HoverCover.svelte b/src/lib/Media/Cover/HoverCover.svelte index 81d6d3fc..48027e44 100644 --- a/src/lib/Media/Cover/HoverCover.svelte +++ b/src/lib/Media/Cover/HoverCover.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import settings from '$stores/settings'; - import type { HoverCoverResponse } from './hoverCover'; +import settings from '$stores/settings'; +import type { HoverCoverResponse } from './hoverCover'; - export let options: HoverCoverResponse; - export let width = 250; +export let options: HoverCoverResponse; +export let width = 250; </script> {#if options.hovering} diff --git a/src/lib/Notification/Notification.svelte b/src/lib/Notification/Notification.svelte index 864d6568..4e6fd2e7 100644 --- a/src/lib/Notification/Notification.svelte +++ b/src/lib/Notification/Notification.svelte @@ -1,21 +1,21 @@ <script lang="ts"> - import settings from '$stores/settings'; - import { onMount } from 'svelte'; - import type { Notification } from './store'; +import settings from '$stores/settings'; +import { onMount } from 'svelte'; +import type { Notification } from './store'; - export let notification: Notification; - export let onRemove: () => void = () => { - return; - }; - export let removed = false; +export let notification: Notification; +export let onRemove: () => void = () => { + return; +}; +export let removed = false; - onMount(() => setTimeout(remove, notification.duration)); +onMount(() => setTimeout(remove, notification.duration)); - const remove = () => { - removed = true; +const remove = () => { + removed = true; - setTimeout(onRemove, 150); - }; + setTimeout(onRemove, 150); +}; </script> {#if !$settings.displayDisableNotifications} diff --git a/src/lib/Notification/NotificationsProvider.svelte b/src/lib/Notification/NotificationsProvider.svelte index 964317e9..15e5f2b2 100644 --- a/src/lib/Notification/NotificationsProvider.svelte +++ b/src/lib/Notification/NotificationsProvider.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import { notifications } from './store'; - import EventNotification from './Notification.svelte'; +import { notifications } from './store'; +import EventNotification from './Notification.svelte'; - export let zIndex = 5000; +export let zIndex = 5000; - const handleRemove = (id: string) => { - notifications.remove(id); - }; +const handleRemove = (id: string) => { + notifications.remove(id); +}; </script> <div class="notifications-container" style="z-index: {zIndex};"> diff --git a/src/lib/Reader/Chapters/MangaDex.svelte b/src/lib/Reader/Chapters/MangaDex.svelte index ac5ce151..f1a90338 100644 --- a/src/lib/Reader/Chapters/MangaDex.svelte +++ b/src/lib/Reader/Chapters/MangaDex.svelte @@ -1,18 +1,18 @@ <script lang="ts"> - interface MangaDexChapter { - id: string; - attributes: { - volume?: string; - chapter: string; - title?: string; - }; - } +interface MangaDexChapter { + id: string; + attributes: { + volume?: string; + chapter: string; + title?: string; + }; +} - interface MangaDexData { - data: MangaDexChapter[]; - } +interface MangaDexData { + data: MangaDexChapter[]; +} - export let data: MangaDexData; +export let data: MangaDexData; </script> <ul> diff --git a/src/lib/Reader/Chapters/Rawkuma.svelte b/src/lib/Reader/Chapters/Rawkuma.svelte index 720c65d5..02e0ed18 100644 --- a/src/lib/Reader/Chapters/Rawkuma.svelte +++ b/src/lib/Reader/Chapters/Rawkuma.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import { getChaptersFromText } from '$lib/Data/Manga/raw'; - import { onMount } from 'svelte'; +import { getChaptersFromText } from '$lib/Data/Manga/raw'; +import { onMount } from 'svelte'; - export let data: string; +export let data: string; - onMount(() => { - console.log(); - }); +onMount(() => { + console.log(); +}); </script> <ul> diff --git a/src/lib/Schedule/CoverBypass.svelte b/src/lib/Schedule/CoverBypass.svelte index 1f166e06..74810105 100644 --- a/src/lib/Schedule/CoverBypass.svelte +++ b/src/lib/Schedule/CoverBypass.svelte @@ -1,22 +1,22 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; - import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; - import type { SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; +import type { Media } from '$lib/Data/AniList/media'; +import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; +import type { SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; - import { outboundLink } from '$lib/Media/links'; - import tooltip from '$lib/Tooltip/tooltip'; - import { abbreviate } from '$lib/Utility/string'; - import settings from '$stores/settings'; +import { outboundLink } from '$lib/Media/links'; +import tooltip from '$lib/Tooltip/tooltip'; +import { abbreviate } from '$lib/Utility/string'; +import settings from '$stores/settings'; - export let media: Media | null; - export let entry: SubsPleaseEpisode; - export let cover = true; - export let showTooltip = true; +export let media: Media | null; +export let entry: SubsPleaseEpisode; +export let cover = true; +export let showTooltip = true; - const abbreviateTo = 40; +const abbreviateTo = 40; - const titleSelect = (media: Media | null) => - media ? media.title.english || media.title.romaji || media.title.native : null; +const titleSelect = (media: Media | null) => + media ? media.title.english || media.title.romaji || media.title.native : null; </script> <a diff --git a/src/lib/Schedule/Crunchyroll.svelte b/src/lib/Schedule/Crunchyroll.svelte index 9d7c8e70..becabebc 100644 --- a/src/lib/Schedule/Crunchyroll.svelte +++ b/src/lib/Schedule/Crunchyroll.svelte @@ -1,47 +1,47 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import crunchyroll from '$lib/Data/Static/crunchyroll.json'; - import './container.css'; - - interface CrunchyrollMedia<T = number | 'soon' | 'continuing'> { - year: number; - month: number; - day: T; - title: string; - } +import Spacer from '$lib/Layout/Spacer.svelte'; +import crunchyroll from '$lib/Data/Static/crunchyroll.json'; +import './container.css'; + +interface CrunchyrollMedia<T = number | 'soon' | 'continuing'> { + year: number; + month: number; + day: T; + title: string; +} - type KnownMedia = { [key: string]: CrunchyrollMedia<number>[] }; +type KnownMedia = { [key: string]: CrunchyrollMedia<number>[] }; - const days: KnownMedia = crunchyroll - .filter((media) => media.day !== 'soon' && media.day !== 'continuing') - .reduce((acc: KnownMedia, media) => { - const date = new Date(media.year, media.month - 1, media.day as number).toLocaleDateString(); +const days: KnownMedia = crunchyroll + .filter((media) => media.day !== 'soon' && media.day !== 'continuing') + .reduce((acc: KnownMedia, media) => { + const date = new Date(media.year, media.month - 1, media.day as number).toLocaleDateString(); - if (!acc[date]) acc[date] = []; + if (!acc[date]) acc[date] = []; - acc[date].push(media as CrunchyrollMedia<number>); + acc[date].push(media as CrunchyrollMedia<number>); - return acc; - }, {}); - const continuing: CrunchyrollMedia<number | string>[] = crunchyroll.filter( - (media) => media.day === 'continuing' - ); - const soon: CrunchyrollMedia<number | string>[] = crunchyroll.filter( - (media) => media.day === 'soon' - ); + return acc; + }, {}); +const continuing: CrunchyrollMedia<number | string>[] = crunchyroll.filter( + (media) => media.day === 'continuing' +); +const soon: CrunchyrollMedia<number | string>[] = crunchyroll.filter( + (media) => media.day === 'soon' +); - $: columnCount = Math.ceil(Object.keys(days).length / 2); +$: columnCount = Math.ceil(Object.keys(days).length / 2); - const ordinalSuffix = (i: number) => { - const j = i % 10; - const k = i % 100; +const ordinalSuffix = (i: number) => { + const j = i % 10; + const k = i % 100; - if (j === 1 && k !== 11) return i + 'st'; - if (j === 2 && k !== 12) return i + 'nd'; - if (j === 3 && k !== 13) return i + 'rd'; + if (j === 1 && k !== 11) return i + 'st'; + if (j === 2 && k !== 12) return i + 'nd'; + if (j === 3 && k !== 13) return i + 'rd'; - return i + 'th'; - }; + return i + 'th'; +}; </script> <div class="list-container" id="crunchyroll" style={`column-count: ${columnCount};`}> diff --git a/src/lib/Schedule/Days.svelte b/src/lib/Schedule/Days.svelte index 99955897..416c0cce 100644 --- a/src/lib/Schedule/Days.svelte +++ b/src/lib/Schedule/Days.svelte @@ -1,104 +1,102 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; - import { findClosestMedia } from '$lib/Media/Anime/Airing/Subtitled/match'; - import type { SubsPlease, SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; - import { outboundLink } from '$lib/Media/links'; - import { parseOrDefault } from '$lib/Utility/parameters'; - import settings from '$stores/settings'; - import CoverBypass from './CoverBypass.svelte'; - import '$lib/List/covers.css'; - import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; - import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; - import { onMount } from 'svelte'; - import identity from '$stores/identity'; - import anime from '$stores/anime'; - import lastPruneTimes from '$stores/lastPruneTimes'; - import Message from '$lib/Loading/Message.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Error from '$lib/Error/RateLimited.svelte'; - - export let subsPlease: SubsPlease; - export let scheduledMedia: Partial<Media[]>; - export let forceListMode = false; - export let user; - - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - let day: string | null = parseOrDefault(urlParameters, 'day', null); - - let mediaListPromise: Promise<Media[]>; - - onMount(async () => { - if (user === undefined || $identity.id === -2) mediaListPromise = Promise.resolve([]); - else - mediaListPromise = mediaListCollection( - user, - $identity, - Type.Anime, - $anime, - $lastPruneTimes.anime, - { - all: true - } - ); - }); - - const shiftSubsPleaseSchedule = (schedule: SubsPlease['schedule']) => { - const shiftedSchedule: { [key: string]: SubsPleaseEpisode[] } = {}; +import { browser } from '$app/environment'; +import { mediaListCollection, Type, type Media } from '$lib/Data/AniList/media'; +import { findClosestMedia } from '$lib/Media/Anime/Airing/Subtitled/match'; +import type { SubsPlease, SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; +import { outboundLink } from '$lib/Media/links'; +import { parseOrDefault } from '$lib/Utility/parameters'; +import settings from '$stores/settings'; +import CoverBypass from './CoverBypass.svelte'; +import '$lib/List/covers.css'; +import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; +import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; +import { onMount } from 'svelte'; +import identity from '$stores/identity'; +import anime from '$stores/anime'; +import lastPruneTimes from '$stores/lastPruneTimes'; +import Message from '$lib/Loading/Message.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import Error from '$lib/Error/RateLimited.svelte'; + +export let subsPlease: SubsPlease; +export let scheduledMedia: Partial<Media[]>; +export let forceListMode = false; +export let user; + +const urlParameters = browser ? new URLSearchParams(window.location.search) : null; +let day: string | null = parseOrDefault(urlParameters, 'day', null); + +let mediaListPromise: Promise<Media[]>; + +onMount(async () => { + if (user === undefined || $identity.id === -2) mediaListPromise = Promise.resolve([]); + else + mediaListPromise = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + all: true + } + ); +}); - if (day && Object.keys(schedule).includes(day)) { - shiftedSchedule[day] = schedule[ - day as keyof typeof schedule - ] as unknown as SubsPleaseEpisode[]; +const shiftSubsPleaseSchedule = (schedule: SubsPlease['schedule']) => { + const shiftedSchedule: { [key: string]: SubsPleaseEpisode[] } = {}; - return shiftedSchedule; - } + if (day && Object.keys(schedule).includes(day)) { + shiftedSchedule[day] = schedule[day as keyof typeof schedule] as unknown as SubsPleaseEpisode[]; - const days = Object.keys(schedule); - const currentDayIndex = days.indexOf(new Date().toLocaleString('en-us', { weekday: 'long' })); + return shiftedSchedule; + } - days - .slice(currentDayIndex) - .concat(days.slice(0, currentDayIndex)) - .forEach((day) => { - const scheduleEntry = schedule[day as keyof typeof schedule]; + const days = Object.keys(schedule); + const currentDayIndex = days.indexOf(new Date().toLocaleString('en-us', { weekday: 'long' })); - shiftedSchedule[day] = Array.isArray(scheduleEntry) - ? scheduleEntry - : ([scheduleEntry] as unknown as SubsPleaseEpisode[]); - }); + days + .slice(currentDayIndex) + .concat(days.slice(0, currentDayIndex)) + .forEach((day) => { + const scheduleEntry = schedule[day as keyof typeof schedule]; - Object.entries(shiftedSchedule).forEach(([day, scheduleEntry]) => { - if (scheduleEntry.length === 0) { - delete shiftedSchedule[day]; - } + shiftedSchedule[day] = Array.isArray(scheduleEntry) + ? scheduleEntry + : ([scheduleEntry] as unknown as SubsPleaseEpisode[]); }); - return shiftedSchedule; - }; + Object.entries(shiftedSchedule).forEach(([day, scheduleEntry]) => { + if (scheduleEntry.length === 0) { + delete shiftedSchedule[day]; + } + }); + + return shiftedSchedule; +}; - const associateMedia = (media: (Media | undefined)[], title: string, mediaList: Media[]) => { - const closestMedia = findClosestMedia(media as Media[], title); +const associateMedia = (media: (Media | undefined)[], title: string, mediaList: Media[]) => { + const closestMedia = findClosestMedia(media as Media[], title); - if ($settings.displayScheduleFilterList && closestMedia && mediaList) - return mediaList.find((m) => m.id === closestMedia?.id) || null; + if ($settings.displayScheduleFilterList && closestMedia && mediaList) + return mediaList.find((m) => m.id === closestMedia?.id) || null; - return closestMedia; - }; + return closestMedia; +}; - const episode = (media: Media, weekday: string) => { - if (media.nextAiringEpisode?.episode === 1) return 1; +const episode = (media: Media, weekday: string) => { + if (media.nextAiringEpisode?.episode === 1) return 1; - if ( - media.nextAiringEpisode?.airingAt && - weekday === new Date().toLocaleString('en-us', { weekday: 'long' }) && - new Date(media.nextAiringEpisode.airingAt * 1000).getTime() - new Date().getTime() > - 24 * 60 * 60 * 1000 - ) - return media.nextAiringEpisode?.episode - 1; + if ( + media.nextAiringEpisode?.airingAt && + weekday === new Date().toLocaleString('en-us', { weekday: 'long' }) && + new Date(media.nextAiringEpisode.airingAt * 1000).getTime() - new Date().getTime() > + 24 * 60 * 60 * 1000 + ) + return media.nextAiringEpisode?.episode - 1; - return media.nextAiringEpisode?.episode || 1; - }; + return media.nextAiringEpisode?.episode || 1; +}; </script> {#await mediaListPromise} diff --git a/src/lib/Settings/Categories/Attributions.svelte b/src/lib/Settings/Categories/Attributions.svelte index e0a77f1f..7d2939d1 100644 --- a/src/lib/Settings/Categories/Attributions.svelte +++ b/src/lib/Settings/Categories/Attributions.svelte @@ -1,6 +1,6 @@ <script> - import Spacer from '$lib/Layout/Spacer.svelte'; - import root from '$lib/Utility/root'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import root from '$lib/Utility/root'; </script> <ul> diff --git a/src/lib/Settings/Categories/Cache.svelte b/src/lib/Settings/Categories/Cache.svelte index 68783bdf..c400e6ae 100644 --- a/src/lib/Settings/Categories/Cache.svelte +++ b/src/lib/Settings/Categories/Cache.svelte @@ -1,6 +1,6 @@ <script> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings from '$stores/settings'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import settings from '$stores/settings'; </script> <small class="opaque"> diff --git a/src/lib/Settings/Categories/Calculation.svelte b/src/lib/Settings/Categories/Calculation.svelte index 99e42463..b8b27fc7 100644 --- a/src/lib/Settings/Categories/Calculation.svelte +++ b/src/lib/Settings/Categories/Calculation.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import { pruneAllManga } from '$lib/Media/Manga/cache'; - import locale from '$stores/locale'; - import settings from '$stores/settings'; - import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; - import SettingHint from '../SettingHint.svelte'; +import { pruneAllManga } from '$lib/Media/Manga/cache'; +import locale from '$stores/locale'; +import settings from '$stores/settings'; +import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; +import SettingHint from '../SettingHint.svelte'; </script> <SettingCheckboxToggle diff --git a/src/lib/Settings/Categories/Debug.svelte b/src/lib/Settings/Categories/Debug.svelte index 8bae4c84..6967f49f 100644 --- a/src/lib/Settings/Categories/Debug.svelte +++ b/src/lib/Settings/Categories/Debug.svelte @@ -1,25 +1,25 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings from '$stores/settings'; - import { addNotification } from '$lib/Notification/store'; - import SettingHint from '../SettingHint.svelte'; - import { options } from '$lib/Notification/options'; - import locale from '$stores/locale'; - import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; - import localforage from 'localforage'; - import { browser } from '$app/environment'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import settings from '$stores/settings'; +import { addNotification } from '$lib/Notification/store'; +import SettingHint from '../SettingHint.svelte'; +import { options } from '$lib/Notification/options'; +import locale from '$stores/locale'; +import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; +import localforage from 'localforage'; +import { browser } from '$app/environment'; - const clearCaches = async () => { - if (!browser) return; +const clearCaches = async () => { + if (!browser) return; - await localforage.removeItem('anime'); - await localforage.removeItem('manga'); - addNotification( - options({ - heading: 'Anime and manga list caches successfully cleared' - }) - ); - }; + await localforage.removeItem('anime'); + await localforage.removeItem('manga'); + addNotification( + options({ + heading: 'Anime and manga list caches successfully cleared' + }) + ); +}; </script> <SettingCheckboxToggle setting="debugDummyLists" text={$locale().debug.dummyLists} /> diff --git a/src/lib/Settings/Categories/Display.svelte b/src/lib/Settings/Categories/Display.svelte index a5f16f97..783dd075 100644 --- a/src/lib/Settings/Categories/Display.svelte +++ b/src/lib/Settings/Categories/Display.svelte @@ -1,70 +1,70 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings from '$stores/settings'; - import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; - import SettingHint from '../SettingHint.svelte'; - import root from '$lib/Utility/root'; - import locale from '$stores/locale'; - import { requestNotifications } from '$lib/Utility/notifications'; - import { getFingerprint } from '$lib/Utility/fingerprint'; - - const onHelperChange = () => { - const mai = document.getElementById('mai') as HTMLImageElement; - - if (!mai) return; - - mai.style.display = 'block'; - - switch ($settings.displayAoButa) { - case 'random': - case 'mai_2': - { - mai.src = '/aobuta/mai.png'; - } - break; - case 'mai': - { - mai.src = '/aobuta/mai_2.webp'; - } - break; - case 'nodoka': - { - mai.src = '/aobuta/nodoka.webp'; - } - break; - case 'kaede': - { - mai.src = '/aobuta/kaede.png'; - } - break; - case 'rio': - { - mai.src = '/aobuta/rio.webp'; - } - break; - case 'sakuta': - { - mai.src = '/aobuta/sakuta.webp'; - } - break; - case 'shouko': - { - mai.src = '/aobuta/shouko.webp'; - } - break; - case 'tomoe': - { - mai.src = '/aobuta/tomoe.webp'; - } - break; - case 'none': { - { - mai.style.display = 'none'; - } - break; +import Spacer from '$lib/Layout/Spacer.svelte'; +import settings from '$stores/settings'; +import SettingCheckboxToggle from '../SettingCheckboxToggle.svelte'; +import SettingHint from '../SettingHint.svelte'; +import root from '$lib/Utility/root'; +import locale from '$stores/locale'; +import { requestNotifications } from '$lib/Utility/notifications'; +import { getFingerprint } from '$lib/Utility/fingerprint'; + +const onHelperChange = () => { + const mai = document.getElementById('mai') as HTMLImageElement; + + if (!mai) return; + + mai.style.display = 'block'; + + switch ($settings.displayAoButa) { + case 'random': + case 'mai_2': + { + mai.src = '/aobuta/mai.png'; } + break; + case 'mai': + { + mai.src = '/aobuta/mai_2.webp'; + } + break; + case 'nodoka': + { + mai.src = '/aobuta/nodoka.webp'; + } + break; + case 'kaede': + { + mai.src = '/aobuta/kaede.png'; + } + break; + case 'rio': + { + mai.src = '/aobuta/rio.webp'; + } + break; + case 'sakuta': + { + mai.src = '/aobuta/sakuta.webp'; + } + break; + case 'shouko': + { + mai.src = '/aobuta/shouko.webp'; + } + break; + case 'tomoe': + { + mai.src = '/aobuta/tomoe.webp'; + } + break; + case 'none': { + { + mai.style.display = 'none'; + } + break; } - }; + } +}; </script> <b>{$locale().settings.display.categories.includeAdditionalMedia}</b><br /> diff --git a/src/lib/Settings/Categories/RSSFeeds.svelte b/src/lib/Settings/Categories/RSSFeeds.svelte index 51554884..452a068b 100644 --- a/src/lib/Settings/Categories/RSSFeeds.svelte +++ b/src/lib/Settings/Categories/RSSFeeds.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import { options } from '$lib/Notification/options'; - import { addNotification } from '$lib/Notification/store'; - import { env } from '$env/dynamic/public'; - import locale from '$stores/locale'; - import SettingHint from '../SettingHint.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; +import { options } from '$lib/Notification/options'; +import { addNotification } from '$lib/Notification/store'; +import { env } from '$env/dynamic/public'; +import locale from '$stores/locale'; +import SettingHint from '../SettingHint.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; - export let user: { accessToken: string; refreshToken: string }; +export let user: { accessToken: string; refreshToken: string }; </script> <button diff --git a/src/lib/Settings/Categories/SettingSync.svelte b/src/lib/Settings/Categories/SettingSync.svelte index d69eab09..39ba9d87 100644 --- a/src/lib/Settings/Categories/SettingSync.svelte +++ b/src/lib/Settings/Categories/SettingSync.svelte @@ -1,13 +1,13 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { options } from '$lib/Notification/options'; - import root from '$lib/Utility/root'; - import identity from '$stores/identity'; - import settings from '$stores/settings'; - import { addNotification } from '$lib/Notification/store'; - import SettingHint from '../SettingHint.svelte'; - import locale from '$stores/locale'; - import settingsSyncTimes from '$stores/settingsSyncTimes'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { options } from '$lib/Notification/options'; +import root from '$lib/Utility/root'; +import identity from '$stores/identity'; +import settings from '$stores/settings'; +import { addNotification } from '$lib/Notification/store'; +import SettingHint from '../SettingHint.svelte'; +import locale from '$stores/locale'; +import settingsSyncTimes from '$stores/settingsSyncTimes'; </script> {#if !$settings.settingsSync} diff --git a/src/lib/Settings/Category.svelte b/src/lib/Settings/Category.svelte index f4936e8a..58930cbb 100644 --- a/src/lib/Settings/Category.svelte +++ b/src/lib/Settings/Category.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - export let title = ''; - export let id = title.toLowerCase(); - export let open = true; - export let newLine = true; +import Spacer from '$lib/Layout/Spacer.svelte'; +export let title = ''; +export let id = title.toLowerCase(); +export let open = true; +export let newLine = true; </script> <details {open} {id}> diff --git a/src/lib/Settings/SettingCheckboxToggle.svelte b/src/lib/Settings/SettingCheckboxToggle.svelte index c9d82907..b5ef4989 100644 --- a/src/lib/Settings/SettingCheckboxToggle.svelte +++ b/src/lib/Settings/SettingCheckboxToggle.svelte @@ -1,57 +1,57 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings, { type Settings } from '$stores/settings'; - - type BooleanSettingsKeys<T> = { - [K in keyof T]: T[K] extends boolean ? K : never; - }; - type SettingsBooleanKeys = BooleanSettingsKeys<Settings>; - - export let sectionBreak = false; - export let disabled = false; - export let text: string | (() => string); - export let setting: SettingsBooleanKeys[keyof SettingsBooleanKeys]; - export let lineBreak = true; - export let onChange: () => void = () => { - return; - }; - export let invert = false; - export let id: string | null = null; - - $: checked = setting ? (invert ? !$settings[setting] : $settings[setting]) : false; - $: field = text instanceof Function ? text() : text; - - // const toggler = (key: keyof Settings) => [ - // () => - // settings.update((s) => { - // (s[key] as boolean) = true; - - // $settings = s; - - // return s; - // }), - // () => - // settings.update((s) => { - // (s[key] as boolean) = false; - - // $settings = s; - - // return s; - // }) - // ]; - - const check = (e: Event & { currentTarget: EventTarget & HTMLInputElement }): void => { - const checked = (e.target as HTMLInputElement).checked; - - if (setting) { - settings.setKey(setting, invert ? !checked : checked); - onChange(); - } - }; - - const flip = () => { - if (setting) $settings[setting] = !$settings[setting]; - }; +import Spacer from '$lib/Layout/Spacer.svelte'; +import settings, { type Settings } from '$stores/settings'; + +type BooleanSettingsKeys<T> = { + [K in keyof T]: T[K] extends boolean ? K : never; +}; +type SettingsBooleanKeys = BooleanSettingsKeys<Settings>; + +export let sectionBreak = false; +export let disabled = false; +export let text: string | (() => string); +export let setting: SettingsBooleanKeys[keyof SettingsBooleanKeys]; +export let lineBreak = true; +export let onChange: () => void = () => { + return; +}; +export let invert = false; +export let id: string | null = null; + +$: checked = setting ? (invert ? !$settings[setting] : $settings[setting]) : false; +$: field = text instanceof Function ? text() : text; + +// const toggler = (key: keyof Settings) => [ +// () => +// settings.update((s) => { +// (s[key] as boolean) = true; + +// $settings = s; + +// return s; +// }), +// () => +// settings.update((s) => { +// (s[key] as boolean) = false; + +// $settings = s; + +// return s; +// }) +// ]; + +const check = (e: Event & { currentTarget: EventTarget & HTMLInputElement }): void => { + const checked = (e.target as HTMLInputElement).checked; + + if (setting) { + settings.setKey(setting, invert ? !checked : checked); + onChange(); + } +}; + +const flip = () => { + if (setting) $settings[setting] = !$settings[setting]; +}; </script> <input type="checkbox" onchange={check} bind:checked {id} /> diff --git a/src/lib/Settings/SettingHint.svelte b/src/lib/Settings/SettingHint.svelte index f82f061c..bac70366 100644 --- a/src/lib/Settings/SettingHint.svelte +++ b/src/lib/Settings/SettingHint.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - export let lineBreak = false; +export let lineBreak = false; </script> {#if lineBreak} diff --git a/src/lib/Settings/SettingToggle.svelte b/src/lib/Settings/SettingToggle.svelte index d7e31322..d389e72f 100644 --- a/src/lib/Settings/SettingToggle.svelte +++ b/src/lib/Settings/SettingToggle.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings, { type Settings } from '$stores/settings'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import settings, { type Settings } from '$stores/settings'; - export let setting: keyof Settings; - export let on = ''; - export let off = ''; - export let sectionBreak = false; - export let disabled = false; +export let setting: keyof Settings; +export let on = ''; +export let off = ''; +export let sectionBreak = false; +export let disabled = false; </script> <a diff --git a/src/lib/Settings/Verbiage.svelte b/src/lib/Settings/Verbiage.svelte index 76193c3b..0baf2768 100644 --- a/src/lib/Settings/Verbiage.svelte +++ b/src/lib/Settings/Verbiage.svelte @@ -1,5 +1,5 @@ <script> - import root from '$lib/Utility/root'; +import root from '$lib/Utility/root'; </script> <details open={false}> diff --git a/src/lib/Tools/ActivityHistory/Grid.svelte b/src/lib/Tools/ActivityHistory/Grid.svelte index b693f912..84c182fa 100644 --- a/src/lib/Tools/ActivityHistory/Grid.svelte +++ b/src/lib/Tools/ActivityHistory/Grid.svelte @@ -1,35 +1,35 @@ <script lang="ts"> - import { - fillMissingDays, - type ActivityHistoryEntry, - activityHistory - } from '$lib/Data/AniList/activity'; - import { onMount } from 'svelte'; - import userIdentity from '$stores/identity'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { clearAllParameters } from '../../Utility/parameters'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; +import { + fillMissingDays, + type ActivityHistoryEntry, + activityHistory +} from '$lib/Data/AniList/activity'; +import { onMount } from 'svelte'; +import userIdentity from '$stores/identity'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import { clearAllParameters } from '../../Utility/parameters'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - export let user: AniListAuthorisation; - export let activityData: ActivityHistoryEntry[] | null = null; - export let currentYear = new Date().getFullYear(); +export let user: AniListAuthorisation; +export let activityData: ActivityHistoryEntry[] | null = null; +export let currentYear = new Date().getFullYear(); - let activityHistoryData: ActivityHistoryEntry[]; - let baseHue = Math.floor(Math.random() * 360); +let activityHistoryData: ActivityHistoryEntry[]; +let baseHue = Math.floor(Math.random() * 360); - onMount(async () => { - clearAllParameters(); +onMount(async () => { + clearAllParameters(); - activityHistoryData = activityData || (await activityHistory($userIdentity)); - }); + activityHistoryData = activityData || (await activityHistory($userIdentity)); +}); - const gradientColour = (amount: number, maxAmount: number, baseHue: number) => { - const lightness = 100 - Math.round((amount / maxAmount) * 50); +const gradientColour = (amount: number, maxAmount: number, baseHue: number) => { + const lightness = 100 - Math.round((amount / maxAmount) * 50); - return `hsl(${baseHue}, 100%, ${lightness}%)`; - }; + return `hsl(${baseHue}, 100%, ${lightness}%)`; +}; </script> {#if user === undefined} diff --git a/src/lib/Tools/ActivityHistory/Tool.svelte b/src/lib/Tools/ActivityHistory/Tool.svelte index 6f6282a6..06924b22 100644 --- a/src/lib/Tools/ActivityHistory/Tool.svelte +++ b/src/lib/Tools/ActivityHistory/Tool.svelte @@ -1,69 +1,69 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { - activityHistory, - fillMissingDays, - type ActivityHistoryEntry - } from '$lib/Data/AniList/activity'; - import { onMount } from 'svelte'; - import userIdentity from '$stores/identity'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { clearAllParameters } from '../../Utility/parameters'; - import { domToBlob } from 'modern-screenshot'; - import ActivityHistoryGrid from './Grid.svelte'; - import SettingHint from '$lib/Settings/SettingHint.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - - export let user: AniListAuthorisation; - - let activityHistoryData: Promise<ActivityHistoryEntry[]>; - let generated = false; - - onMount(async () => { - clearAllParameters(); - - if (user !== undefined) activityHistoryData = activityHistory($userIdentity); - }); - - // const incrementDate = (date: Date): Date => { - // date.setDate(date.getDate() + 1); - - // return date; - // }; - - const screenshot = async () => { - let element = document.querySelector('.grid') as HTMLElement; - - if (element !== null) { - domToBlob(element, { - quality: 1, - scale: 2 - }).then((blob) => { - const downloadWrapper = document.createElement('a'); - const image = document.createElement('img'); - const object = (window.URL || window.webkitURL || window || {}).createObjectURL(blob); - - downloadWrapper.href = object; - downloadWrapper.target = '_blank'; - image.src = object; - - downloadWrapper.appendChild(image); - - const gridFinal = document.getElementById('grid-final'); - - if (gridFinal !== null) { - gridFinal.innerHTML = ''; - - gridFinal.appendChild(downloadWrapper); - - generated = true; - } - - downloadWrapper.click(); - }); - } - }; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { + activityHistory, + fillMissingDays, + type ActivityHistoryEntry +} from '$lib/Data/AniList/activity'; +import { onMount } from 'svelte'; +import userIdentity from '$stores/identity'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import { clearAllParameters } from '../../Utility/parameters'; +import { domToBlob } from 'modern-screenshot'; +import ActivityHistoryGrid from './Grid.svelte'; +import SettingHint from '$lib/Settings/SettingHint.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; + +export let user: AniListAuthorisation; + +let activityHistoryData: Promise<ActivityHistoryEntry[]>; +let generated = false; + +onMount(async () => { + clearAllParameters(); + + if (user !== undefined) activityHistoryData = activityHistory($userIdentity); +}); + +// const incrementDate = (date: Date): Date => { +// date.setDate(date.getDate() + 1); + +// return date; +// }; + +const screenshot = async () => { + let element = document.querySelector('.grid') as HTMLElement; + + if (element !== null) { + domToBlob(element, { + quality: 1, + scale: 2 + }).then((blob) => { + const downloadWrapper = document.createElement('a'); + const image = document.createElement('img'); + const object = (window.URL || window.webkitURL || window || {}).createObjectURL(blob); + + downloadWrapper.href = object; + downloadWrapper.target = '_blank'; + image.src = object; + + downloadWrapper.appendChild(image); + + const gridFinal = document.getElementById('grid-final'); + + if (gridFinal !== null) { + gridFinal.innerHTML = ''; + + gridFinal.appendChild(downloadWrapper); + + generated = true; + } + + downloadWrapper.click(); + }); + } +}; </script> {#if user === undefined} diff --git a/src/lib/Tools/Birthdays.svelte b/src/lib/Tools/Birthdays.svelte index e6654833..6c81a233 100644 --- a/src/lib/Tools/Birthdays.svelte +++ b/src/lib/Tools/Birthdays.svelte @@ -1,111 +1,111 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { page } from '$app/stores'; - import { ACDBBirthdays, type ACDBBirthday } from '$lib/Data/Birthday/secondary'; - import { aniSearchBirthdays, type aniSearchBirthday } from '$lib/Data/Birthday/primary'; - import Error from '$lib/Error/RateLimited.svelte'; - import { onMount } from 'svelte'; - import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - - interface Birthday { - name: string; - image: string; - origin?: string; +import { browser } from '$app/environment'; +import { page } from '$app/stores'; +import { ACDBBirthdays, type ACDBBirthday } from '$lib/Data/Birthday/secondary'; +import { aniSearchBirthdays, type aniSearchBirthday } from '$lib/Data/Birthday/primary'; +import Error from '$lib/Error/RateLimited.svelte'; +import { onMount } from 'svelte'; +import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import Message from '$lib/Loading/Message.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; + +interface Birthday { + name: string; + image: string; + origin?: string; +} + +const urlParameters = browser ? new URLSearchParams(window.location.search) : null; +let date = new Date(); +let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); +let day = parseOrDefault(urlParameters, 'day', date.getDate()); +let birthdays: Promise<{ birthdays: Birthday[]; allSourcesFailed: boolean }>; + +$: { + month = Math.min(month, 12); + month = Math.max(month, 1); + day = Math.min(day, new Date(2024, month, 0).getDate()); + day = Math.max(day, 1); + + birthdays = resolveBirthdays(month, day); + + if (browser) { + $page.url.searchParams.set('month', month.toString()); + $page.url.searchParams.set('day', day.toString()); + clearAllParameters(['month', 'day']); + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } +} - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - let date = new Date(); - let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); - let day = parseOrDefault(urlParameters, 'day', date.getDate()); - let birthdays: Promise<{ birthdays: Birthday[]; allSourcesFailed: boolean }>; - - $: { - month = Math.min(month, 12); - month = Math.max(month, 1); - day = Math.min(day, new Date(2024, month, 0).getDate()); - day = Math.max(day, 1); - - birthdays = resolveBirthdays(month, day); - - if (browser) { - $page.url.searchParams.set('month', month.toString()); - $page.url.searchParams.set('day', day.toString()); - clearAllParameters(['month', 'day']); - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } - } +onMount(() => clearAllParameters(['month', 'day'])); - onMount(() => clearAllParameters(['month', 'day'])); +const normaliseName = (name: string): string => name.toLowerCase().split(' ').sort().join(' '); - const normaliseName = (name: string): string => name.toLowerCase().split(' ').sort().join(' '); +const fixName = (name: string): string => { + const split = name.split(' '); + const last = split[split.length - 1]; - const fixName = (name: string): string => { - const split = name.split(' '); - const last = split[split.length - 1]; + if (last === last.toUpperCase()) { + split[split.length - 1] = last[0] + last.slice(1).toLowerCase(); - if (last === last.toUpperCase()) { - split[split.length - 1] = last[0] + last.slice(1).toLowerCase(); + return split.join(' '); + } - return split.join(' '); - } + const bracketIndex = name.indexOf('['); - const bracketIndex = name.indexOf('['); + if (bracketIndex !== -1) return name.slice(0, bracketIndex).trim(); - if (bracketIndex !== -1) return name.slice(0, bracketIndex).trim(); + return name; +}; - return name; - }; +const combineBirthdaySources = ( + acdb: ACDBBirthday[], + aniSearch: aniSearchBirthday[] +): Birthday[] => { + const nameMap = new Map<string, Birthday>(); - const combineBirthdaySources = ( - acdb: ACDBBirthday[], - aniSearch: aniSearchBirthday[] - ): Birthday[] => { - const nameMap = new Map<string, Birthday>(); - - for (const entry of aniSearch.map((entry) => ({ - ...entry, - normalisedName: normaliseName(fixName(entry.name)) - }))) { - if (!nameMap.has(entry.normalisedName)) - nameMap.set(entry.normalisedName, { - name: fixName(entry.name), - image: entry.image - }); - } - - for (const entry of acdb) { - const normalisedName = normaliseName(fixName(entry.name)); + for (const entry of aniSearch.map((entry) => ({ + ...entry, + normalisedName: normaliseName(fixName(entry.name)) + }))) { + if (!nameMap.has(entry.normalisedName)) + nameMap.set(entry.normalisedName, { + name: fixName(entry.name), + image: entry.image + }); + } - if (!nameMap.has(normalisedName)) - nameMap.set(normalisedName, { - name: entry.name, - image: entry.character_image, - origin: entry.origin - }); - } + for (const entry of acdb) { + const normalisedName = normaliseName(fixName(entry.name)); - return Array.from(nameMap.values()); - }; + if (!nameMap.has(normalisedName)) + nameMap.set(normalisedName, { + name: entry.name, + image: entry.character_image, + origin: entry.origin + }); + } - const resolveBirthdays = async ( - month: number, - day: number - ): Promise<{ birthdays: Birthday[]; allSourcesFailed: boolean }> => { - const [acdbResult, aniSearchResult] = await Promise.allSettled([ - ACDBBirthdays(month, day), - browser ? aniSearchBirthdays(month, day) : Promise.resolve([]) - ]); - const acdb = acdbResult.status === 'fulfilled' ? acdbResult.value : []; - const aniSearch = aniSearchResult.status === 'fulfilled' ? aniSearchResult.value : []; - - return { - birthdays: combineBirthdaySources(acdb, aniSearch), - allSourcesFailed: acdbResult.status === 'rejected' && aniSearchResult.status === 'rejected' - }; + return Array.from(nameMap.values()); +}; + +const resolveBirthdays = async ( + month: number, + day: number +): Promise<{ birthdays: Birthday[]; allSourcesFailed: boolean }> => { + const [acdbResult, aniSearchResult] = await Promise.allSettled([ + ACDBBirthdays(month, day), + browser ? aniSearchBirthdays(month, day) : Promise.resolve([]) + ]); + const acdb = acdbResult.status === 'fulfilled' ? acdbResult.value : []; + const aniSearch = aniSearchResult.status === 'fulfilled' ? aniSearchResult.value : []; + + return { + birthdays: combineBirthdaySources(acdb, aniSearch), + allSourcesFailed: acdbResult.status === 'rejected' && aniSearchResult.status === 'rejected' }; +}; </script> {#await birthdays} diff --git a/src/lib/Tools/BirthdaysTemplate.svelte b/src/lib/Tools/BirthdaysTemplate.svelte index 5f476275..0ec937c4 100644 --- a/src/lib/Tools/BirthdaysTemplate.svelte +++ b/src/lib/Tools/BirthdaysTemplate.svelte @@ -1,36 +1,36 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { page } from '$app/stores'; - import { onMount } from 'svelte'; - import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; - import Message from '$lib/Loading/Message.svelte'; - import locale from '$stores/locale'; - import Error from '$lib/Error/RateLimited.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; +import { browser } from '$app/environment'; +import { page } from '$app/stores'; +import { onMount } from 'svelte'; +import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; +import Message from '$lib/Loading/Message.svelte'; +import locale from '$stores/locale'; +import Error from '$lib/Error/RateLimited.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; - export let remoteURL: string; +export let remoteURL: string; - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - let date = new Date(); - let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); - let day = parseOrDefault(urlParameters, 'day', date.getDate()); - const remoteBirthdays = fetch(remoteURL); +const urlParameters = browser ? new URLSearchParams(window.location.search) : null; +let date = new Date(); +let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); +let day = parseOrDefault(urlParameters, 'day', date.getDate()); +const remoteBirthdays = fetch(remoteURL); - $: { - month = Math.min(month, 12); - month = Math.max(month, 1); - day = Math.min(day, new Date(2024, month, 0).getDate()); - day = Math.max(day, 1); +$: { + month = Math.min(month, 12); + month = Math.max(month, 1); + day = Math.min(day, new Date(2024, month, 0).getDate()); + day = Math.max(day, 1); - if (browser) { - $page.url.searchParams.set('month', month.toString()); - $page.url.searchParams.set('day', day.toString()); - clearAllParameters(['month', 'day']); - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } + if (browser) { + $page.url.searchParams.set('month', month.toString()); + $page.url.searchParams.set('day', day.toString()); + clearAllParameters(['month', 'day']); + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } +} - onMount(() => clearAllParameters(['month', 'day'])); +onMount(() => clearAllParameters(['month', 'day'])); </script> {#await remoteBirthdays} diff --git a/src/lib/Tools/DumpProfile.svelte b/src/lib/Tools/DumpProfile.svelte index 717814d2..a111028c 100644 --- a/src/lib/Tools/DumpProfile.svelte +++ b/src/lib/Tools/DumpProfile.svelte @@ -1,30 +1,30 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { dumpUser } from '$lib/Data/AniList/user'; - import RateLimited from '$lib/Error/RateLimited.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import InputTemplate from './InputTemplate.svelte'; - import LZString from 'lz-string'; - - let submission = ''; - - // Credit: @hoh - const decodeJSON = (about: string): JSON | null => { - const match = (about || '').match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/); - - if (match) +import Spacer from '$lib/Layout/Spacer.svelte'; +import { dumpUser } from '$lib/Data/AniList/user'; +import RateLimited from '$lib/Error/RateLimited.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import InputTemplate from './InputTemplate.svelte'; +import LZString from 'lz-string'; + +let submission = ''; + +// Credit: @hoh +const decodeJSON = (about: string): JSON | null => { + const match = (about || '').match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/); + + if (match) + try { + return JSON.parse(atob(match[1])); + } catch { try { - return JSON.parse(atob(match[1])); + return JSON.parse(LZString.decompressFromBase64(match[1])); } catch { - try { - return JSON.parse(LZString.decompressFromBase64(match[1])); - } catch { - return null; - } + return null; } + } - return null; - }; + return null; +}; </script> <InputTemplate field="Username" bind:submission event="Dump User" submitText="Dump"> diff --git a/src/lib/Tools/EpisodeDiscussionCollector.svelte b/src/lib/Tools/EpisodeDiscussionCollector.svelte index 71e13add..99c53f20 100644 --- a/src/lib/Tools/EpisodeDiscussionCollector.svelte +++ b/src/lib/Tools/EpisodeDiscussionCollector.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { threads } from '$lib/Data/AniList/forum'; - import { onMount } from 'svelte'; - import { clearAllParameters } from '../Utility/parameters'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import InputTemplate from './InputTemplate.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { threads } from '$lib/Data/AniList/forum'; +import { onMount } from 'svelte'; +import { clearAllParameters } from '../Utility/parameters'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import InputTemplate from './InputTemplate.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; - let submission = ''; +let submission = ''; - onMount(clearAllParameters); +onMount(clearAllParameters); </script> <InputTemplate diff --git a/src/lib/Tools/FollowFix.svelte b/src/lib/Tools/FollowFix.svelte index 62548379..a0e9902a 100644 --- a/src/lib/Tools/FollowFix.svelte +++ b/src/lib/Tools/FollowFix.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import { toggleFollow } from '$lib/Data/AniList/follow'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; +import { toggleFollow } from '$lib/Data/AniList/follow'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - export let user: AniListAuthorisation; +export let user: AniListAuthorisation; - let input = ''; - let submit = ''; +let input = ''; +let submit = ''; </script> {#if user === undefined} diff --git a/src/lib/Tools/Hayai.svelte b/src/lib/Tools/Hayai.svelte index 1ba99d86..aa1112da 100644 --- a/src/lib/Tools/Hayai.svelte +++ b/src/lib/Tools/Hayai.svelte @@ -1,83 +1,83 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { onMount } from 'svelte'; - import JSZip from 'jszip'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { onMount } from 'svelte'; +import JSZip from 'jszip'; - let fileInput: HTMLInputElement | null = null; +let fileInput: HTMLInputElement | null = null; - const handleFileUpload = async () => { - if (!fileInput || !fileInput.files || fileInput.files.length === 0) return; +const handleFileUpload = async () => { + if (!fileInput || !fileInput.files || fileInput.files.length === 0) return; - const file = fileInput.files[0]; - const reader = new FileReader(); + const file = fileInput.files[0]; + const reader = new FileReader(); - reader.onload = async (event) => { - const zip = await JSZip.loadAsync((event.target as FileReader).result as ArrayBuffer); - const newZip = new JSZip(); + reader.onload = async (event) => { + const zip = await JSZip.loadAsync((event.target as FileReader).result as ArrayBuffer); + const newZip = new JSZip(); - for (const relativePath in zip.files) { - const zipEntry = zip.files[relativePath]; + for (const relativePath in zip.files) { + const zipEntry = zip.files[relativePath]; - if (zipEntry.dir) { - newZip.folder(relativePath); - } else if (relativePath.endsWith('.xhtml') || relativePath.endsWith('.html')) { - newZip.file(relativePath, applyBionicReadingToHTML(await zipEntry.async('text'))); - } else { - newZip.file(relativePath, await zipEntry.async('arraybuffer')); - } + if (zipEntry.dir) { + newZip.folder(relativePath); + } else if (relativePath.endsWith('.xhtml') || relativePath.endsWith('.html')) { + newZip.file(relativePath, applyBionicReadingToHTML(await zipEntry.async('text'))); + } else { + newZip.file(relativePath, await zipEntry.async('arraybuffer')); } + } - downloadEPUB( - await newZip.generateAsync({ type: 'blob' }), - `${file.name.split('.epub')[0]}_hayai.epub` - ); - }; - - reader.readAsArrayBuffer(file); + downloadEPUB( + await newZip.generateAsync({ type: 'blob' }), + `${file.name.split('.epub')[0]}_hayai.epub` + ); }; - const applyBionicReadingToHTML = (content: string) => { - const contentParser = new DOMParser().parseFromString(content, 'text/html'); + reader.readAsArrayBuffer(file); +}; - for (const paragraph of contentParser.getElementsByTagName('p')) - paragraph.innerHTML = applyBionicReadingToString(paragraph.textContent ?? ''); +const applyBionicReadingToHTML = (content: string) => { + const contentParser = new DOMParser().parseFromString(content, 'text/html'); - return contentParser.documentElement.outerHTML; - }; + for (const paragraph of contentParser.getElementsByTagName('p')) + paragraph.innerHTML = applyBionicReadingToString(paragraph.textContent ?? ''); - const applyBionicReadingToString = (text: string) => - text - .split(/\s+/) - .map((word) => { - if (/^\W+$/.test(word) || word.length <= 2) return word; + return contentParser.documentElement.outerHTML; +}; - let boldLength; +const applyBionicReadingToString = (text: string) => + text + .split(/\s+/) + .map((word) => { + if (/^\W+$/.test(word) || word.length <= 2) return word; - if (word.length <= 4) { - boldLength = 2; - } else if (word.length <= 7) { - boldLength = 3; - } else if (word.length <= 10) { - boldLength = 4; - } else { - boldLength = Math.ceil(word.length * 0.5); - } + let boldLength; - return `<strong>${word.slice(0, boldLength)}</strong>${word.slice(boldLength)}`; - }) - .join(' '); + if (word.length <= 4) { + boldLength = 2; + } else if (word.length <= 7) { + boldLength = 3; + } else if (word.length <= 10) { + boldLength = 4; + } else { + boldLength = Math.ceil(word.length * 0.5); + } - const downloadEPUB = (blob: Blob | MediaSource, fileName: string) => { - const link = document.createElement('a'); + return `<strong>${word.slice(0, boldLength)}</strong>${word.slice(boldLength)}`; + }) + .join(' '); - link.href = URL.createObjectURL(blob); - link.download = fileName; +const downloadEPUB = (blob: Blob | MediaSource, fileName: string) => { + const link = document.createElement('a'); - link.click(); - URL.revokeObjectURL(link.href); - }; + link.href = URL.createObjectURL(blob); + link.download = fileName; + + link.click(); + URL.revokeObjectURL(link.href); +}; - onMount(() => (fileInput = document.getElementById('epub-file') as HTMLInputElement)); +onMount(() => (fileInput = document.getElementById('epub-file') as HTMLInputElement)); </script> <div class="card"> diff --git a/src/lib/Tools/InputTemplate.svelte b/src/lib/Tools/InputTemplate.svelte index 48913646..af77b296 100644 --- a/src/lib/Tools/InputTemplate.svelte +++ b/src/lib/Tools/InputTemplate.svelte @@ -1,26 +1,26 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { clearAllParameters } from '$lib/Utility/parameters'; - import { onMount } from 'svelte'; - import SettingHint from '$lib/Settings/SettingHint.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { clearAllParameters } from '$lib/Utility/parameters'; +import { onMount } from 'svelte'; +import SettingHint from '$lib/Settings/SettingHint.svelte'; - export let field: string; - export let submission: string; - export let event: string | undefined = undefined; - export let submitText: string; - export let saveParameters: string[] = []; - export let onSubmit = () => { - return; - }; - export let preserveCase = false; - export let prompt = `Enter a ${ - preserveCase ? field : field.toLowerCase() - } to search for to continue.`; - export let hint: string | undefined = undefined; +export let field: string; +export let submission: string; +export let event: string | undefined = undefined; +export let submitText: string; +export let saveParameters: string[] = []; +export let onSubmit = () => { + return; +}; +export let preserveCase = false; +export let prompt = `Enter a ${ + preserveCase ? field : field.toLowerCase() +} to search for to continue.`; +export let hint: string | undefined = undefined; - let input = ''; +let input = ''; - onMount(() => clearAllParameters(saveParameters)); +onMount(() => clearAllParameters(saveParameters)); </script> <div class="card"> diff --git a/src/lib/Tools/Likes.svelte b/src/lib/Tools/Likes.svelte index 46d1edaf..d8545c39 100644 --- a/src/lib/Tools/Likes.svelte +++ b/src/lib/Tools/Likes.svelte @@ -1,22 +1,22 @@ <script lang="ts"> - import { activityLikes } from '$lib/Data/AniList/activity'; - import { threadLikes } from '$lib/Data/AniList/forum'; - import RateLimited from '$lib/Error/RateLimited.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import settings from '$stores/settings'; - import InputTemplate from './InputTemplate.svelte'; +import { activityLikes } from '$lib/Data/AniList/activity'; +import { threadLikes } from '$lib/Data/AniList/forum'; +import RateLimited from '$lib/Error/RateLimited.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; +import settings from '$stores/settings'; +import InputTemplate from './InputTemplate.svelte'; - let submission = ''; +let submission = ''; - $: normalisedSubmission = submission.replace(/.*\/(activity|thread)\/(\d+).*/, '$2'); - $: submissionType = submission.replace(/.*\/(activity|thread)\/(\d+).*/, '$1'); - $: likesPromise = - submissionType === 'activity' - ? activityLikes(Number(normalisedSubmission)) - : submissionType === 'thread' - ? threadLikes(Number(normalisedSubmission)) - : Promise.resolve(null); +$: normalisedSubmission = submission.replace(/.*\/(activity|thread)\/(\d+).*/, '$2'); +$: submissionType = submission.replace(/.*\/(activity|thread)\/(\d+).*/, '$1'); +$: likesPromise = + submissionType === 'activity' + ? activityLikes(Number(normalisedSubmission)) + : submissionType === 'thread' + ? threadLikes(Number(normalisedSubmission)) + : Promise.resolve(null); </script> <InputTemplate diff --git a/src/lib/Tools/Picker.svelte b/src/lib/Tools/Picker.svelte index dab74599..df11eef5 100644 --- a/src/lib/Tools/Picker.svelte +++ b/src/lib/Tools/Picker.svelte @@ -1,10 +1,10 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { goto } from '$app/navigation'; - import root from '$lib/Utility/root'; - import { tools } from './tools'; +import { browser } from '$app/environment'; +import { goto } from '$app/navigation'; +import root from '$lib/Utility/root'; +import { tools } from './tools'; - export let tool: string; +export let tool: string; </script> <blockquote> diff --git a/src/lib/Tools/RandomFollower.svelte b/src/lib/Tools/RandomFollower.svelte index 34a9b48e..628f1b35 100644 --- a/src/lib/Tools/RandomFollower.svelte +++ b/src/lib/Tools/RandomFollower.svelte @@ -1,13 +1,13 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { followers } from '$lib/Data/AniList/following'; - import RateLimited from '$lib/Error/RateLimited.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import TextSwap from '$lib/Layout/TextTransition.svelte'; - import InputTemplate from './InputTemplate.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { followers } from '$lib/Data/AniList/following'; +import RateLimited from '$lib/Error/RateLimited.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import TextSwap from '$lib/Layout/TextTransition.svelte'; +import InputTemplate from './InputTemplate.svelte'; - let submission = ''; - let randomSeed = 0; +let submission = ''; +let randomSeed = 0; </script> <InputTemplate diff --git a/src/lib/Tools/SequelCatcher/List.svelte b/src/lib/Tools/SequelCatcher/List.svelte index a7e03ed0..79e28703 100644 --- a/src/lib/Tools/SequelCatcher/List.svelte +++ b/src/lib/Tools/SequelCatcher/List.svelte @@ -1,28 +1,28 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { filterRelations, type Media } from '$lib/Data/AniList/media'; - import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; - import { outboundLink } from '$lib/Media/links'; - import settings from '$stores/settings'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { filterRelations, type Media } from '$lib/Data/AniList/media'; +import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; +import { outboundLink } from '$lib/Media/links'; +import settings from '$stores/settings'; - export let mediaListUnchecked: Media[]; +export let mediaListUnchecked: Media[]; - let includeCurrent = false; - let includeSideStories = false; +let includeCurrent = false; +let includeSideStories = false; - const matchCheck = (media: Media | undefined, swap = false) => - (media && - media.mediaListEntry && - media.mediaListEntry?.status !== 'CURRENT' && - media.mediaListEntry?.status !== 'REPEATING' && - media.mediaListEntry?.status !== 'PAUSED') || - !media - ? swap - ? undefined - : media - : swap - ? media - : undefined; +const matchCheck = (media: Media | undefined, swap = false) => + (media && + media.mediaListEntry && + media.mediaListEntry?.status !== 'CURRENT' && + media.mediaListEntry?.status !== 'REPEATING' && + media.mediaListEntry?.status !== 'PAUSED') || + !media + ? swap + ? undefined + : media + : swap + ? media + : undefined; </script> <input type="checkbox" bind:checked={includeCurrent} /> Include current (watching, rewatching, diff --git a/src/lib/Tools/SequelCatcher/Tool.svelte b/src/lib/Tools/SequelCatcher/Tool.svelte index 05227ac4..01742d6f 100644 --- a/src/lib/Tools/SequelCatcher/Tool.svelte +++ b/src/lib/Tools/SequelCatcher/Tool.svelte @@ -1,39 +1,32 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import List from './List.svelte'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import userIdentity from '$stores/identity'; - import { type Media, mediaListCollection, Type } from '$lib/Data/AniList/media'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - import anime from '$stores/anime'; - import identity from '$stores/identity'; - import { onMount } from 'svelte'; - import lastPruneTimes from '$stores/lastPruneTimes'; - import Message from '$lib/Loading/Message.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Username from '$lib/Layout/Username.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import List from './List.svelte'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import userIdentity from '$stores/identity'; +import { type Media, mediaListCollection, Type } from '$lib/Data/AniList/media'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; +import anime from '$stores/anime'; +import identity from '$stores/identity'; +import { onMount } from 'svelte'; +import lastPruneTimes from '$stores/lastPruneTimes'; +import Message from '$lib/Loading/Message.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import Username from '$lib/Layout/Username.svelte'; - export let user: AniListAuthorisation; +export let user: AniListAuthorisation; - let mediaList: Promise<Media[]>; +let mediaList: Promise<Media[]>; - onMount(async () => { - if (user === undefined || $identity.id === -2) return; +onMount(async () => { + if (user === undefined || $identity.id === -2) return; - mediaList = mediaListCollection( - user, - $userIdentity, - Type.Anime, - $anime, - $lastPruneTimes.anime, - { - forcePrune: true, - includeCompleted: true, - all: true, - includeRelations: true - } - ); + mediaList = mediaListCollection(user, $userIdentity, Type.Anime, $anime, $lastPruneTimes.anime, { + forcePrune: true, + includeCompleted: true, + all: true, + includeRelations: true }); +}); </script> {#if user === undefined || $identity.id === -2} diff --git a/src/lib/Tools/SequelSpy/Prequels.svelte b/src/lib/Tools/SequelSpy/Prequels.svelte index b22db3af..5929821e 100644 --- a/src/lib/Tools/SequelSpy/Prequels.svelte +++ b/src/lib/Tools/SequelSpy/Prequels.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import type { MediaPrequel } from '$lib/Data/AniList/prequels'; - import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; - import { airingTime } from '$lib/Media/Anime/Airing/time'; - import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; - import settings from '$stores/settings'; - import type { Media } from '$lib/Data/AniList/media'; +import type { MediaPrequel } from '$lib/Data/AniList/prequels'; +import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; +import { airingTime } from '$lib/Media/Anime/Airing/time'; +import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; +import settings from '$stores/settings'; +import type { Media } from '$lib/Data/AniList/media'; - export let currentPrequels: MediaPrequel[]; +export let currentPrequels: MediaPrequel[]; - const prequelAiringTime = (prequel: MediaPrequel) => - airingTime(prequel as unknown as Media, null, false, true); +const prequelAiringTime = (prequel: MediaPrequel) => + airingTime(prequel as unknown as Media, null, false, true); </script> <ul> diff --git a/src/lib/Tools/SequelSpy/Tool.svelte b/src/lib/Tools/SequelSpy/Tool.svelte index 998cab13..0a862984 100644 --- a/src/lib/Tools/SequelSpy/Tool.svelte +++ b/src/lib/Tools/SequelSpy/Tool.svelte @@ -1,38 +1,38 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { prequels, type MediaPrequel } from '$lib/Data/AniList/prequels'; - import { onMount } from 'svelte'; - import { clearAllParameters, parseOrDefault } from '../../Utility/parameters'; - import { page } from '$app/stores'; - import { browser } from '$app/environment'; - import { season as getSeason } from '$lib/Media/Anime/season'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import identity from '$stores/identity'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - import Prequels from './Prequels.svelte'; - - export let user: AniListAuthorisation; - - let currentPrequels: Promise<MediaPrequel[]> = Promise.resolve([]) as Promise<MediaPrequel[]>; - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - let year = parseOrDefault(urlParameters, 'year', new Date().getFullYear()); - let season = parseOrDefault(urlParameters, 'season', getSeason()); - - $: { - if (year.toString().length === 4 && $identity.id !== -2 && user) - currentPrequels = prequels(user, year, season); - } - $: { - if (browser) { - $page.url.searchParams.set('year', year.toString()); - $page.url.searchParams.set('season', season.toString()); - clearAllParameters(['year', 'season']); - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } +import Spacer from '$lib/Layout/Spacer.svelte'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import { prequels, type MediaPrequel } from '$lib/Data/AniList/prequels'; +import { onMount } from 'svelte'; +import { clearAllParameters, parseOrDefault } from '../../Utility/parameters'; +import { page } from '$app/stores'; +import { browser } from '$app/environment'; +import { season as getSeason } from '$lib/Media/Anime/season'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import identity from '$stores/identity'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; +import Prequels from './Prequels.svelte'; + +export let user: AniListAuthorisation; + +let currentPrequels: Promise<MediaPrequel[]> = Promise.resolve([]) as Promise<MediaPrequel[]>; +const urlParameters = browser ? new URLSearchParams(window.location.search) : null; +let year = parseOrDefault(urlParameters, 'year', new Date().getFullYear()); +let season = parseOrDefault(urlParameters, 'season', getSeason()); + +$: { + if (year.toString().length === 4 && $identity.id !== -2 && user) + currentPrequels = prequels(user, year, season); +} +$: { + if (browser) { + $page.url.searchParams.set('year', year.toString()); + $page.url.searchParams.set('season', season.toString()); + clearAllParameters(['year', 'season']); + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } +} - onMount(() => clearAllParameters(['year', 'season'])); +onMount(() => clearAllParameters(['year', 'season'])); </script> {#if user === undefined || $identity.id === -2} diff --git a/src/lib/Tools/Tracker/Tool.svelte b/src/lib/Tools/Tracker/Tool.svelte index a3705fbe..1849cbb1 100644 --- a/src/lib/Tools/Tracker/Tool.svelte +++ b/src/lib/Tools/Tracker/Tool.svelte @@ -1,70 +1,70 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { v6 as uuidv6 } from 'uuid'; - import { database, type TrackerEntry } from '$lib/Database/IDB/tracker'; - import { onMount } from 'svelte'; - import Message from '$lib/Loading/Message.svelte'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import { v6 as uuidv6 } from 'uuid'; +import { database, type TrackerEntry } from '$lib/Database/IDB/tracker'; +import { onMount } from 'svelte'; +import Message from '$lib/Loading/Message.svelte'; - let url = ''; - let title = ''; - let progress = 0; - let error = ''; - let masterList: TrackerEntry[] | null = null; - let confirmDelete = 0; +let url = ''; +let title = ''; +let progress = 0; +let error = ''; +let masterList: TrackerEntry[] | null = null; +let confirmDelete = 0; - $: listAccess = masterList || []; +$: listAccess = masterList || []; - onMount(async () => { - masterList = await database.entries.toArray(); - }); +onMount(async () => { + masterList = await database.entries.toArray(); +}); - const adjustEntry = (id: string, to: number) => { - const entry = listAccess.find((entry) => entry.id === id); +const adjustEntry = (id: string, to: number) => { + const entry = listAccess.find((entry) => entry.id === id); - if (!entry) return; + if (!entry) return; - entry.progress = Math.max(0, to); + entry.progress = Math.max(0, to); - database.entries.update(id, { progress: entry.progress }); + database.entries.update(id, { progress: entry.progress }); - masterList = listAccess.map((entry) => - entry.id === id ? { ...entry, progress: entry.progress } : entry - ); - }; + masterList = listAccess.map((entry) => + entry.id === id ? { ...entry, progress: entry.progress } : entry + ); +}; - const addEntry = async (url: string, title: string, progress: number) => { - if (!url || !title) { - error = 'URL and title are required fields'; +const addEntry = async (url: string, title: string, progress: number) => { + if (!url || !title) { + error = 'URL and title are required fields'; - return; - } + return; + } - if (listAccess.some((entry) => entry.url === url)) { - error = - 'Entry with URL already exists: ' + listAccess.find((entry) => entry.url === url)?.title; + if (listAccess.some((entry) => entry.url === url)) { + error = + 'Entry with URL already exists: ' + listAccess.find((entry) => entry.url === url)?.title; - return; - } + return; + } - await database.entries.add({ url, title, progress, id: uuidv6() }); + await database.entries.add({ url, title, progress, id: uuidv6() }); - masterList = await database.entries.toArray(); - }; + masterList = await database.entries.toArray(); +}; - const deleteEntry = async (id: string) => { - if (confirmDelete !== 1) { - confirmDelete = 1; - error = 'Click again to confirm deletion'; +const deleteEntry = async (id: string) => { + if (confirmDelete !== 1) { + confirmDelete = 1; + error = 'Click again to confirm deletion'; - return; - } + return; + } - await database.entries.delete(id); + await database.entries.delete(id); - masterList = await database.entries.toArray(); - confirmDelete = 0; - error = ''; - }; + masterList = await database.entries.toArray(); + confirmDelete = 0; + error = ''; +}; </script> <div class="card"> diff --git a/src/lib/Tools/UmaMusumeBirthdays.svelte b/src/lib/Tools/UmaMusumeBirthdays.svelte index 8ee6697b..cc8c1513 100644 --- a/src/lib/Tools/UmaMusumeBirthdays.svelte +++ b/src/lib/Tools/UmaMusumeBirthdays.svelte @@ -1,51 +1,51 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { page } from '$app/stores'; - import Error from '$lib/Error/RateLimited.svelte'; - import { onMount } from 'svelte'; - import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import settings from '$stores/settings'; - import locale from '$stores/locale'; +import { browser } from '$app/environment'; +import { page } from '$app/stores'; +import Error from '$lib/Error/RateLimited.svelte'; +import { onMount } from 'svelte'; +import { clearAllParameters, parseOrDefault } from '../Utility/parameters'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import Message from '$lib/Loading/Message.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; +import settings from '$stores/settings'; +import locale from '$stores/locale'; - interface Birthday { - birth_day: number; - birth_month: number; - game_id: number; - id: number; - name_en: string; - name_jp: string; - preferred_url: string; - sns_icon: string; - } +interface Birthday { + birth_day: number; + birth_month: number; + game_id: number; + id: number; + name_en: string; + name_jp: string; + preferred_url: string; + sns_icon: string; +} - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - let date = new Date(); - let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); - let day = parseOrDefault(urlParameters, 'day', date.getDate()); - let umapyoi: Promise<Birthday[]>; +const urlParameters = browser ? new URLSearchParams(window.location.search) : null; +let date = new Date(); +let month = parseOrDefault(urlParameters, 'month', date.getMonth() + 1); +let day = parseOrDefault(urlParameters, 'day', date.getDate()); +let umapyoi: Promise<Birthday[]>; - $: { - month = Math.min(month, 12); - month = Math.max(month, 1); - day = Math.min(day, new Date(new Date().getFullYear(), month, 0).getDate()); - day = Math.max(day, 1); +$: { + month = Math.min(month, 12); + month = Math.max(month, 1); + day = Math.min(day, new Date(new Date().getFullYear(), month, 0).getDate()); + day = Math.max(day, 1); - if (browser) { - $page.url.searchParams.set('month', month.toString()); - $page.url.searchParams.set('day', day.toString()); - clearAllParameters(['month', 'day']); - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } + if (browser) { + $page.url.searchParams.set('month', month.toString()); + $page.url.searchParams.set('day', day.toString()); + clearAllParameters(['month', 'day']); + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } +} - onMount(() => { - clearAllParameters(['month', 'day']); +onMount(() => { + clearAllParameters(['month', 'day']); - umapyoi = fetch('https://umapyoi.net/api/v1/character/birthday').then((r) => r.json()); - }); + umapyoi = fetch('https://umapyoi.net/api/v1/character/birthday').then((r) => r.json()); +}); </script> {#await umapyoi} diff --git a/src/lib/Tools/Wrapped/ActivityHistory.svelte b/src/lib/Tools/Wrapped/ActivityHistory.svelte index 3da401d4..f1f1a28a 100644 --- a/src/lib/Tools/Wrapped/ActivityHistory.svelte +++ b/src/lib/Tools/Wrapped/ActivityHistory.svelte @@ -1,12 +1,12 @@ <script lang="ts"> - import type { ActivityHistoryEntry } from '$lib/Data/AniList/activity'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import ActivityHistoryGrid from '../ActivityHistory/Grid.svelte'; +import type { ActivityHistoryEntry } from '$lib/Data/AniList/activity'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import ActivityHistoryGrid from '../ActivityHistory/Grid.svelte'; - export let user: AniListAuthorisation; - export let activities: ActivityHistoryEntry[]; - export let year: number; - export let activityHistoryPosition: 'TOP' | 'BELOW_TOP' | 'ORIGINAL'; +export let user: AniListAuthorisation; +export let activities: ActivityHistoryEntry[]; +export let year: number; +export let activityHistoryPosition: 'TOP' | 'BELOW_TOP' | 'ORIGINAL'; </script> <div diff --git a/src/lib/Tools/Wrapped/DataLoader.svelte b/src/lib/Tools/Wrapped/DataLoader.svelte index 9c99b4a4..6063f349 100644 --- a/src/lib/Tools/Wrapped/DataLoader.svelte +++ b/src/lib/Tools/Wrapped/DataLoader.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - import { onMount } from 'svelte'; +import { onMount } from 'svelte'; - export let onLoad: () => void; +export let onLoad: () => void; - onMount(() => onLoad()); +onMount(() => onLoad()); </script> diff --git a/src/lib/Tools/Wrapped/Media.svelte b/src/lib/Tools/Wrapped/Media.svelte index 90e0cde3..bbd7ee5b 100644 --- a/src/lib/Tools/Wrapped/Media.svelte +++ b/src/lib/Tools/Wrapped/Media.svelte @@ -1,17 +1,17 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; - import type { Wrapped } from '$lib/Data/AniList/wrapped'; - import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; - import proxy from '$lib/Utility/proxy'; +import type { Media } from '$lib/Data/AniList/media'; +import type { Wrapped } from '$lib/Data/AniList/wrapped'; +import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; +import proxy from '$lib/Utility/proxy'; - export let animeList: Media[] | undefined; - export let mangaList: Media[] | undefined; - export let wrapped: Wrapped; - export let updateWidth: () => void; - export let highestRatedMediaPercentage: boolean; - export let highestRatedCount: number; - export let animeMostTitle: string; - export let mangaMostTitle: string; +export let animeList: Media[] | undefined; +export let mangaList: Media[] | undefined; +export let wrapped: Wrapped; +export let updateWidth: () => void; +export let highestRatedMediaPercentage: boolean; +export let highestRatedCount: number; +export let animeMostTitle: string; +export let mangaMostTitle: string; </script> {#if animeList !== undefined || mangaList !== undefined} diff --git a/src/lib/Tools/Wrapped/MediaExtras.svelte b/src/lib/Tools/Wrapped/MediaExtras.svelte index 78b89e36..d9dc8efb 100644 --- a/src/lib/Tools/Wrapped/MediaExtras.svelte +++ b/src/lib/Tools/Wrapped/MediaExtras.svelte @@ -1,11 +1,11 @@ <script lang="ts"> - import type { TopMedia } from '$lib/Data/AniList/wrapped'; - import proxy from '$lib/Utility/proxy'; +import type { TopMedia } from '$lib/Data/AniList/wrapped'; +import proxy from '$lib/Utility/proxy'; - export let topMedia: TopMedia; - export let updateWidth: () => void; - export let highestRatedGenreTagPercentage: boolean; - export let genreTagTitle: string; +export let topMedia: TopMedia; +export let updateWidth: () => void; +export let highestRatedGenreTagPercentage: boolean; +export let genreTagTitle: string; </script> <div class="categories-grid" style="padding-top: 0;"> diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte index 3985b4c0..3f13b483 100644 --- a/src/lib/Tools/Wrapped/Tool.svelte +++ b/src/lib/Tools/Wrapped/Tool.svelte @@ -1,748 +1,730 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import './wrapped.css'; - import userIdentity from '$stores/identity'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { onMount } from 'svelte'; - import { - tops, - wrapped, - type TopMedia, - SortOptions, - type Wrapped - } from '$lib/Data/AniList/wrapped'; - import { - fullActivityHistory, - activityHistory as getActivityHistory, - type ActivityHistoryEntry - } from '$lib/Data/AniList/activity'; - import { Type, mediaListCollection, type Media } from '$lib/Data/AniList/media'; - import anime from '$stores/anime'; - import lastPruneTimes from '$stores/lastPruneTimes'; - import manga from '$stores/manga'; - import Error from '$lib/Error/RateLimited.svelte'; - import { domToBlob } from 'modern-screenshot'; - import { browser } from '$app/environment'; - import { page } from '$app/stores'; - import { clearAllParameters } from '../../Utility/parameters'; - import SettingHint from '$lib/Settings/SettingHint.svelte'; - import { database } from '$lib/Database/IDB/activities'; - import Activity from './Top/Activity.svelte'; - import Anime from './Top/Anime.svelte'; - import Manga from './Top/Manga.svelte'; - import ActivityHistory from './ActivityHistory.svelte'; - import MediaExtras from './MediaExtras.svelte'; - import MediaPanel from './Media.svelte'; - import Watermark from './Watermark.svelte'; - import DataLoader from './DataLoader.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - - export let user: AniListAuthorisation; - - const currentYear = new Date(Date.now()).getFullYear(); - let selectedYear = new Date(Date.now()).getFullYear(); - let episodes = 0; - let chapters = 0; - let minutesWatched = 0; - let animeList: Media[] | undefined = undefined; - let mangaList: Media[] | undefined = undefined; - let calculatedAnimeList: Media[] | undefined = undefined; - let calculatedMangaList: Media[] | undefined = undefined; - let originalAnimeList: Media[] | undefined = undefined; - let originalMangaList: Media[] | undefined = undefined; - let transparency = false; - let lightTheme = true; - let watermark = false; - let includeMusic = false; - let includeSpecials = true; - let includeRepeats = false; - let width = 1920; - let lightMode = false; - let highestRatedCount = 5; - let genreTagCount = 5; - let mounted = false; - let generated = false; - let disableActivityHistory = true; - let excludedKeywordsInput = ''; - let excludedKeywords: string[] = []; - let useFullActivityHistory = false; - let disableLoopingActivityCounter = false; - let topGenresTags = true; - let topMedia: TopMedia; - let highestRatedMediaPercentage = true; - let highestRatedGenreTagPercentage = true; - let genreTagsSort = SortOptions.SCORE; - let mediaSort = SortOptions.SCORE; - let includeMovies = true; - let includeOVAs = true; - let activityHistoryPosition: 'TOP' | 'BELOW_TOP' | 'ORIGINAL' = 'ORIGINAL'; - let includeOngoingMediaFromPreviousYears = false; - let excludeUnratedUnwatched = true; - let startDateFilter: Date | null = null; - let endDateFilter: Date | null = null; - let dateTicked = false; - let shouldFetchData = false; - let needsRefetch = false; - let dataFetched = false; - let fetchKey = 0; - let lastSelectedYear = selectedYear; - let lastUseFullActivityHistory = useFullActivityHistory; - let lastDisableLoopingActivityCounter = disableLoopingActivityCounter; - let lastStartDateFilter: Date | null = startDateFilter; - let lastEndDateFilter: Date | null = endDateFilter; - - $: { - if (browser && mounted) { - $page.url.searchParams.set('transparency', transparency.toString()); - $page.url.searchParams.set('lightTheme', lightTheme.toString()); - $page.url.searchParams.set('watermark', watermark.toString()); - $page.url.searchParams.set('includeMusic', includeMusic.toString()); - $page.url.searchParams.set('includeSpecials', includeSpecials.toString()); - $page.url.searchParams.set('includeRepeats', includeRepeats.toString()); - $page.url.searchParams.set('lightMode', lightMode.toString()); - $page.url.searchParams.set('highestRatedCount', highestRatedCount.toString()); - $page.url.searchParams.set('genreTagCount', genreTagCount.toString()); - $page.url.searchParams.set('disableActivityHistory', disableActivityHistory.toString()); - $page.url.searchParams.set( - 'highestRatedMediaPercentage', - highestRatedMediaPercentage.toString() - ); - $page.url.searchParams.set( - 'highestRatedGenreTagPercentage', - highestRatedGenreTagPercentage.toString() - ); - $page.url.searchParams.set('genreTagsSort', genreTagsSort.toString()); - $page.url.searchParams.set('mediaSort', mediaSort.toString()); - $page.url.searchParams.set('includeMovies', includeMovies.toString()); - $page.url.searchParams.set('includeOVAs', includeOVAs.toString()); - $page.url.searchParams.set('excludeUnratedUnwatched', excludeUnratedUnwatched.toString()); - $page.url.searchParams.set( - 'disableLoopingActivityCounter', - disableLoopingActivityCounter.toString() - ); - - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } - } +import Spacer from '$lib/Layout/Spacer.svelte'; +import './wrapped.css'; +import userIdentity from '$stores/identity'; +import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; +import { onMount } from 'svelte'; +import { tops, wrapped, type TopMedia, SortOptions, type Wrapped } from '$lib/Data/AniList/wrapped'; +import { + fullActivityHistory, + activityHistory as getActivityHistory, + type ActivityHistoryEntry +} from '$lib/Data/AniList/activity'; +import { Type, mediaListCollection, type Media } from '$lib/Data/AniList/media'; +import anime from '$stores/anime'; +import lastPruneTimes from '$stores/lastPruneTimes'; +import manga from '$stores/manga'; +import Error from '$lib/Error/RateLimited.svelte'; +import { domToBlob } from 'modern-screenshot'; +import { browser } from '$app/environment'; +import { page } from '$app/stores'; +import { clearAllParameters } from '../../Utility/parameters'; +import SettingHint from '$lib/Settings/SettingHint.svelte'; +import { database } from '$lib/Database/IDB/activities'; +import Activity from './Top/Activity.svelte'; +import Anime from './Top/Anime.svelte'; +import Manga from './Top/Manga.svelte'; +import ActivityHistory from './ActivityHistory.svelte'; +import MediaExtras from './MediaExtras.svelte'; +import MediaPanel from './Media.svelte'; +import Watermark from './Watermark.svelte'; +import DataLoader from './DataLoader.svelte'; +import Skeleton from '$lib/Loading/Skeleton.svelte'; +import Message from '$lib/Loading/Message.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; +import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; + +export let user: AniListAuthorisation; + +const currentYear = new Date(Date.now()).getFullYear(); +let selectedYear = new Date(Date.now()).getFullYear(); +let episodes = 0; +let chapters = 0; +let minutesWatched = 0; +let animeList: Media[] | undefined = undefined; +let mangaList: Media[] | undefined = undefined; +let calculatedAnimeList: Media[] | undefined = undefined; +let calculatedMangaList: Media[] | undefined = undefined; +let originalAnimeList: Media[] | undefined = undefined; +let originalMangaList: Media[] | undefined = undefined; +let transparency = false; +let lightTheme = true; +let watermark = false; +let includeMusic = false; +let includeSpecials = true; +let includeRepeats = false; +let width = 1920; +let lightMode = false; +let highestRatedCount = 5; +let genreTagCount = 5; +let mounted = false; +let generated = false; +let disableActivityHistory = true; +let excludedKeywordsInput = ''; +let excludedKeywords: string[] = []; +let useFullActivityHistory = false; +let disableLoopingActivityCounter = false; +let topGenresTags = true; +let topMedia: TopMedia; +let highestRatedMediaPercentage = true; +let highestRatedGenreTagPercentage = true; +let genreTagsSort = SortOptions.SCORE; +let mediaSort = SortOptions.SCORE; +let includeMovies = true; +let includeOVAs = true; +let activityHistoryPosition: 'TOP' | 'BELOW_TOP' | 'ORIGINAL' = 'ORIGINAL'; +let includeOngoingMediaFromPreviousYears = false; +let excludeUnratedUnwatched = true; +let startDateFilter: Date | null = null; +let endDateFilter: Date | null = null; +let dateTicked = false; +let shouldFetchData = false; +let needsRefetch = false; +let dataFetched = false; +let fetchKey = 0; +let lastSelectedYear = selectedYear; +let lastUseFullActivityHistory = useFullActivityHistory; +let lastDisableLoopingActivityCounter = disableLoopingActivityCounter; +let lastStartDateFilter: Date | null = startDateFilter; +let lastEndDateFilter: Date | null = endDateFilter; + +$: { + if (browser && mounted) { + $page.url.searchParams.set('transparency', transparency.toString()); + $page.url.searchParams.set('lightTheme', lightTheme.toString()); + $page.url.searchParams.set('watermark', watermark.toString()); + $page.url.searchParams.set('includeMusic', includeMusic.toString()); + $page.url.searchParams.set('includeSpecials', includeSpecials.toString()); + $page.url.searchParams.set('includeRepeats', includeRepeats.toString()); + $page.url.searchParams.set('lightMode', lightMode.toString()); + $page.url.searchParams.set('highestRatedCount', highestRatedCount.toString()); + $page.url.searchParams.set('genreTagCount', genreTagCount.toString()); + $page.url.searchParams.set('disableActivityHistory', disableActivityHistory.toString()); + $page.url.searchParams.set( + 'highestRatedMediaPercentage', + highestRatedMediaPercentage.toString() + ); + $page.url.searchParams.set( + 'highestRatedGenreTagPercentage', + highestRatedGenreTagPercentage.toString() + ); + $page.url.searchParams.set('genreTagsSort', genreTagsSort.toString()); + $page.url.searchParams.set('mediaSort', mediaSort.toString()); + $page.url.searchParams.set('includeMovies', includeMovies.toString()); + $page.url.searchParams.set('includeOVAs', includeOVAs.toString()); + $page.url.searchParams.set('excludeUnratedUnwatched', excludeUnratedUnwatched.toString()); + $page.url.searchParams.set( + 'disableLoopingActivityCounter', + disableLoopingActivityCounter.toString() + ); - $: { - if (dataFetched) { - const yearChanged = selectedYear !== lastSelectedYear; - const fullActivityChanged = useFullActivityHistory !== lastUseFullActivityHistory; - const loopingChanged = disableLoopingActivityCounter !== lastDisableLoopingActivityCounter; - const startDateChanged = startDateFilter !== lastStartDateFilter; - const endDateChanged = endDateFilter !== lastEndDateFilter; - - if ( - yearChanged || - fullActivityChanged || - loopingChanged || - startDateChanged || - endDateChanged - ) - needsRefetch = true; - } + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } +} + +$: { + if (dataFetched) { + const yearChanged = selectedYear !== lastSelectedYear; + const fullActivityChanged = useFullActivityHistory !== lastUseFullActivityHistory; + const loopingChanged = disableLoopingActivityCounter !== lastDisableLoopingActivityCounter; + const startDateChanged = startDateFilter !== lastStartDateFilter; + const endDateChanged = endDateFilter !== lastEndDateFilter; + + if (yearChanged || fullActivityChanged || loopingChanged || startDateChanged || endDateChanged) + needsRefetch = true; + } +} + +$: { + includeMusic = includeMusic; + includeSpecials = includeSpecials; + includeRepeats = includeRepeats; + disableActivityHistory = disableActivityHistory; + highestRatedMediaPercentage = highestRatedMediaPercentage; + highestRatedGenreTagPercentage = highestRatedGenreTagPercentage; + topGenresTags = topGenresTags; + genreTagsSort = genreTagsSort; + mediaSort = mediaSort; + includeMovies = includeMovies; + includeOVAs = includeOVAs; + selectedYear = selectedYear; + includeOngoingMediaFromPreviousYears = includeOngoingMediaFromPreviousYears; + excludeUnratedUnwatched = excludeUnratedUnwatched; + + if (shouldFetchData) update().then(updateWidth).catch(updateWidth); +} +$: { + animeList = animeList; + mangaList = mangaList; + highestRatedCount = highestRatedCount; + + new Promise((resolve) => setTimeout(resolve, 1)).then(updateWidth); +} +$: { + genreTagCount = genreTagCount; + + if (animeList && mangaList) + topMedia = tops( + [...(animeList || []), ...(mangaList || [])], + genreTagCount, + genreTagsSort, + excludedKeywords + ); - $: { - includeMusic = includeMusic; - includeSpecials = includeSpecials; - includeRepeats = includeRepeats; - disableActivityHistory = disableActivityHistory; - highestRatedMediaPercentage = highestRatedMediaPercentage; - highestRatedGenreTagPercentage = highestRatedGenreTagPercentage; - topGenresTags = topGenresTags; - genreTagsSort = genreTagsSort; - mediaSort = mediaSort; - includeMovies = includeMovies; - includeOVAs = includeOVAs; - selectedYear = selectedYear; - includeOngoingMediaFromPreviousYears = includeOngoingMediaFromPreviousYears; - excludeUnratedUnwatched = excludeUnratedUnwatched; - - if (shouldFetchData) update().then(updateWidth).catch(updateWidth); + new Promise((resolve) => setTimeout(resolve, 1)).then(updateWidth); +} +$: { + excludedKeywords = excludedKeywords; + + if (excludedKeywords.length > 0 && animeList !== undefined && mangaList !== undefined) { + animeList = originalAnimeList; + mangaList = originalMangaList; + animeList = excludeKeywords(animeList as Media[]); + mangaList = excludeKeywords(mangaList as Media[]); } - $: { - animeList = animeList; - mangaList = mangaList; - highestRatedCount = highestRatedCount; - new Promise((resolve) => setTimeout(resolve, 1)).then(updateWidth); + updateWidth(); +} +$: genreTagTitle = (() => { + switch (genreTagsSort) { + case SortOptions.SCORE: + return 'Highest Rated'; + case SortOptions.MINUTES_WATCHED: + return 'Most Watched'; + case SortOptions.COUNT: + return 'Most Common'; } - $: { - genreTagCount = genreTagCount; - - if (animeList && mangaList) - topMedia = tops( - [...(animeList || []), ...(mangaList || [])], - genreTagCount, - genreTagsSort, - excludedKeywords - ); - - new Promise((resolve) => setTimeout(resolve, 1)).then(updateWidth); +})(); +$: animeMostTitle = (() => { + switch (mediaSort) { + case SortOptions.SCORE: + return 'Highest Rated'; + case SortOptions.MINUTES_WATCHED: + return 'Most Watched'; + case SortOptions.COUNT: + return 'Most Common'; } - $: { - excludedKeywords = excludedKeywords; - - if (excludedKeywords.length > 0 && animeList !== undefined && mangaList !== undefined) { - animeList = originalAnimeList; - mangaList = originalMangaList; - animeList = excludeKeywords(animeList as Media[]); - mangaList = excludeKeywords(mangaList as Media[]); - } - - updateWidth(); +})(); +$: mangaMostTitle = (() => { + switch (mediaSort) { + case SortOptions.SCORE: + return 'Highest Rated'; + case SortOptions.MINUTES_WATCHED: + return 'Most Read'; + case SortOptions.COUNT: + return 'Most Common'; } - $: genreTagTitle = (() => { - switch (genreTagsSort) { - case SortOptions.SCORE: - return 'Highest Rated'; - case SortOptions.MINUTES_WATCHED: - return 'Most Watched'; - case SortOptions.COUNT: - return 'Most Common'; - } - })(); - $: animeMostTitle = (() => { - switch (mediaSort) { - case SortOptions.SCORE: - return 'Highest Rated'; - case SortOptions.MINUTES_WATCHED: - return 'Most Watched'; - case SortOptions.COUNT: - return 'Most Common'; - } - })(); - $: mangaMostTitle = (() => { - switch (mediaSort) { - case SortOptions.SCORE: - return 'Highest Rated'; - case SortOptions.MINUTES_WATCHED: - return 'Most Read'; - case SortOptions.COUNT: - return 'Most Common'; - } - })(); - - const updateWidth = () => { - if (!browser) return; - - const wrappedContainer = document.querySelector('#wrapped') as HTMLElement; - - if (!wrappedContainer) return; - - wrappedContainer.style.width = `1920px`; - - const reset = () => { - let topWidths = 0; - let middleWidths = 0; - let bottomWidths = 0; - - wrappedContainer.querySelectorAll('.category').forEach((item) => { - const category = item as HTMLElement; - const style = window.getComputedStyle(category); - const width = - category.offsetWidth + - parseFloat(style.marginLeft) + - parseFloat(style.marginRight) + - parseFloat(style.paddingLeft) + - parseFloat(style.paddingRight) + - parseFloat(style.borderLeftWidth) + - parseFloat(style.borderRightWidth); - - if (category.classList.contains('top-category')) { - topWidths += width; - } else if (category.classList.contains('middle-category')) { - middleWidths += width; - } else if (category.classList.contains('bottom-category')) { - bottomWidths += width; - } - }); - - let requiredWidth = topWidths > middleWidths ? topWidths : middleWidths; +})(); + +const updateWidth = () => { + if (!browser) return; + + const wrappedContainer = document.querySelector('#wrapped') as HTMLElement; + + if (!wrappedContainer) return; + + wrappedContainer.style.width = `1920px`; + + const reset = () => { + let topWidths = 0; + let middleWidths = 0; + let bottomWidths = 0; + + wrappedContainer.querySelectorAll('.category').forEach((item) => { + const category = item as HTMLElement; + const style = window.getComputedStyle(category); + const width = + category.offsetWidth + + parseFloat(style.marginLeft) + + parseFloat(style.marginRight) + + parseFloat(style.paddingLeft) + + parseFloat(style.paddingRight) + + parseFloat(style.borderLeftWidth) + + parseFloat(style.borderRightWidth); + + if (category.classList.contains('top-category')) { + topWidths += width; + } else if (category.classList.contains('middle-category')) { + middleWidths += width; + } else if (category.classList.contains('bottom-category')) { + bottomWidths += width; + } + }); - if (!disableActivityHistory && bottomWidths > requiredWidth) requiredWidth = bottomWidths; + let requiredWidth = topWidths > middleWidths ? topWidths : middleWidths; - requiredWidth += wrappedContainer.offsetWidth - wrappedContainer.clientWidth; + if (!disableActivityHistory && bottomWidths > requiredWidth) requiredWidth = bottomWidths; - wrappedContainer.style.width = `${requiredWidth}px`; - width = requiredWidth; - }; + requiredWidth += wrappedContainer.offsetWidth - wrappedContainer.clientWidth; - reset(); - reset(); + wrappedContainer.style.width = `${requiredWidth}px`; + width = requiredWidth; }; - onMount(async () => { - clearAllParameters([ - 'transparency', - 'lightTheme', - 'watermark', - 'includeMusic', - 'includeSpecials', - 'includeRepeats', - 'forceDark', - 'highestRatedCount', - 'genreTagCount', - 'disableActivityHistory', - 'highestRatedMediaPercentage', - 'highestRatedGenreTagPercentage', - 'genreTagsSort', - 'mediaSort', - 'includeMovies', - 'includeOVAs', - 'disableLoopingActivityCounter' - ]); - - if (browser) { - transparency = $page.url.searchParams.get('transparency') === 'true'; - lightTheme = $page.url.searchParams.get('lightTheme') === 'true'; - watermark = $page.url.searchParams.get('watermark') === 'true'; - includeMusic = $page.url.searchParams.get('includeMusic') === 'true'; - includeSpecials = $page.url.searchParams.get('includeSpecials') === 'true'; - includeRepeats = $page.url.searchParams.get('includeRepeats') === 'true'; - lightMode = $page.url.searchParams.get('lightMode') === 'true'; - highestRatedCount = parseInt($page.url.searchParams.get('highestRatedCount') || '5', 10); - genreTagCount = parseInt($page.url.searchParams.get('genreTagCount') || '5', 10); - disableActivityHistory = $page.url.searchParams.get('disableActivityHistory') === 'true'; - disableLoopingActivityCounter = - $page.url.searchParams.get('disableLoopingActivityCounter') === 'true'; - highestRatedMediaPercentage = - $page.url.searchParams.get('highestRatedMediaPercentage') === 'true'; - highestRatedGenreTagPercentage = - $page.url.searchParams.get('highestRatedGenreTagPercentage') === 'true'; - // genreTagsSort = parseInt($page.url.searchParams.get('genreTagsSort') || '0', 10); - // mediaSort = parseInt($page.url.searchParams.get('mediaSort') || '0', 10); - includeMovies = $page.url.searchParams.get('includeMovies') === 'true'; - includeOVAs = $page.url.searchParams.get('includeOVAs') === 'true'; - } - - await update().then(() => (mounted = true)); - }); - - const triggerFetch = () => { - shouldFetchData = true; - needsRefetch = false; - dataFetched = true; - fetchKey += 1; - lastSelectedYear = selectedYear; - lastUseFullActivityHistory = useFullActivityHistory; - lastDisableLoopingActivityCounter = disableLoopingActivityCounter; - lastStartDateFilter = startDateFilter; - lastEndDateFilter = endDateFilter; - }; + reset(); + reset(); +}; + +onMount(async () => { + clearAllParameters([ + 'transparency', + 'lightTheme', + 'watermark', + 'includeMusic', + 'includeSpecials', + 'includeRepeats', + 'forceDark', + 'highestRatedCount', + 'genreTagCount', + 'disableActivityHistory', + 'highestRatedMediaPercentage', + 'highestRatedGenreTagPercentage', + 'genreTagsSort', + 'mediaSort', + 'includeMovies', + 'includeOVAs', + 'disableLoopingActivityCounter' + ]); + + if (browser) { + transparency = $page.url.searchParams.get('transparency') === 'true'; + lightTheme = $page.url.searchParams.get('lightTheme') === 'true'; + watermark = $page.url.searchParams.get('watermark') === 'true'; + includeMusic = $page.url.searchParams.get('includeMusic') === 'true'; + includeSpecials = $page.url.searchParams.get('includeSpecials') === 'true'; + includeRepeats = $page.url.searchParams.get('includeRepeats') === 'true'; + lightMode = $page.url.searchParams.get('lightMode') === 'true'; + highestRatedCount = parseInt($page.url.searchParams.get('highestRatedCount') || '5', 10); + genreTagCount = parseInt($page.url.searchParams.get('genreTagCount') || '5', 10); + disableActivityHistory = $page.url.searchParams.get('disableActivityHistory') === 'true'; + disableLoopingActivityCounter = + $page.url.searchParams.get('disableLoopingActivityCounter') === 'true'; + highestRatedMediaPercentage = + $page.url.searchParams.get('highestRatedMediaPercentage') === 'true'; + highestRatedGenreTagPercentage = + $page.url.searchParams.get('highestRatedGenreTagPercentage') === 'true'; + // genreTagsSort = parseInt($page.url.searchParams.get('genreTagsSort') || '0', 10); + // mediaSort = parseInt($page.url.searchParams.get('mediaSort') || '0', 10); + includeMovies = $page.url.searchParams.get('includeMovies') === 'true'; + includeOVAs = $page.url.searchParams.get('includeOVAs') === 'true'; + } - const createDummyMedia = (type: 'ANIME' | 'MANGA'): Media => ({ - id: 0, - idMal: 0, - status: 'FINISHED', - type, - episodes: type === 'ANIME' ? 0 : 0, - chapters: type === 'MANGA' ? 0 : 0, - volumes: 0, - duration: 0, - format: type === 'ANIME' ? 'TV' : 'MANGA', - title: { - romaji: '...', - english: '...', - native: '...' - }, - synonyms: [], - mediaListEntry: { - progress: 0, - progressVolumes: 0, - status: 'COMPLETED', - score: 0, - repeat: 0, - startedAt: { - year: 0, - month: 0, - day: 0 - }, - completedAt: { - year: 0, - month: 0, - day: 0 - }, - createdAt: 0, - updatedAt: 0, - customLists: {} - }, - startDate: { + await update().then(() => (mounted = true)); +}); + +const triggerFetch = () => { + shouldFetchData = true; + needsRefetch = false; + dataFetched = true; + fetchKey += 1; + lastSelectedYear = selectedYear; + lastUseFullActivityHistory = useFullActivityHistory; + lastDisableLoopingActivityCounter = disableLoopingActivityCounter; + lastStartDateFilter = startDateFilter; + lastEndDateFilter = endDateFilter; +}; + +const createDummyMedia = (type: 'ANIME' | 'MANGA'): Media => ({ + id: 0, + idMal: 0, + status: 'FINISHED', + type, + episodes: type === 'ANIME' ? 0 : 0, + chapters: type === 'MANGA' ? 0 : 0, + volumes: 0, + duration: 0, + format: type === 'ANIME' ? 'TV' : 'MANGA', + title: { + romaji: '...', + english: '...', + native: '...' + }, + synonyms: [], + mediaListEntry: { + progress: 0, + progressVolumes: 0, + status: 'COMPLETED', + score: 0, + repeat: 0, + startedAt: { year: 0, - month: 0 + month: 0, + day: 0 }, - endDate: { + completedAt: { year: 0, - month: 0 + month: 0, + day: 0 }, - coverImage: { - extraLarge: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg', - medium: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg' + createdAt: 0, + updatedAt: 0, + customLists: {} + }, + startDate: { + year: 0, + month: 0 + }, + endDate: { + year: 0, + month: 0 + }, + coverImage: { + extraLarge: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg', + medium: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg' + }, + tags: [], + genres: [], + season: 'WINTER', + isAdult: false, + relations: { + edges: [] + } +}); + +const dummyWrapped: Wrapped = { + statistics: { + anime: { + startYears: [], + genres: [], + tags: [] }, - tags: [], - genres: [], - season: 'WINTER', - isAdult: false, - relations: { - edges: [] + manga: { + startYears: [], + genres: [], + tags: [] } - }); - - const dummyWrapped: Wrapped = { - statistics: { - anime: { - startYears: [], - genres: [], - tags: [] - }, - manga: { - startYears: [], - genres: [], - tags: [] - } - }, - activities: { - statusCount: 0, - messageCount: 0 - }, - avatar: { - large: 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/3.jpg' + }, + activities: { + statusCount: 0, + messageCount: 0 + }, + avatar: { + large: 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/3.jpg' + } +}; + +const dummyActivities: ActivityHistoryEntry[] = []; +const dummyAnimeList: Media[] = [createDummyMedia('ANIME')]; +const dummyMangaList: Media[] = [createDummyMedia('MANGA')]; + +const update = async () => { + if ($userIdentity.id === -1) return; + + let rawAnimeList = await mediaListCollection( + user, + $userIdentity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + forcePrune: dateTicked ? false : true, + includeCompleted: true, + all: true } - }; - - const dummyActivities: ActivityHistoryEntry[] = []; - const dummyAnimeList: Media[] = [createDummyMedia('ANIME')]; - const dummyMangaList: Media[] = [createDummyMedia('MANGA')]; - - const update = async () => { - if ($userIdentity.id === -1) return; - - let rawAnimeList = await mediaListCollection( - user, - $userIdentity, - Type.Anime, - $anime, - $lastPruneTimes.anime, - { - forcePrune: dateTicked ? false : true, - includeCompleted: true, - all: true - } - ); - calculatedAnimeList = rawAnimeList - .filter( - (item, index, self) => - self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && - (includeMusic ? true : item.format !== 'MUSIC') && - (includeRepeats + ); + calculatedAnimeList = rawAnimeList + .filter( + (item, index, self) => + self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && + (includeMusic ? true : item.format !== 'MUSIC') && + (includeRepeats + ? true + : item.startDate.year === selectedYear || item.endDate.year === selectedYear ? true - : item.startDate.year === selectedYear || item.endDate.year === selectedYear - ? true - : item.mediaListEntry?.repeat === 0) && - (item.mediaListEntry?.startedAt.year === selectedYear || - item.mediaListEntry?.completedAt.year === selectedYear || - ((item.mediaListEntry?.createdAt - ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.progress >= 1 - : false)) && - (includeMovies ? true : item.format !== 'MOVIE') && - (includeSpecials ? true : item.format !== 'SPECIAL') && - (includeOVAs ? true : item.format !== 'OVA') && - (excludeUnratedUnwatched ? item.mediaListEntry?.score !== 0 : true) && - (excludeUnratedUnwatched ? item.mediaListEntry?.progress !== 0 : true) && - (startDateFilter && item.mediaListEntry?.startedAt - ? new Date( - item.mediaListEntry.startedAt.year, - item.mediaListEntry?.startedAt.month - 1, - item.mediaListEntry.startedAt.day - ) >= new Date(startDateFilter) - : true) && - (endDateFilter && item.mediaListEntry?.startedAt - ? new Date( - item.mediaListEntry.completedAt.year, - item.mediaListEntry?.completedAt.month - 1, - item.mediaListEntry.completedAt.day - ) <= new Date(endDateFilter) - : true) - ) - .sort((a, b) => { - switch (mediaSort) { - case SortOptions.MINUTES_WATCHED: - if (a.duration === undefined || a.mediaListEntry?.progress === undefined) return 1; - else if (b.duration === undefined || b.mediaListEntry?.progress === undefined) - return -1; - else - return ( - b.duration * b.mediaListEntry.progress - a.duration * a.mediaListEntry.progress - ); - case SortOptions.SCORE: - default: - if (a.mediaListEntry?.score === undefined) return 1; - else if (b.mediaListEntry?.score === undefined) return -1; - else return b.mediaListEntry?.score - a.mediaListEntry?.score; - } - }); - - animeList = rawAnimeList - .filter( - (item, index, self) => - self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && - (includeMusic ? true : item.format !== 'MUSIC') && - (includeRepeats + : item.mediaListEntry?.repeat === 0) && + (item.mediaListEntry?.startedAt.year === selectedYear || + item.mediaListEntry?.completedAt.year === selectedYear || + ((item.mediaListEntry?.createdAt + ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.progress >= 1 + : false)) && + (includeMovies ? true : item.format !== 'MOVIE') && + (includeSpecials ? true : item.format !== 'SPECIAL') && + (includeOVAs ? true : item.format !== 'OVA') && + (excludeUnratedUnwatched ? item.mediaListEntry?.score !== 0 : true) && + (excludeUnratedUnwatched ? item.mediaListEntry?.progress !== 0 : true) && + (startDateFilter && item.mediaListEntry?.startedAt + ? new Date( + item.mediaListEntry.startedAt.year, + item.mediaListEntry?.startedAt.month - 1, + item.mediaListEntry.startedAt.day + ) >= new Date(startDateFilter) + : true) && + (endDateFilter && item.mediaListEntry?.startedAt + ? new Date( + item.mediaListEntry.completedAt.year, + item.mediaListEntry?.completedAt.month - 1, + item.mediaListEntry.completedAt.day + ) <= new Date(endDateFilter) + : true) + ) + .sort((a, b) => { + switch (mediaSort) { + case SortOptions.MINUTES_WATCHED: + if (a.duration === undefined || a.mediaListEntry?.progress === undefined) return 1; + else if (b.duration === undefined || b.mediaListEntry?.progress === undefined) return -1; + else + return b.duration * b.mediaListEntry.progress - a.duration * a.mediaListEntry.progress; + case SortOptions.SCORE: + default: + if (a.mediaListEntry?.score === undefined) return 1; + else if (b.mediaListEntry?.score === undefined) return -1; + else return b.mediaListEntry?.score - a.mediaListEntry?.score; + } + }); + + animeList = rawAnimeList + .filter( + (item, index, self) => + self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && + (includeMusic ? true : item.format !== 'MUSIC') && + (includeRepeats + ? true + : item.startDate.year === selectedYear || item.endDate.year === selectedYear ? true - : item.startDate.year === selectedYear || item.endDate.year === selectedYear - ? true - : item.mediaListEntry?.repeat === 0) && - (item.mediaListEntry?.startedAt.year === selectedYear || - item.mediaListEntry?.completedAt.year === selectedYear || - ((item.mediaListEntry?.createdAt - ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.progress >= 1 - : false) || - (includeOngoingMediaFromPreviousYears - ? (item.mediaListEntry?.updatedAt - ? new Date(item.mediaListEntry?.updatedAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.status === 'CURRENT' - : false - : false)) && - (includeMovies ? true : item.format !== 'MOVIE') && - (includeSpecials ? true : item.format !== 'SPECIAL') && - (includeOVAs ? true : item.format !== 'OVA') - ) - .sort((a, b) => { - switch (mediaSort) { - case SortOptions.MINUTES_WATCHED: - if (a.duration === undefined || a.mediaListEntry?.progress === undefined) return 1; - else if (b.duration === undefined || b.mediaListEntry?.progress === undefined) - return -1; - else - return ( - b.duration * b.mediaListEntry.progress - a.duration * a.mediaListEntry.progress - ); - case SortOptions.SCORE: - default: - if (a.mediaListEntry?.score === undefined) return 1; - else if (b.mediaListEntry?.score === undefined) return -1; - else return b.mediaListEntry?.score - a.mediaListEntry?.score; - } - }); - - let rawMangaList = await mediaListCollection( - user, - $userIdentity, - Type.Manga, - $manga, - $lastPruneTimes.manga, - { - forcePrune: dateTicked ? false : true, - includeCompleted: true, - all: true + : item.mediaListEntry?.repeat === 0) && + (item.mediaListEntry?.startedAt.year === selectedYear || + item.mediaListEntry?.completedAt.year === selectedYear || + ((item.mediaListEntry?.createdAt + ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.progress >= 1 + : false) || + (includeOngoingMediaFromPreviousYears + ? (item.mediaListEntry?.updatedAt + ? new Date(item.mediaListEntry?.updatedAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.status === 'CURRENT' + : false + : false)) && + (includeMovies ? true : item.format !== 'MOVIE') && + (includeSpecials ? true : item.format !== 'SPECIAL') && + (includeOVAs ? true : item.format !== 'OVA') + ) + .sort((a, b) => { + switch (mediaSort) { + case SortOptions.MINUTES_WATCHED: + if (a.duration === undefined || a.mediaListEntry?.progress === undefined) return 1; + else if (b.duration === undefined || b.mediaListEntry?.progress === undefined) return -1; + else + return b.duration * b.mediaListEntry.progress - a.duration * a.mediaListEntry.progress; + case SortOptions.SCORE: + default: + if (a.mediaListEntry?.score === undefined) return 1; + else if (b.mediaListEntry?.score === undefined) return -1; + else return b.mediaListEntry?.score - a.mediaListEntry?.score; } - ); - calculatedMangaList = rawMangaList - .filter( - (item, index, self) => - self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && - (includeRepeats ? true : item.mediaListEntry?.repeat === 0) && - (item.mediaListEntry?.startedAt.year === selectedYear || - item.mediaListEntry?.completedAt.year === selectedYear || - ((item.mediaListEntry?.createdAt - ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.progress >= 1 - : false)) && - (excludeUnratedUnwatched ? item.mediaListEntry?.score !== 0 : true) && - (excludeUnratedUnwatched ? item.mediaListEntry?.progress !== 0 : true) && - (startDateFilter && item.mediaListEntry?.startedAt - ? new Date( - item.mediaListEntry.startedAt.year, - item.mediaListEntry?.startedAt.month - 1, - item.mediaListEntry.startedAt.day - ) >= new Date(startDateFilter) - : true) && - (endDateFilter && item.mediaListEntry?.startedAt - ? new Date( - item.mediaListEntry.completedAt.year, - item.mediaListEntry?.completedAt.month - 1, - item.mediaListEntry.completedAt.day - ) <= new Date(endDateFilter) - : true) - ) - .sort((a, b) => { - if (a.mediaListEntry?.score === undefined) return 1; - else if (b.mediaListEntry?.score === undefined) return -1; - else return b.mediaListEntry?.score - a.mediaListEntry?.score; - }); - - mangaList = rawMangaList - .filter( - (item, index, self) => - self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && - (includeRepeats ? true : item.mediaListEntry?.repeat === 0) && - (item.mediaListEntry?.startedAt.year === selectedYear || - item.mediaListEntry?.completedAt.year === selectedYear || - ((item.mediaListEntry?.createdAt - ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.progress >= 1 - : false) || - (includeOngoingMediaFromPreviousYears - ? (item.mediaListEntry?.updatedAt - ? new Date(item.mediaListEntry?.updatedAt * 1000).getFullYear() === selectedYear - : false) && item.mediaListEntry - ? item.mediaListEntry?.status === 'CURRENT' - : false - : false)) - ) - .sort((a, b) => { - if (a.mediaListEntry?.score === undefined) return 1; - else if (b.mediaListEntry?.score === undefined) return -1; - else return b.mediaListEntry?.score - a.mediaListEntry?.score; - }); - - episodes = 0; - minutesWatched = 0; - chapters = 0; - dateTicked = false; - - for (const media of calculatedAnimeList) { - episodes += media.mediaListEntry?.progress || 0; - minutesWatched += (media.mediaListEntry?.progress || 0) * media.duration || 0; + }); + + let rawMangaList = await mediaListCollection( + user, + $userIdentity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + forcePrune: dateTicked ? false : true, + includeCompleted: true, + all: true } + ); + calculatedMangaList = rawMangaList + .filter( + (item, index, self) => + self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && + (includeRepeats ? true : item.mediaListEntry?.repeat === 0) && + (item.mediaListEntry?.startedAt.year === selectedYear || + item.mediaListEntry?.completedAt.year === selectedYear || + ((item.mediaListEntry?.createdAt + ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.progress >= 1 + : false)) && + (excludeUnratedUnwatched ? item.mediaListEntry?.score !== 0 : true) && + (excludeUnratedUnwatched ? item.mediaListEntry?.progress !== 0 : true) && + (startDateFilter && item.mediaListEntry?.startedAt + ? new Date( + item.mediaListEntry.startedAt.year, + item.mediaListEntry?.startedAt.month - 1, + item.mediaListEntry.startedAt.day + ) >= new Date(startDateFilter) + : true) && + (endDateFilter && item.mediaListEntry?.startedAt + ? new Date( + item.mediaListEntry.completedAt.year, + item.mediaListEntry?.completedAt.month - 1, + item.mediaListEntry.completedAt.day + ) <= new Date(endDateFilter) + : true) + ) + .sort((a, b) => { + if (a.mediaListEntry?.score === undefined) return 1; + else if (b.mediaListEntry?.score === undefined) return -1; + else return b.mediaListEntry?.score - a.mediaListEntry?.score; + }); - for (const media of calculatedMangaList) chapters += media.mediaListEntry?.progress || 0; - }; + mangaList = rawMangaList + .filter( + (item, index, self) => + self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index && + (includeRepeats ? true : item.mediaListEntry?.repeat === 0) && + (item.mediaListEntry?.startedAt.year === selectedYear || + item.mediaListEntry?.completedAt.year === selectedYear || + ((item.mediaListEntry?.createdAt + ? new Date(item.mediaListEntry?.createdAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.progress >= 1 + : false) || + (includeOngoingMediaFromPreviousYears + ? (item.mediaListEntry?.updatedAt + ? new Date(item.mediaListEntry?.updatedAt * 1000).getFullYear() === selectedYear + : false) && item.mediaListEntry + ? item.mediaListEntry?.status === 'CURRENT' + : false + : false)) + ) + .sort((a, b) => { + if (a.mediaListEntry?.score === undefined) return 1; + else if (b.mediaListEntry?.score === undefined) return -1; + else return b.mediaListEntry?.score - a.mediaListEntry?.score; + }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - // const year = (statistic: { startYears: any }) => - // statistic.startYears.find((y: { startYear: number }) => y.startYear === 2023); - - const screenshot = async () => { - let element = document.querySelector('#wrapped') as HTMLElement; - - if (element !== null) { - domToBlob(element, { - backgroundColor: transparency ? 'transparent' : lightTheme ? '#edf1f5' : '#0b1622', - quality: 1, - scale: 2, - fetch: { - requestInit: { - mode: 'cors' - }, - bypassingCache: true - } - }).then((blob) => { - const downloadWrapper = document.createElement('a'); - // const wrappedImageButton = document.getElementById( - // 'wrapped-image-download' - // ) as HTMLAnchorElement; - const image = document.createElement('img'); - const object = (window.URL || window.webkitURL || window || {}).createObjectURL(blob); - - // downloadWrapper.download = `due_dot_moe_wrapped_${dark ? 'dark' : 'light'}.png`; - downloadWrapper.href = object; - downloadWrapper.target = '_blank'; - image.src = object; - - downloadWrapper.appendChild(image); - - // if (wrappedImageButton !== null) { - // wrappedImageButton.href = object; - // } - - const wrappedFinal = document.getElementById('wrapped-final'); - - if (wrappedFinal !== null) { - wrappedFinal.innerHTML = ''; - - wrappedFinal.appendChild(downloadWrapper); - - generated = true; - } - - downloadWrapper.click(); - }); - } - }; + episodes = 0; + minutesWatched = 0; + chapters = 0; + dateTicked = false; - // const abbreviate = (string: string, maxLength = 40, enabled = true) => { - // if (!enabled) { - // return string; - // } - - // if (string.length <= maxLength) { - // return string; - // } - - // return string.slice(0, maxLength - 3) + ' …'; - // }; - - const submitExcludedKeywords = () => { - if (excludedKeywordsInput.length <= 0 && excludedKeywords.length > 0) { - animeList = originalAnimeList; - mangaList = originalMangaList; - excludedKeywords = []; - } else if (excludedKeywordsInput.length >= 0 && excludedKeywords.length <= 0) { - originalAnimeList = animeList; - originalMangaList = mangaList; - } + for (const media of calculatedAnimeList) { + episodes += media.mediaListEntry?.progress || 0; + minutesWatched += (media.mediaListEntry?.progress || 0) * media.duration || 0; + } - if (excludedKeywordsInput.length > 0) - excludedKeywords = excludedKeywordsInput - .split(',') - .map((k) => k.trim()) - .filter((k) => k.length > 0); - }; + for (const media of calculatedMangaList) chapters += media.mediaListEntry?.progress || 0; +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// const year = (statistic: { startYears: any }) => +// statistic.startYears.find((y: { startYear: number }) => y.startYear === 2023); + +const screenshot = async () => { + let element = document.querySelector('#wrapped') as HTMLElement; + + if (element !== null) { + domToBlob(element, { + backgroundColor: transparency ? 'transparent' : lightTheme ? '#edf1f5' : '#0b1622', + quality: 1, + scale: 2, + fetch: { + requestInit: { + mode: 'cors' + }, + bypassingCache: true + } + }).then((blob) => { + const downloadWrapper = document.createElement('a'); + // const wrappedImageButton = document.getElementById( + // 'wrapped-image-download' + // ) as HTMLAnchorElement; + const image = document.createElement('img'); + const object = (window.URL || window.webkitURL || window || {}).createObjectURL(blob); + + // downloadWrapper.download = `due_dot_moe_wrapped_${dark ? 'dark' : 'light'}.png`; + downloadWrapper.href = object; + downloadWrapper.target = '_blank'; + image.src = object; + + downloadWrapper.appendChild(image); - const excludeKeywords = (media: Media[]) => { - if (excludedKeywords.length <= 0) return media; + // if (wrappedImageButton !== null) { + // wrappedImageButton.href = object; + // } - return media.filter((m) => { - for (const keyword of excludedKeywords) { - if (m.title.english?.toLowerCase().includes(keyword.toLowerCase())) return false; - if (m.title.romaji?.toLowerCase().includes(keyword.toLowerCase())) return false; - if (m.title.native?.toLowerCase().includes(keyword.toLowerCase())) return false; + const wrappedFinal = document.getElementById('wrapped-final'); + + if (wrappedFinal !== null) { + wrappedFinal.innerHTML = ''; + + wrappedFinal.appendChild(downloadWrapper); + + generated = true; } - return true; + downloadWrapper.click(); }); - }; + } +}; + +// const abbreviate = (string: string, maxLength = 40, enabled = true) => { +// if (!enabled) { +// return string; +// } + +// if (string.length <= maxLength) { +// return string; +// } + +// return string.slice(0, maxLength - 3) + ' …'; +// }; + +const submitExcludedKeywords = () => { + if (excludedKeywordsInput.length <= 0 && excludedKeywords.length > 0) { + animeList = originalAnimeList; + mangaList = originalMangaList; + excludedKeywords = []; + } else if (excludedKeywordsInput.length >= 0 && excludedKeywords.length <= 0) { + originalAnimeList = animeList; + originalMangaList = mangaList; + } - const pruneFullYear = async () => { - await database.activities.bulkDelete((await database.activities.toArray()).map((m) => m.page)); - }; + if (excludedKeywordsInput.length > 0) + excludedKeywords = excludedKeywordsInput + .split(',') + .map((k) => k.trim()) + .filter((k) => k.length > 0); +}; + +const excludeKeywords = (media: Media[]) => { + if (excludedKeywords.length <= 0) return media; + + return media.filter((m) => { + for (const keyword of excludedKeywords) { + if (m.title.english?.toLowerCase().includes(keyword.toLowerCase())) return false; + if (m.title.romaji?.toLowerCase().includes(keyword.toLowerCase())) return false; + if (m.title.native?.toLowerCase().includes(keyword.toLowerCase())) return false; + } - // const mergeArraySort = (a: any, b: any, mode: 'tags' | 'genres') => { - // let merged = [...a, ...b].sort((a, b) => b.meanScore - a.meanScore); - - // merged = merged.filter( - // (item, index, self) => - // self.findIndex((itemToCompare) => - // mode === 'genres' - // ? itemToCompare.genre === item.genre - // : itemToCompare.tag.name === item.tag.name - // ) === index - // ); - - // return merged; - // }; - - // const randomCoverFromTop10 = ( - // statistics: { anime: any; manga: any }, - // mode: 'tags' | 'genres' - // ) => { - // const top = mergeArraySort(statistics.anime[mode], statistics.manga[mode], mode); - - // return mediaCover(top[Math.floor(Math.random() * top.length)].mediaIds[0]); - // }; + return true; + }); +}; + +const pruneFullYear = async () => { + await database.activities.bulkDelete((await database.activities.toArray()).map((m) => m.page)); +}; + +// const mergeArraySort = (a: any, b: any, mode: 'tags' | 'genres') => { +// let merged = [...a, ...b].sort((a, b) => b.meanScore - a.meanScore); + +// merged = merged.filter( +// (item, index, self) => +// self.findIndex((itemToCompare) => +// mode === 'genres' +// ? itemToCompare.genre === item.genre +// : itemToCompare.tag.name === item.tag.name +// ) === index +// ); + +// return merged; +// }; + +// const randomCoverFromTop10 = ( +// statistics: { anime: any; manga: any }, +// mode: 'tags' | 'genres' +// ) => { +// const top = mergeArraySort(statistics.anime[mode], statistics.manga[mode], mode); + +// return mediaCover(top[Math.floor(Math.random() * top.length)].mediaIds[0]); +// }; </script> {#if $userIdentity.id === -2 || user === undefined} diff --git a/src/lib/Tools/Wrapped/Top/Activity.svelte b/src/lib/Tools/Wrapped/Top/Activity.svelte index ea6ba592..fdfd90e0 100644 --- a/src/lib/Tools/Wrapped/Top/Activity.svelte +++ b/src/lib/Tools/Wrapped/Top/Activity.svelte @@ -1,16 +1,16 @@ <script lang="ts"> - import type { ActivityHistoryEntry } from '$lib/Data/AniList/activity'; - import identity from '$stores/identity'; - import type { Wrapped } from '$lib/Data/AniList/wrapped'; - import proxy from '$lib/Utility/proxy'; +import type { ActivityHistoryEntry } from '$lib/Data/AniList/activity'; +import identity from '$stores/identity'; +import type { Wrapped } from '$lib/Data/AniList/wrapped'; +import proxy from '$lib/Utility/proxy'; - export let wrapped: Wrapped; - export let year: number; - export let activities: ActivityHistoryEntry[]; - export let useFullActivityHistory: boolean; - export let updateWidth: () => void; +export let wrapped: Wrapped; +export let year: number; +export let activities: ActivityHistoryEntry[]; +export let useFullActivityHistory: boolean; +export let updateWidth: () => void; - const currentYear = new Date(Date.now()).getFullYear(); +const currentYear = new Date(Date.now()).getFullYear(); </script> <div class="grid-item image-grid avatar-grid category top-category"> diff --git a/src/lib/Tools/Wrapped/Top/Anime.svelte b/src/lib/Tools/Wrapped/Top/Anime.svelte index 08df7fd3..6caf4b8a 100644 --- a/src/lib/Tools/Wrapped/Top/Anime.svelte +++ b/src/lib/Tools/Wrapped/Top/Anime.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; +import type { Media } from '$lib/Data/AniList/media'; - export let minutesWatched: number; - export let animeList: Media[] | undefined; - export let episodes: number; +export let minutesWatched: number; +export let animeList: Media[] | undefined; +export let episodes: number; </script> <div class="category-grid pure-category category top-category"> diff --git a/src/lib/Tools/Wrapped/Top/Manga.svelte b/src/lib/Tools/Wrapped/Top/Manga.svelte index a36f7724..9fa4ab00 100644 --- a/src/lib/Tools/Wrapped/Top/Manga.svelte +++ b/src/lib/Tools/Wrapped/Top/Manga.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; - import { estimatedDayReading } from '$lib/Media/Manga/time'; +import type { Media } from '$lib/Data/AniList/media'; +import { estimatedDayReading } from '$lib/Media/Manga/time'; - export let mangaList: Media[] | undefined; - export let chapters: number; +export let mangaList: Media[] | undefined; +export let chapters: number; </script> <div class="category-grid pure-category category top-category"> diff --git a/src/lib/Tooltip/LinkedTooltip.svelte b/src/lib/Tooltip/LinkedTooltip.svelte index 290dbab3..4c90d9af 100644 --- a/src/lib/Tooltip/LinkedTooltip.svelte +++ b/src/lib/Tooltip/LinkedTooltip.svelte @@ -1,203 +1,199 @@ <script lang="ts"> - import tooltipPosition from '$stores/tooltipPosition'; - import { fade } from 'svelte/transition'; - - export let id: string | undefined = undefined; - export let pin: string | undefined = undefined; - export let content: string; - export let disable = false; - export let pinPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'; - export let offset = 10; - export let tooltipTransitionTime = 200; - export let tooltipHideDelay = 10; - export let debounceDelay = 100; - export let tooltipOpacityTransitionTime = 200; - export let relative = false; - export let ignoreAnchorStyling = false; - - let tooltipDiv: HTMLDivElement | null = null; - let hideTimeout: number | null = null; - let debounceTimer: number | null = null; - let opacity = 0; - - const createTooltip = () => { - if (!tooltipDiv) { - tooltipDiv = document.createElement('div'); - tooltipDiv.style.position = 'absolute'; - tooltipDiv.style.zIndex = '1000'; - opacity = 0; - tooltipDiv.style.pointerEvents = 'none'; - tooltipDiv.style.whiteSpace = 'nowrap'; - - tooltipDiv.classList.add('card'); - tooltipDiv.classList.add('card-small'); - document.body.appendChild(tooltipDiv); - } - }; - - const updateTooltipPosition = (x: number, y: number) => { - if (tooltipDiv) { - if (pin) { - const pinnedElement = document.getElementById(pin); - - if (pinnedElement) { - const rectangle = pinnedElement.getBoundingClientRect(); - const parentRectangle = pinnedElement.offsetParent?.getBoundingClientRect(); - const tooltipWidth = tooltipDiv.offsetWidth; - const tooltipHeight = tooltipDiv.offsetHeight; - let top = 0; - let left = 0; - - switch (pinPosition) { - case 'top': - if (relative && parentRectangle) { - top = rectangle.top - tooltipHeight - offset - parentRectangle.top; - left = - rectangle.left + rectangle.width / 2 - tooltipWidth / 2 - parentRectangle.left; - } else { - top = rectangle.top - tooltipHeight - offset; - left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2; - } - - break; - case 'bottom': - if (relative && parentRectangle) { - top = rectangle.top + rectangle.height + offset - parentRectangle.top; - left = - rectangle.left + rectangle.width / 2 - tooltipWidth / 2 - parentRectangle.left; - } else { - top = rectangle.top + rectangle.height + offset; - left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2; - } - - break; - case 'left': - if (relative && parentRectangle) { - top = - rectangle.top + rectangle.height / 2 - tooltipHeight / 2 - parentRectangle.top; - left = rectangle.left - tooltipWidth - offset - parentRectangle.left; - } else { - top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2; - left = rectangle.left - tooltipWidth - offset; - } - - break; - case 'right': - if (relative && parentRectangle) { - top = - rectangle.top + rectangle.height / 2 - tooltipHeight / 2 - parentRectangle.top; - left = rectangle.left + rectangle.width + offset - parentRectangle.left; - } else { - top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2; - left = rectangle.left + rectangle.width + offset; - } - - break; - } - - if (relative && parentRectangle) { - if (left + parentRectangle.left < 0) left = offset - parentRectangle.left; - if (left + tooltipWidth + parentRectangle.left > window.innerWidth) - left = window.innerWidth - tooltipWidth - offset - parentRectangle.left; - if (top + parentRectangle.top < 0) +import tooltipPosition from '$stores/tooltipPosition'; +import { fade } from 'svelte/transition'; + +export let id: string | undefined = undefined; +export let pin: string | undefined = undefined; +export let content: string; +export let disable = false; +export let pinPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'; +export let offset = 10; +export let tooltipTransitionTime = 200; +export let tooltipHideDelay = 10; +export let debounceDelay = 100; +export let tooltipOpacityTransitionTime = 200; +export let relative = false; +export let ignoreAnchorStyling = false; + +let tooltipDiv: HTMLDivElement | null = null; +let hideTimeout: number | null = null; +let debounceTimer: number | null = null; +let opacity = 0; + +const createTooltip = () => { + if (!tooltipDiv) { + tooltipDiv = document.createElement('div'); + tooltipDiv.style.position = 'absolute'; + tooltipDiv.style.zIndex = '1000'; + opacity = 0; + tooltipDiv.style.pointerEvents = 'none'; + tooltipDiv.style.whiteSpace = 'nowrap'; + + tooltipDiv.classList.add('card'); + tooltipDiv.classList.add('card-small'); + document.body.appendChild(tooltipDiv); + } +}; + +const updateTooltipPosition = (x: number, y: number) => { + if (tooltipDiv) { + if (pin) { + const pinnedElement = document.getElementById(pin); + + if (pinnedElement) { + const rectangle = pinnedElement.getBoundingClientRect(); + const parentRectangle = pinnedElement.offsetParent?.getBoundingClientRect(); + const tooltipWidth = tooltipDiv.offsetWidth; + const tooltipHeight = tooltipDiv.offsetHeight; + let top = 0; + let left = 0; + + switch (pinPosition) { + case 'top': + if (relative && parentRectangle) { + top = rectangle.top - tooltipHeight - offset - parentRectangle.top; + left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2 - parentRectangle.left; + } else { + top = rectangle.top - tooltipHeight - offset; + left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2; + } + + break; + case 'bottom': + if (relative && parentRectangle) { top = rectangle.top + rectangle.height + offset - parentRectangle.top; - if (top + tooltipHeight + parentRectangle.top > window.innerHeight) - top = window.innerHeight - tooltipHeight - offset - parentRectangle.top; - } else { - if (left < 0) left = offset; - if (left + tooltipWidth > window.innerWidth) - left = window.innerWidth - tooltipWidth - offset; - if (top < 0) top = rectangle.top + rectangle.height + offset; - if (top + tooltipHeight > window.innerHeight) - top = window.innerHeight - tooltipHeight - offset; - } - - tooltipPosition.set({ - x: left, - y: top + (relative ? 0 : window.scrollY) - }); - - return; + left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2 - parentRectangle.left; + } else { + top = rectangle.top + rectangle.height + offset; + left = rectangle.left + rectangle.width / 2 - tooltipWidth / 2; + } + + break; + case 'left': + if (relative && parentRectangle) { + top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2 - parentRectangle.top; + left = rectangle.left - tooltipWidth - offset - parentRectangle.left; + } else { + top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2; + left = rectangle.left - tooltipWidth - offset; + } + + break; + case 'right': + if (relative && parentRectangle) { + top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2 - parentRectangle.top; + left = rectangle.left + rectangle.width + offset - parentRectangle.left; + } else { + top = rectangle.top + rectangle.height / 2 - tooltipHeight / 2; + left = rectangle.left + rectangle.width + offset; + } + + break; } - } - const tooltipWidth = tooltipDiv.offsetWidth; - const tooltipHeight = tooltipDiv.offsetHeight; - let top = y - tooltipHeight - offset; - let left = x - tooltipWidth / 2; + if (relative && parentRectangle) { + if (left + parentRectangle.left < 0) left = offset - parentRectangle.left; + if (left + tooltipWidth + parentRectangle.left > window.innerWidth) + left = window.innerWidth - tooltipWidth - offset - parentRectangle.left; + if (top + parentRectangle.top < 0) + top = rectangle.top + rectangle.height + offset - parentRectangle.top; + if (top + tooltipHeight + parentRectangle.top > window.innerHeight) + top = window.innerHeight - tooltipHeight - offset - parentRectangle.top; + } else { + if (left < 0) left = offset; + if (left + tooltipWidth > window.innerWidth) + left = window.innerWidth - tooltipWidth - offset; + if (top < 0) top = rectangle.top + rectangle.height + offset; + if (top + tooltipHeight > window.innerHeight) + top = window.innerHeight - tooltipHeight - offset; + } - if (left < 0) left = offset; - if (left + tooltipWidth > window.innerWidth) left = window.innerWidth - tooltipWidth - offset; - if (top < 0) top = y + offset; + tooltipPosition.set({ + x: left, + y: top + (relative ? 0 : window.scrollY) + }); - tooltipPosition.set({ - x: left, - y: top - }); + return; + } } - }; - const showTooltip = (content: string, x: number, y: number) => { - if (hideTimeout !== null) { - clearTimeout(hideTimeout); + const tooltipWidth = tooltipDiv.offsetWidth; + const tooltipHeight = tooltipDiv.offsetHeight; + let top = y - tooltipHeight - offset; + let left = x - tooltipWidth / 2; - hideTimeout = null; - } + if (left < 0) left = offset; + if (left + tooltipWidth > window.innerWidth) left = window.innerWidth - tooltipWidth - offset; + if (top < 0) top = y + offset; - createTooltip(); + tooltipPosition.set({ + x: left, + y: top + }); + } +}; - if (tooltipDiv) { - tooltipDiv.innerHTML = content.replace(/\n/g, '<br>'); - tooltipDiv.style.opacity = '0'; +const showTooltip = (content: string, x: number, y: number) => { + if (hideTimeout !== null) { + clearTimeout(hideTimeout); - updateTooltipPosition(x, y); - setTimeout(() => { - if (tooltipDiv) { - opacity = 1; - } - }, 10); - } - }; + hideTimeout = null; + } - const hideTooltip = () => { - setTimeout(() => { - if (tooltipDiv) { - opacity = 0; + createTooltip(); - hideTimeout = window.setTimeout(() => { - if (tooltipDiv) { - document.body.removeChild(tooltipDiv); + if (tooltipDiv) { + tooltipDiv.innerHTML = content.replace(/\n/g, '<br>'); + tooltipDiv.style.opacity = '0'; - tooltipDiv = null; - } - }, tooltipTransitionTime); + updateTooltipPosition(x, y); + setTimeout(() => { + if (tooltipDiv) { + opacity = 1; } - }, tooltipHideDelay); - }; + }, 10); + } +}; - const handleMouseEnter = (event: MouseEvent) => { - if (disable) return; +const hideTooltip = () => { + setTimeout(() => { + if (tooltipDiv) { + opacity = 0; - if (hideTimeout !== null) { - clearTimeout(hideTimeout); + hideTimeout = window.setTimeout(() => { + if (tooltipDiv) { + document.body.removeChild(tooltipDiv); - hideTimeout = null; + tooltipDiv = null; + } + }, tooltipTransitionTime); } + }, tooltipHideDelay); +}; + +const handleMouseEnter = (event: MouseEvent) => { + if (disable) return; + + if (hideTimeout !== null) { + clearTimeout(hideTimeout); + + hideTimeout = null; + } - if (!tooltipDiv) showTooltip(content, event.pageX, event.pageY); - }; + if (!tooltipDiv) showTooltip(content, event.pageX, event.pageY); +}; - const handleMouseMove = (event: MouseEvent) => { - if (debounceTimer !== null) clearTimeout(debounceTimer); +const handleMouseMove = (event: MouseEvent) => { + if (debounceTimer !== null) clearTimeout(debounceTimer); - debounceTimer = window.setTimeout(() => { - if (tooltipDiv && opacity === 1) updateTooltipPosition(event.pageX, event.pageY); - }, debounceDelay); - }; + debounceTimer = window.setTimeout(() => { + if (tooltipDiv && opacity === 1) updateTooltipPosition(event.pageX, event.pageY); + }, debounceDelay); +}; - const handleMouseLeave = () => { - hideTooltip(); - }; +const handleMouseLeave = () => { + hideTooltip(); +}; </script> <span diff --git a/src/lib/User/BadgeWall/AWC.svelte b/src/lib/User/BadgeWall/AWC.svelte index 8d340282..52eb670a 100644 --- a/src/lib/User/BadgeWall/AWC.svelte +++ b/src/lib/User/BadgeWall/AWC.svelte @@ -1,63 +1,63 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import type { AWCBadgesGroup } from '$lib/Data/awc'; - import { cdn, thumbnail } from '$lib/Utility/image'; - import type { Preferences } from '../../../graphql/$types'; - import FallbackBadge from './FallbackBadge.svelte'; - import './badges.css'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import type { AWCBadgesGroup } from '$lib/Data/awc'; +import { cdn, thumbnail } from '$lib/Utility/image'; +import type { Preferences } from '../../../graphql/$types'; +import FallbackBadge from './FallbackBadge.svelte'; +import './badges.css'; - export let awcPromise: Promise<Response>; - export let categoryFilter: string | null; - export let isOwner: boolean; - export let preferences: Preferences; +export let awcPromise: Promise<Response>; +export let categoryFilter: string | null; +export let isOwner: boolean; +export let preferences: Preferences; - const awcBadgesGrouped = (awcResponse: string): AWCBadgesGroup[] => { - return Array.from( - new DOMParser().parseFromString(awcResponse, 'text/html').querySelectorAll('.container') - ) - .map((c) => { - const container = c as HTMLDivElement; - const header = container.querySelector('.container-header') as HTMLDivElement; +const awcBadgesGrouped = (awcResponse: string): AWCBadgesGroup[] => { + return Array.from( + new DOMParser().parseFromString(awcResponse, 'text/html').querySelectorAll('.container') + ) + .map((c) => { + const container = c as HTMLDivElement; + const header = container.querySelector('.container-header') as HTMLDivElement; - if (!header) return; + if (!header) return; - if (!['Anime', 'Manga', 'Special'].includes(header.innerText)) return; - - if (header.innerText === 'Special') { - return { - group: header.innerText, - badges: Array.from(container.querySelectorAll('.badge-display img')).map((b) => { - const badge = b as HTMLImageElement; - - return { - link: '#', - description: badge.alt, - image: badge.src.includes('placeholder') - ? cdn('https://awc.moe/static/images/badge-placeholder.png') - : badge.src - }; - }) - }; - } + if (!['Anime', 'Manga', 'Special'].includes(header.innerText)) return; + if (header.innerText === 'Special') { return { group: header.innerText, - badges: Array.from(container.querySelectorAll('.badge-display a')).map((b) => { - const badge = b as HTMLAnchorElement; - const image = badge.querySelector('img') as HTMLImageElement; + badges: Array.from(container.querySelectorAll('.badge-display img')).map((b) => { + const badge = b as HTMLImageElement; return { - link: badge.href, - description: image.alt, - image: image.src.includes('placeholder') + link: '#', + description: badge.alt, + image: badge.src.includes('placeholder') ? cdn('https://awc.moe/static/images/badge-placeholder.png') - : image.src + : badge.src }; }) }; - }) - .filter((b) => b !== undefined) as AWCBadgesGroup[]; - }; + } + + return { + group: header.innerText, + badges: Array.from(container.querySelectorAll('.badge-display a')).map((b) => { + const badge = b as HTMLAnchorElement; + const image = badge.querySelector('img') as HTMLImageElement; + + return { + link: badge.href, + description: image.alt, + image: image.src.includes('placeholder') + ? cdn('https://awc.moe/static/images/badge-placeholder.png') + : image.src + }; + }) + }; + }) + .filter((b) => b !== undefined) as AWCBadgesGroup[]; +}; </script> {#await awcPromise then badges} diff --git a/src/lib/User/BadgeWall/BadgePreview.svelte b/src/lib/User/BadgeWall/BadgePreview.svelte index 062fdac7..cf0cb284 100644 --- a/src/lib/User/BadgeWall/BadgePreview.svelte +++ b/src/lib/User/BadgeWall/BadgePreview.svelte @@ -1,104 +1,104 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { thumbnail } from '$lib/Utility/image'; - import type { Badge } from '$lib/Database/SB/User/badges'; - import { cdn } from '$lib/Utility/image'; - import { databaseTimeToDate } from '$lib/Utility/time'; - import locale from '$stores/locale'; - import { onMount } from 'svelte'; - import root from '$lib/Utility/root'; - import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; - - export let selectedBadge: Badge | undefined; - export let onNext: () => void = () => undefined; - export let onPrevious: () => void = () => undefined; - export let hasNext: boolean; - export let hasPrevious: boolean; - - let source = cdn(thumbnail(selectedBadge?.image || '')) || ''; - let badgeReference: HTMLImageElement; - - $: { - if (selectedBadge && selectedBadge.image) { - const image = new Image(); - - image.src = cdn(selectedBadge.image) || ''; - image.onload = () => { - source = image.src; - }; - } - } - - $: { - if (selectedBadge) - fetch(root(`/api/badges?incrementClickCount=${selectedBadge.id}`), { - method: 'PUT' - }); +import Spacer from '$lib/Layout/Spacer.svelte'; +import { thumbnail } from '$lib/Utility/image'; +import type { Badge } from '$lib/Database/SB/User/badges'; +import { cdn } from '$lib/Utility/image'; +import { databaseTimeToDate } from '$lib/Utility/time'; +import locale from '$stores/locale'; +import { onMount } from 'svelte'; +import root from '$lib/Utility/root'; +import ParallaxImage from '$lib/Image/ParallaxImage.svelte'; + +export let selectedBadge: Badge | undefined; +export let onNext: () => void = () => undefined; +export let onPrevious: () => void = () => undefined; +export let hasNext: boolean; +export let hasPrevious: boolean; + +let source = cdn(thumbnail(selectedBadge?.image || '')) || ''; +let badgeReference: HTMLImageElement; + +$: { + if (selectedBadge && selectedBadge.image) { + const image = new Image(); + + image.src = cdn(selectedBadge.image) || ''; + image.onload = () => { + source = image.src; + }; } +} - onMount(() => { - badgeReference = document.querySelector('.badge-container-image') as HTMLImageElement; +$: { + if (selectedBadge) + fetch(root(`/api/badges?incrementClickCount=${selectedBadge.id}`), { + method: 'PUT' + }); +} - const handleClickOutside = (event: MouseEvent) => { - if ((event.target as HTMLElement).classList.contains('popup')) selectedBadge = undefined; - }; +onMount(() => { + badgeReference = document.querySelector('.badge-container-image') as HTMLImageElement; - document.addEventListener('click', handleClickOutside); + const handleClickOutside = (event: MouseEvent) => { + if ((event.target as HTMLElement).classList.contains('popup')) selectedBadge = undefined; + }; - return () => { - document.removeEventListener('click', handleClickOutside); - }; - }); - - const classifySource = (source: string) => { - let name = source; - const sourceLower = source.toLowerCase(); - - if (sourceLower.includes('pixiv.net')) { - name = 'Pixiv'; - } else if (sourceLower.includes('twitter.com') || sourceLower.includes('x.com')) { - name = 'X (Twitter)'; - } else if (sourceLower.includes('zerochan.net')) { - name = 'Zerochan'; - } else if (sourceLower.includes('imgur.com')) { - name = 'Imgur'; - } else if (sourceLower.includes('lofter.com')) { - name = 'Lofter'; - } + document.addEventListener('click', handleClickOutside); - return `<a href="${source}" target="_blank">${name}</a>`; + return () => { + document.removeEventListener('click', handleClickOutside); }; +}); + +const classifySource = (source: string) => { + let name = source; + const sourceLower = source.toLowerCase(); + + if (sourceLower.includes('pixiv.net')) { + name = 'Pixiv'; + } else if (sourceLower.includes('twitter.com') || sourceLower.includes('x.com')) { + name = 'X (Twitter)'; + } else if (sourceLower.includes('zerochan.net')) { + name = 'Zerochan'; + } else if (sourceLower.includes('imgur.com')) { + name = 'Imgur'; + } else if (sourceLower.includes('lofter.com')) { + name = 'Lofter'; + } - const classifyDesigner = (designer: string) => { - let name = designer; - let userLink = designer; - const designerLower = designer.toLowerCase(); - const anilistUser = designer.match(/https?:\/\/anilist\.co\/user\/([^/]+)\/?/); - - if (anilistUser) { - name = anilistUser[1]; - } else if (designerLower.startsWith('@')) { - name = designer.replace('@', ''); - userLink = `https://anilist.co/user/${name}/`; - } else if (!designerLower.startsWith('http')) { - userLink = `https://anilist.co/user/${name}/`; - } + return `<a href="${source}" target="_blank">${name}</a>`; +}; + +const classifyDesigner = (designer: string) => { + let name = designer; + let userLink = designer; + const designerLower = designer.toLowerCase(); + const anilistUser = designer.match(/https?:\/\/anilist\.co\/user\/([^/]+)\/?/); + + if (anilistUser) { + name = anilistUser[1]; + } else if (designerLower.startsWith('@')) { + name = designer.replace('@', ''); + userLink = `https://anilist.co/user/${name}/`; + } else if (!designerLower.startsWith('http')) { + userLink = `https://anilist.co/user/${name}/`; + } - return `<a href="${userLink}" target="_blank">@${name}</a>`; - }; + return `<a href="${userLink}" target="_blank">@${name}</a>`; +}; - const onClick = (event: MouseEvent) => { - event.preventDefault(); +const onClick = (event: MouseEvent) => { + event.preventDefault(); - if ( - event.clientX < - badgeReference.getBoundingClientRect().left + badgeReference.getBoundingClientRect().width / 2 - ) { - onPrevious(); - } else { - onNext(); - } - }; + if ( + event.clientX < + badgeReference.getBoundingClientRect().left + badgeReference.getBoundingClientRect().width / 2 + ) { + onPrevious(); + } else { + onNext(); + } +}; </script> {#if selectedBadge} diff --git a/src/lib/User/BadgeWall/Badges.svelte b/src/lib/User/BadgeWall/Badges.svelte index 99d500da..04943c64 100644 --- a/src/lib/User/BadgeWall/Badges.svelte +++ b/src/lib/User/BadgeWall/Badges.svelte @@ -1,21 +1,21 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import locale from '$stores/locale'; - import { databaseTimeToDate } from '$lib/Utility/time'; - import FallbackImage from '$lib/Image/FallbackImage.svelte'; - import { cdn, thumbnail } from '$lib/Utility/image'; - import FallbackBadge from './FallbackBadge.svelte'; - import type { Preferences } from '../../../graphql/$types'; - import type { IndexedBadge } from './badge'; +import Spacer from '$lib/Layout/Spacer.svelte'; +import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; +import tooltip from '$lib/Tooltip/tooltip'; +import locale from '$stores/locale'; +import { databaseTimeToDate } from '$lib/Utility/time'; +import FallbackImage from '$lib/Image/FallbackImage.svelte'; +import { cdn, thumbnail } from '$lib/Utility/image'; +import FallbackBadge from './FallbackBadge.svelte'; +import type { Preferences } from '../../../graphql/$types'; +import type { IndexedBadge } from './badge'; - export let ungroupedBadges: IndexedBadge[]; - export let groupedBadges: [string, IndexedBadge[]][]; - export let categoryFilter: string | null; - export let editMode: boolean; - export let preferences: Preferences | undefined; - export let selectedBadge: IndexedBadge | undefined = undefined; +export let ungroupedBadges: IndexedBadge[]; +export let groupedBadges: [string, IndexedBadge[]][]; +export let categoryFilter: string | null; +export let editMode: boolean; +export let preferences: Preferences | undefined; +export let selectedBadge: IndexedBadge | undefined = undefined; </script> {#if ungroupedBadges.length === 0} diff --git a/src/lib/User/BadgeWall/FallbackBadge.svelte b/src/lib/User/BadgeWall/FallbackBadge.svelte index bda93e4e..86e90939 100644 --- a/src/lib/User/BadgeWall/FallbackBadge.svelte +++ b/src/lib/User/BadgeWall/FallbackBadge.svelte @@ -1,70 +1,70 @@ <script lang="ts"> - import { classifyDesignerName } from './badge'; - import locale from '$stores/locale'; - import { tweened } from 'svelte/motion'; - import type { Badge } from '../../Database/SB/User/badges'; - import type { AWCBadge } from '../../Data/awc'; - import Tooltip from '../../Tooltip/LinkedTooltip.svelte'; - import { databaseTimeToDate } from '../../Utility/time'; - import { cubicOut } from 'svelte/easing'; - import { dev } from '$app/environment'; - import type { Preferences } from '../../../graphql/$types'; +import { classifyDesignerName } from './badge'; +import locale from '$stores/locale'; +import { tweened } from 'svelte/motion'; +import type { Badge } from '../../Database/SB/User/badges'; +import type { AWCBadge } from '../../Data/awc'; +import Tooltip from '../../Tooltip/LinkedTooltip.svelte'; +import { databaseTimeToDate } from '../../Utility/time'; +import { cubicOut } from 'svelte/easing'; +import { dev } from '$app/environment'; +import type { Preferences } from '../../../graphql/$types'; - export let source: string | null | undefined; - export let alternative: string | null | undefined; - export let fallback: string | null | undefined; - export let maxReplaceCount = 1; - export let replaceDelay = 1000; - export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; - export let hideOnError = false; - export let badge: Badge | AWCBadge; - export let style = ''; - export let selectedBadge: Badge | null = null; - export let awc = false; - export let index: number | null = null; - export let preferences: Preferences | undefined; +export let source: string | null | undefined; +export let alternative: string | null | undefined; +export let fallback: string | null | undefined; +export let maxReplaceCount = 1; +export let replaceDelay = 1000; +export let error = 'https://i2.kym-cdn.com/photos/images/newsfeed/000/290/992/0aa.jpg'; +export let hideOnError = false; +export let badge: Badge | AWCBadge; +export let style = ''; +export let selectedBadge: Badge | null = null; +export let awc = false; +export let index: number | null = null; +export let preferences: Preferences | undefined; - let replaceCount = 0; - let badgeReference: HTMLImageElement; - const mouse = tweened( - { x: 0, y: 0 }, - { - duration: 75, - easing: cubicOut - } - ); +let replaceCount = 0; +let badgeReference: HTMLImageElement; +const mouse = tweened( + { x: 0, y: 0 }, + { + duration: 75, + easing: cubicOut + } +); - const delayedReplace = (event: Event, image: string | undefined | null) => { - if (replaceCount >= maxReplaceCount) return; +const delayedReplace = (event: Event, image: string | undefined | null) => { + if (replaceCount >= maxReplaceCount) return; - setTimeout(() => { - (event.target as HTMLImageElement).src = image || ''; + setTimeout(() => { + (event.target as HTMLImageElement).src = image || ''; - replaceCount += 1; - }, replaceDelay); - }; + replaceCount += 1; + }, replaceDelay); +}; - const handleMouseMove = (event: MouseEvent) => { - const boundingRectangle = badgeReference.getBoundingClientRect(); - const factor = 1.25; - const limit = 50; +const handleMouseMove = (event: MouseEvent) => { + const boundingRectangle = badgeReference.getBoundingClientRect(); + const factor = 1.25; + const limit = 50; - if ($mouse.x === 0 && $mouse.y === 0) $mouse = { x: event.clientX, y: event.clientY }; + if ($mouse.x === 0 && $mouse.y === 0) $mouse = { x: event.clientX, y: event.clientY }; - $mouse.x += - (-(event.clientX - boundingRectangle.left - boundingRectangle.width / 2) - $mouse.x) * factor; - $mouse.y += - (-(event.clientY - boundingRectangle.top - boundingRectangle.height / 2) - $mouse.y) * factor; - $mouse.x = Math.max(Math.min($mouse.x, limit), -limit); - $mouse.y = Math.max(Math.min($mouse.y, limit), -limit); - }; + $mouse.x += + (-(event.clientX - boundingRectangle.left - boundingRectangle.width / 2) - $mouse.x) * factor; + $mouse.y += + (-(event.clientY - boundingRectangle.top - boundingRectangle.height / 2) - $mouse.y) * factor; + $mouse.x = Math.max(Math.min($mouse.x, limit), -limit); + $mouse.y = Math.max(Math.min($mouse.y, limit), -limit); +}; - const handleMouseLeave = () => { - $mouse = { x: 0, y: 0 }; - }; +const handleMouseLeave = () => { + $mouse = { x: 0, y: 0 }; +}; - const isDBBadge = (b: Badge | AWCBadge): b is Badge => 'id' in b; - const asAWCBadge = (b: Badge | AWCBadge) => b as AWCBadge; +const isDBBadge = (b: Badge | AWCBadge): b is Badge => 'id' in b; +const asAWCBadge = (b: Badge | AWCBadge) => b as AWCBadge; </script> {#if replaceCount < maxReplaceCount} diff --git a/src/lib/Utility/Loading.svelte b/src/lib/Utility/Loading.svelte index 92cbc1ac..67691cbb 100644 --- a/src/lib/Utility/Loading.svelte +++ b/src/lib/Utility/Loading.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - export let type: string | undefined = undefined; - export let percent: number | undefined = undefined; - export let card = true; +export let type: string | undefined = undefined; +export let percent: number | undefined = undefined; +export let card = true; </script> <div class:card> |