diff options
| author | Fuwn <[email protected]> | 2024-10-09 00:41:20 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-10-09 00:41:43 -0700 |
| commit | 998b63a35256ac985a5a2714dd1ca451af4dfd8a (patch) | |
| tree | 50796121a9d5ab0330fdc5d7e098bda2860d9726 /src/lib/Tools | |
| parent | feat(graphql): add badgeCount field (diff) | |
| download | due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.tar.xz due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.zip | |
chore(prettier): use spaces instead of tabs
Diffstat (limited to 'src/lib/Tools')
27 files changed, 2217 insertions, 2217 deletions
diff --git a/src/lib/Tools/ActivityHistory/Grid.svelte b/src/lib/Tools/ActivityHistory/Grid.svelte index 22d90d33..db9f3839 100644 --- a/src/lib/Tools/ActivityHistory/Grid.svelte +++ b/src/lib/Tools/ActivityHistory/Grid.svelte @@ -1,82 +1,82 @@ <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} - <LogInRestricted /> + <LogInRestricted /> {:else if activityHistoryData === undefined} - <Skeleton card={false} count={1} height="150px" /> + <Skeleton card={false} count={1} height="150px" /> {:else} - {@const filledActivities = fillMissingDays(activityHistoryData, false, currentYear)} - {@const highestActivity = Math.max(...filledActivities.map((activity) => activity.amount))} + {@const filledActivities = fillMissingDays(activityHistoryData, false, currentYear)} + {@const highestActivity = Math.max(...filledActivities.map((activity) => activity.amount))} - <div class="grid"> - {#each filledActivities as activity} - <div - class="grid-item" - style="background-color: {gradientColour(activity.amount, highestActivity, baseHue)}" - on:click={() => (baseHue = Math.floor(Math.random() * 360))} - on:keydown={() => { - return; - }} - role="button" - tabindex="0" - use:tooltip - title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ - activity.amount - }`} - /> - {/each} - </div> + <div class="grid"> + {#each filledActivities as activity} + <div + class="grid-item" + style="background-color: {gradientColour(activity.amount, highestActivity, baseHue)}" + on:click={() => (baseHue = Math.floor(Math.random() * 360))} + on:keydown={() => { + return; + }} + role="button" + tabindex="0" + use:tooltip + title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ + activity.amount + }`} + /> + {/each} + </div> {/if} <style> - .grid { - display: grid; - grid-template-columns: repeat(52, 1fr); - grid-template-rows: repeat(7, 1fr); - /* gap: 2px; */ - grid-auto-flow: column; - width: min-content; - background-color: white; - border-radius: 3px; - overflow: hidden; - } - .grid-item { - width: 20px; - height: 20px; - /* width: 3.25vw; */ - /* height: 3.25vw; */ - /* border: 1px solid #ccc; */ - } + .grid { + display: grid; + grid-template-columns: repeat(52, 1fr); + grid-template-rows: repeat(7, 1fr); + /* gap: 2px; */ + grid-auto-flow: column; + width: min-content; + background-color: white; + border-radius: 3px; + overflow: hidden; + } + .grid-item { + width: 20px; + height: 20px; + /* width: 3.25vw; */ + /* height: 3.25vw; */ + /* border: 1px solid #ccc; */ + } </style> diff --git a/src/lib/Tools/ActivityHistory/Tool.svelte b/src/lib/Tools/ActivityHistory/Tool.svelte index aafac40d..b6e66a5e 100644 --- a/src/lib/Tools/ActivityHistory/Tool.svelte +++ b/src/lib/Tools/ActivityHistory/Tool.svelte @@ -1,116 +1,116 @@ <script lang="ts"> - 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 { + 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} - <LogInRestricted /> + <LogInRestricted /> {:else} - {#await activityHistoryData} - <Skeleton card={false} count={5} height="0.9rem" list /> - {:then activities} - {#if activities === undefined} - <Skeleton card={false} count={5} height="0.9rem" list /> - {:else} - {@const filledActivities = fillMissingDays(activities)} - - <div class="card"> - <ActivityHistoryGrid {user} /> - - <p /> - - <div id="grid-final" /> - - {#if generated} - <p /> - {/if} - - <button on:click={screenshot}>Generate grid image</button> - </div> - - <p /> - - <details open> - <summary>Days in risk of developing an activity history hole</summary> - - <SettingHint> - Days in which you did not log any activity or only have one activity logged. - </SettingHint> - - <ul> - {#each filledActivities as activity} - {#if activity.amount === 0} - <li> - {new Date( - activity.date * 1000 + new Date().getTimezoneOffset() * 60 * 1000 - ).toDateString()} - </li> - {/if} - {/each} - </ul> - </details> - {/if} - {/await} + {#await activityHistoryData} + <Skeleton card={false} count={5} height="0.9rem" list /> + {:then activities} + {#if activities === undefined} + <Skeleton card={false} count={5} height="0.9rem" list /> + {:else} + {@const filledActivities = fillMissingDays(activities)} + + <div class="card"> + <ActivityHistoryGrid {user} /> + + <p /> + + <div id="grid-final" /> + + {#if generated} + <p /> + {/if} + + <button on:click={screenshot}>Generate grid image</button> + </div> + + <p /> + + <details open> + <summary>Days in risk of developing an activity history hole</summary> + + <SettingHint> + Days in which you did not log any activity or only have one activity logged. + </SettingHint> + + <ul> + {#each filledActivities as activity} + {#if activity.amount === 0} + <li> + {new Date( + activity.date * 1000 + new Date().getTimezoneOffset() * 60 * 1000 + ).toDateString()} + </li> + {/if} + {/each} + </ul> + </details> + {/if} + {/await} {/if} diff --git a/src/lib/Tools/Birthdays.svelte b/src/lib/Tools/Birthdays.svelte index 15e2d2fb..97ff40d8 100644 --- a/src/lib/Tools/Birthdays.svelte +++ b/src/lib/Tools/Birthdays.svelte @@ -1,166 +1,166 @@ <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; - } - - 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 anisearchBirthdays: Promise<aniSearchBirthday[]>; - let acdbBirthdays: Promise<ACDBBirthday[]>; - - $: { - 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) anisearchBirthdays = aniSearchBirthdays(month, day); - - acdbBirthdays = ACDBBirthdays(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'])); - - 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]; - - if (last === last.toUpperCase()) { - split[split.length - 1] = last[0] + last.slice(1).toLowerCase(); - return split.join(' '); - } - - const bracketIndex = name.indexOf('['); - - if (bracketIndex !== -1) return name.slice(0, bracketIndex).trim(); - - return name; - }; - - 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)); - - if (!nameMap.has(normalisedName)) - nameMap.set(normalisedName, { - name: entry.name, - image: entry.character_image, - origin: entry.origin - }); - } - - return Array.from(nameMap.values()); - }; + 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 anisearchBirthdays: Promise<aniSearchBirthday[]>; + let acdbBirthdays: Promise<ACDBBirthday[]>; + + $: { + 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) anisearchBirthdays = aniSearchBirthdays(month, day); + + acdbBirthdays = ACDBBirthdays(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'])); + + 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]; + + if (last === last.toUpperCase()) { + split[split.length - 1] = last[0] + last.slice(1).toLowerCase(); + return split.join(' '); + } + + const bracketIndex = name.indexOf('['); + + if (bracketIndex !== -1) return name.slice(0, bracketIndex).trim(); + + return name; + }; + + 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)); + + if (!nameMap.has(normalisedName)) + nameMap.set(normalisedName, { + name: entry.name, + image: entry.character_image, + origin: entry.origin + }); + } + + return Array.from(nameMap.values()); + }; </script> {#await acdbBirthdays} - <Message message="Loading birthday set one ..." /> + <Message message="Loading birthday set one ..." /> - <Skeleton grid={true} count={100} width="150px" height="170px" /> + <Skeleton grid={true} count={100} width="150px" height="170px" /> {:then acdbBirthdays} - {#await anisearchBirthdays} - <Message message="Loading birthday set two ..." /> - - <Skeleton grid={true} count={100} width="150px" height="170px" /> - {:then anisearch} - {@const birthdays = combineBirthdaySources(acdbBirthdays, anisearch)} - - <p> - <select bind:value={month}> - {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} - <option value={month}> - {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} - </option> - {/each} - </select> - - <select bind:value={day}> - {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} - <option value={day}>{day}</option> - {/each} - </select> - </p> - - <div class="characters"> - {#each birthdays as birthday} - <div class="card card-small"> - <a - href={`https://anilist.co/search/characters?search=${encodeURIComponent( - birthday.name - ).replace(/%20/g, '+')}`} - target="_blank" - title={birthday.origin} - use:tooltip - data-tooltip-disable={birthday.origin === undefined} - > - {birthday.name} - <img src={birthday.image} alt="Character (Large)" class="character-image" /> - </a> - </div> - {/each} - </div> - {:catch} - <Error type="Character" card /> - {/await} + {#await anisearchBirthdays} + <Message message="Loading birthday set two ..." /> + + <Skeleton grid={true} count={100} width="150px" height="170px" /> + {:then anisearch} + {@const birthdays = combineBirthdaySources(acdbBirthdays, anisearch)} + + <p> + <select bind:value={month}> + {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} + <option value={month}> + {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} + </option> + {/each} + </select> + + <select bind:value={day}> + {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} + <option value={day}>{day}</option> + {/each} + </select> + </p> + + <div class="characters"> + {#each birthdays as birthday} + <div class="card card-small"> + <a + href={`https://anilist.co/search/characters?search=${encodeURIComponent( + birthday.name + ).replace(/%20/g, '+')}`} + target="_blank" + title={birthday.origin} + use:tooltip + data-tooltip-disable={birthday.origin === undefined} + > + {birthday.name} + <img src={birthday.image} alt="Character (Large)" class="character-image" /> + </a> + </div> + {/each} + </div> + {:catch} + <Error type="Character" card /> + {/await} {:catch} - <Error type="Character" card /> + <Error type="Character" card /> {/await} <style lang="scss"> - .characters { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); - gap: 1rem; - grid-row-gap: 1rem; - align-items: start; - - img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 8px; - margin-top: 0.5rem; - box-shadow: 0 4px 30px var(--base01); - } - } + .characters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: 1rem; + grid-row-gap: 1rem; + align-items: start; + + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + margin-top: 0.5rem; + box-shadow: 0 4px 30px var(--base01); + } + } </style> diff --git a/src/lib/Tools/DumpProfile.svelte b/src/lib/Tools/DumpProfile.svelte index a6adba6d..45d4ffc9 100644 --- a/src/lib/Tools/DumpProfile.svelte +++ b/src/lib/Tools/DumpProfile.svelte @@ -1,52 +1,52 @@ <script lang="ts"> - 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(LZString.decompressFromBase64(match[1])); - } catch { - return null; - } - } - - return null; - }; + 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(LZString.decompressFromBase64(match[1])); + } catch { + return null; + } + } + + return null; + }; </script> <!-- svelte-ignore missing-declaration --> <InputTemplate field="Username" bind:submission event="Dump User" submitText="Dump"> - {#await dumpUser(submission)} - <Skeleton card={false} count={1} height="500px" /> - {:then dump} - {@const decoded = decodeJSON(dump.about)} + {#await dumpUser(submission)} + <Skeleton card={false} count={1} height="500px" /> + {:then dump} + {@const decoded = decodeJSON(dump.about)} - <pre>{JSON.stringify(dump, null, 2)}</pre> + <pre>{JSON.stringify(dump, null, 2)}</pre> - {#if decoded && (dump.about || '').includes('[](json')} - <p /> + {#if decoded && (dump.about || '').includes('[](json')} + <p /> - <pre>{JSON.stringify(decoded, null, 2).replaceAll(/\\n/g, '\n')}</pre> - {/if} - {:catch} - <RateLimited type="User" list={false} /> - {/await} + <pre>{JSON.stringify(decoded, null, 2).replaceAll(/\\n/g, '\n')}</pre> + {/if} + {:catch} + <RateLimited type="User" list={false} /> + {/await} </InputTemplate> <style> - pre { - margin: 0; - } + pre { + margin: 0; + } </style> diff --git a/src/lib/Tools/EpisodeDiscussionCollector.svelte b/src/lib/Tools/EpisodeDiscussionCollector.svelte index d213949f..4c61f3cf 100644 --- a/src/lib/Tools/EpisodeDiscussionCollector.svelte +++ b/src/lib/Tools/EpisodeDiscussionCollector.svelte @@ -1,61 +1,61 @@ <script lang="ts"> - 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 { 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 - field="Username" - bind:submission - event="Collect Episode Discussions" - submitText="Search" + field="Username" + bind:submission + event="Collect Episode Discussions" + submitText="Search" > - {#if submission !== ''} - {#await threads(submission)} - <Skeleton card={false} count={5} height="0.9rem" list /> - {:then threads} - <ul> - {#each threads - .filter((thread) => thread.title.includes('[Spoilers]') && thread.title.includes('Episode')) - .sort((a, b) => b.createdAt - a.createdAt) as thread} - <li> - <span class="opaque" style="white-space: pre;"> - {new Date(thread.createdAt * 1000).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - })} - </span> - <a - href={`https://anilist.co/forum/thread/${thread.id}`} - target="_blank" - title={`<img src="${thread.mediaCategories[0].coverImage.extraLarge}" style="width: 250px; object-fit: cover; border-radius: 8px;" />`} - use:tooltip - > - {thread.title.replace('[Spoilers]', '')} - </a> - </li> - {/each} - </ul> - {:catch} - <p>Threads could not be loaded. You might have been rate-limited.</p> - <p> - Try again in a few minutes. If the problem persists, please contact <a - href="https://anilist.co/user/fuwn" - target="_blank">@fuwn</a - > on AniList. - </p> - {/await} - {:else} - <p /> + {#if submission !== ''} + {#await threads(submission)} + <Skeleton card={false} count={5} height="0.9rem" list /> + {:then threads} + <ul> + {#each threads + .filter((thread) => thread.title.includes('[Spoilers]') && thread.title.includes('Episode')) + .sort((a, b) => b.createdAt - a.createdAt) as thread} + <li> + <span class="opaque" style="white-space: pre;"> + {new Date(thread.createdAt * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} + </span> + <a + href={`https://anilist.co/forum/thread/${thread.id}`} + target="_blank" + title={`<img src="${thread.mediaCategories[0].coverImage.extraLarge}" style="width: 250px; object-fit: cover; border-radius: 8px;" />`} + use:tooltip + > + {thread.title.replace('[Spoilers]', '')} + </a> + </li> + {/each} + </ul> + {:catch} + <p>Threads could not be loaded. You might have been rate-limited.</p> + <p> + Try again in a few minutes. If the problem persists, please contact <a + href="https://anilist.co/user/fuwn" + target="_blank">@fuwn</a + > on AniList. + </p> + {/await} + {:else} + <p /> - Enter a username to search for to continue. - {/if} + Enter a username to search for to continue. + {/if} </InputTemplate> diff --git a/src/lib/Tools/FollowFix.svelte b/src/lib/Tools/FollowFix.svelte index 434fb746..b9fddeff 100644 --- a/src/lib/Tools/FollowFix.svelte +++ b/src/lib/Tools/FollowFix.svelte @@ -1,52 +1,52 @@ <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} - <LogInRestricted /> + <LogInRestricted /> {:else} - <p> - <!-- svelte-ignore missing-declaration --> - <input - type="text" - minlength="1" - placeholder="Username" - bind:value={input} - on:keypress={(e) => { - if (e.key === 'Enter') { - submit = input; + <p> + <!-- svelte-ignore missing-declaration --> + <input + type="text" + minlength="1" + placeholder="Username" + bind:value={input} + on:keypress={(e) => { + if (e.key === 'Enter') { + submit = input; - // eslint-disable-next-line no-undef - umami.track('Fix Follow'); - } - }} - /> - <a href={'#'} on:click={() => (submit = input)}> - Toggle follow for {input.length === 0 ? '...' : input} - </a> - </p> + // eslint-disable-next-line no-undef + umami.track('Fix Follow'); + } + }} + /> + <a href={'#'} on:click={() => (submit = input)}> + Toggle follow for {input.length === 0 ? '...' : input} + </a> + </p> - {#if submit.length > 0} - {#await toggleFollow(user, submit)} - Toggling follow ... - {:then response} - <p> - Successfully toggled follow for {submit}. + {#if submit.length > 0} + {#await toggleFollow(user, submit)} + Toggling follow ... + {:then response} + <p> + Successfully toggled follow for {submit}. - {submit} - {response.isFollower ? 'is' : 'is not'} following you, {response.isFollowing - ? 'but' - : 'and'} you {response.isFollowing ? 'are' : 'are not'} following them. - </p> - {:catch} - <p>Failed to toggle follow for {submit}.</p> - {/await} - {/if} + {submit} + {response.isFollower ? 'is' : 'is not'} following you, {response.isFollowing + ? 'but' + : 'and'} you {response.isFollowing ? 'are' : 'are not'} following them. + </p> + {:catch} + <p>Failed to toggle follow for {submit}.</p> + {/await} + {/if} {/if} diff --git a/src/lib/Tools/Hayai.svelte b/src/lib/Tools/Hayai.svelte index f1eddb1c..1790af53 100644 --- a/src/lib/Tools/Hayai.svelte +++ b/src/lib/Tools/Hayai.svelte @@ -1,102 +1,102 @@ <script lang="ts"> - import { onMount } from 'svelte'; - import JSZip from 'jszip'; + 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` - ); - }; + downloadEPUB( + await newZip.generateAsync({ type: 'blob' }), + `${file.name.split('.epub')[0]}_hayai.epub` + ); + }; - reader.readAsArrayBuffer(file); - }; + reader.readAsArrayBuffer(file); + }; - const applyBionicReadingToHTML = (content: string) => { - const contentParser = new DOMParser().parseFromString(content, 'text/html'); + const applyBionicReadingToHTML = (content: string) => { + const contentParser = new DOMParser().parseFromString(content, 'text/html'); - for (const paragraph of contentParser.getElementsByTagName('p')) - paragraph.innerHTML = applyBionicReadingToString(paragraph.textContent ?? ''); + for (const paragraph of contentParser.getElementsByTagName('p')) + paragraph.innerHTML = applyBionicReadingToString(paragraph.textContent ?? ''); - return contentParser.documentElement.outerHTML; - }; + return contentParser.documentElement.outerHTML; + }; - const applyBionicReadingToString = (text: string) => - text - .split(/\s+/) - .map((word) => { - if (/^\W+$/.test(word) || word.length <= 2) return word; + const applyBionicReadingToString = (text: string) => + text + .split(/\s+/) + .map((word) => { + if (/^\W+$/.test(word) || word.length <= 2) return word; - let boldLength; + let boldLength; - 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); - } + 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); + } - return `<strong>${word.slice(0, boldLength)}</strong>${word.slice(boldLength)}`; - }) - .join(' '); + return `<strong>${word.slice(0, boldLength)}</strong>${word.slice(boldLength)}`; + }) + .join(' '); - const downloadEPUB = (blob: Blob | MediaSource, fileName: string) => { - const link = document.createElement('a'); + const downloadEPUB = (blob: Blob | MediaSource, fileName: string) => { + const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = fileName; + link.href = URL.createObjectURL(blob); + link.download = fileName; - link.click(); - URL.revokeObjectURL(link.href); - }; + 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"> - {@html applyBionicReadingToString('Upload an EPUB; receive a "bionic" EPUB.')} + {@html applyBionicReadingToString('Upload an EPUB; receive a "bionic" EPUB.')} - <br /> + <br /> - <small> - {@html applyBionicReadingToString( - "Bionic reading is a method of displaying text that aims to enhance readability and reading speed by guiding the reader's eyes using artificial fixation points." - )} - </small> + <small> + {@html applyBionicReadingToString( + "Bionic reading is a method of displaying text that aims to enhance readability and reading speed by guiding the reader's eyes using artificial fixation points." + )} + </small> - <p /> + <p /> - {@html applyBionicReadingToString( - `After selecting an EPUB file, 早い will apply a bionic reading filter over any and all words, and return the newly created "bionic" EPUB file.` - )} + {@html applyBionicReadingToString( + `After selecting an EPUB file, 早い will apply a bionic reading filter over any and all words, and return the newly created "bionic" EPUB file.` + )} - <p /> + <p /> - <input type="file" id="epub-file" accept=".epub" on:change={handleFileUpload} /> + <input type="file" id="epub-file" accept=".epub" on:change={handleFileUpload} /> </div> diff --git a/src/lib/Tools/HololiveBirthdays.svelte b/src/lib/Tools/HololiveBirthdays.svelte index 059a1f91..68a591de 100644 --- a/src/lib/Tools/HololiveBirthdays.svelte +++ b/src/lib/Tools/HololiveBirthdays.svelte @@ -1,95 +1,95 @@ <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 birthdays from '$lib/Data/Static/hololiveBirthdays.json'; + 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 birthdays from '$lib/Data/Static/hololiveBirthdays.json'; - 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 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()); - $: todaysBirthdays = birthdays.filter( - (birthday) => birthday.month === month && birthday.day === day - ); + $: todaysBirthdays = birthdays.filter( + (birthday) => birthday.month === month && birthday.day === day + ); - $: { - 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'])); </script> <p> - <select bind:value={month}> - {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} - <option value={month}> - {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} - </option> - {/each} - </select> + <select bind:value={month}> + {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} + <option value={month}> + {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} + </option> + {/each} + </select> - <select bind:value={day}> - {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} - <option value={day}>{day}</option> - {/each} - </select> + <select bind:value={day}> + {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} + <option value={day}>{day}</option> + {/each} + </select> </p> {#if todaysBirthdays.length === 0} - <Message - message={`No birthdays for ${$locale().dayFormatter( - new Date(new Date().getFullYear(), month - 1, day) - )}.`} - fullscreen={false} - loader="ripple" - /> + <Message + message={`No birthdays for ${$locale().dayFormatter( + new Date(new Date().getFullYear(), month - 1, day) + )}.`} + fullscreen={false} + loader="ripple" + /> {:else} - <div class="characters"> - {#each todaysBirthdays as birthday} - <div class="card card-small"> - <a - href={`https://anilist.co/search/characters?search=${encodeURIComponent( - birthday.name - ).replace(/%20/g, '+')}`} - target="_blank" - > - {birthday.name} - <img src={birthday.image} alt="Character" class="character-image" /> - </a> - </div> - {/each} - </div> + <div class="characters"> + {#each todaysBirthdays as birthday} + <div class="card card-small"> + <a + href={`https://anilist.co/search/characters?search=${encodeURIComponent( + birthday.name + ).replace(/%20/g, '+')}`} + target="_blank" + > + {birthday.name} + <img src={birthday.image} alt="Character" class="character-image" /> + </a> + </div> + {/each} + </div> {/if} <style lang="scss"> - .characters { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); - gap: 1rem; - grid-row-gap: 1rem; - align-items: start; + .characters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: 1rem; + grid-row-gap: 1rem; + align-items: start; - img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 8px; - margin-top: 0.5rem; - box-shadow: 0 4px 30px var(--base01); - } - } + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + margin-top: 0.5rem; + box-shadow: 0 4px 30px var(--base01); + } + } </style> diff --git a/src/lib/Tools/InputTemplate.svelte b/src/lib/Tools/InputTemplate.svelte index c81c12d5..72e2f807 100644 --- a/src/lib/Tools/InputTemplate.svelte +++ b/src/lib/Tools/InputTemplate.svelte @@ -1,71 +1,71 @@ <script lang="ts"> - import { clearAllParameters } from '$lib/Utility/parameters'; - import { onMount } from 'svelte'; - import SettingHint from '$lib/Settings/SettingHint.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"> - <p> - <!-- svelte-ignore missing-declaration --> - <input - type="text" - placeholder={field} - bind:value={input} - on:keypress={(e) => { - if (e.key === 'Enter') { - submission = input; + <p> + <!-- svelte-ignore missing-declaration --> + <input + type="text" + placeholder={field} + bind:value={input} + on:keypress={(e) => { + if (e.key === 'Enter') { + submission = input; - onSubmit(); + onSubmit(); - // eslint-disable-next-line no-undef - if (event) umami.track(event); - } - }} - /> - <button - class="button-lined" - on:click={() => { - submission = input; + // eslint-disable-next-line no-undef + if (event) umami.track(event); + } + }} + /> + <button + class="button-lined" + on:click={() => { + submission = input; - onSubmit(); - }} - title="Or click your Enter key" - data-umami-event={event} - > - {submitText} - </button> + onSubmit(); + }} + title="Or click your Enter key" + data-umami-event={event} + > + {submitText} + </button> - {#if hint !== undefined} - <br /> - <div style="margin-top: .25rem;"> - <SettingHint>{hint}</SettingHint> - </div> - {/if} - </p> + {#if hint !== undefined} + <br /> + <div style="margin-top: .25rem;"> + <SettingHint>{hint}</SettingHint> + </div> + {/if} + </p> - {#if submission !== ''} - <slot /> - {:else} - <p /> + {#if submission !== ''} + <slot /> + {:else} + <p /> - {prompt} - {/if} + {prompt} + {/if} </div> diff --git a/src/lib/Tools/Likes.svelte b/src/lib/Tools/Likes.svelte index fbbc30af..7b626c94 100644 --- a/src/lib/Tools/Likes.svelte +++ b/src/lib/Tools/Likes.svelte @@ -1,58 +1,58 @@ <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 - field="Activity or Thread URL" - bind:submission - event="Get All Likes" - submitText="Get All Likes" + field="Activity or Thread URL" + bind:submission + event="Get All Likes" + submitText="Get All Likes" > - {#if submission.length > 5 && submission.match(/https:\/\/anilist.co\/(activity|forum\/thread)\/\d+/)} - {#await likesPromise} - <Skeleton card={false} count={5} height="0.9rem" list /> - {:then likes} - {#if likes && likes.length > 0} - <ul> - {#each likes as like} - <li> - <a - href={`https://anilist.co/user/${like.name}`} - target="_blank" - title={`<img src="${ - $settings.displayDataSaver ? like.avatar?.medium : like.avatar?.large - }" style="width: 150px; object-fit: cover; border-radius: 8px;" />`} - use:tooltip - > - {like.name} - </a> - </li> - {/each} - </ul> - {:else} - No likes were found for that {submissionType}. - {/if} - {:catch} - <RateLimited type="Likes" list={false} /> - {/await} - {:else} - Please enter a valid Activity or Thread URL. - {/if} + {#if submission.length > 5 && submission.match(/https:\/\/anilist.co\/(activity|forum\/thread)\/\d+/)} + {#await likesPromise} + <Skeleton card={false} count={5} height="0.9rem" list /> + {:then likes} + {#if likes && likes.length > 0} + <ul> + {#each likes as like} + <li> + <a + href={`https://anilist.co/user/${like.name}`} + target="_blank" + title={`<img src="${ + $settings.displayDataSaver ? like.avatar?.medium : like.avatar?.large + }" style="width: 150px; object-fit: cover; border-radius: 8px;" />`} + use:tooltip + > + {like.name} + </a> + </li> + {/each} + </ul> + {:else} + No likes were found for that {submissionType}. + {/if} + {:catch} + <RateLimited type="Likes" list={false} /> + {/await} + {:else} + Please enter a valid Activity or Thread URL. + {/if} </InputTemplate> diff --git a/src/lib/Tools/Picker.svelte b/src/lib/Tools/Picker.svelte index 8b2d183f..583a7a0d 100644 --- a/src/lib/Tools/Picker.svelte +++ b/src/lib/Tools/Picker.svelte @@ -1,23 +1,23 @@ <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> - <select - bind:value={tool} - on:change={() => { - if (browser) goto(root(`/tools/${tool}`)); - }} - > - <option value="default" selected disabled hidden>Select a tool to continue</option> + <select + bind:value={tool} + on:change={() => { + if (browser) goto(root(`/tools/${tool}`)); + }} + > + <option value="default" selected disabled hidden>Select a tool to continue</option> - {#each Object.keys(tools).filter((t) => t !== 'default') as t} - <option value={t}>{tools[t].short || tools[t].name()}</option> - {/each} - </select> + {#each Object.keys(tools).filter((t) => t !== 'default') as t} + <option value={t}>{tools[t].short || tools[t].name()}</option> + {/each} + </select> </blockquote> diff --git a/src/lib/Tools/RandomFollower.svelte b/src/lib/Tools/RandomFollower.svelte index a8fe7141..acb5a33a 100644 --- a/src/lib/Tools/RandomFollower.svelte +++ b/src/lib/Tools/RandomFollower.svelte @@ -1,32 +1,32 @@ <script lang="ts"> - 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 { 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 - field="Username" - bind:submission - event="Random Follower" - submitText="Generate" - onSubmit={() => (randomSeed = Math.random())} + field="Username" + bind:submission + event="Random Follower" + submitText="Generate" + onSubmit={() => (randomSeed = Math.random())} > - {#await followers(submission)} - <Skeleton card={false} count={1} height="0.9rem" list /> - {:then users} - {@const user = users[Math.floor(randomSeed * users.length)]} + {#await followers(submission)} + <Skeleton card={false} count={1} height="0.9rem" list /> + {:then users} + {@const user = users[Math.floor(randomSeed * users.length)]} - <p /> + <p /> - <a href={`https://anilist.co/user/${user.id}`} target="_blank"> - <TextSwap text={user.name} /> - </a> - {:catch} - <RateLimited type="Followers" list={false} /> - {/await} + <a href={`https://anilist.co/user/${user.id}`} target="_blank"> + <TextSwap text={user.name} /> + </a> + {:catch} + <RateLimited type="Followers" list={false} /> + {/await} </InputTemplate> diff --git a/src/lib/Tools/SequelCatcher/List.svelte b/src/lib/Tools/SequelCatcher/List.svelte index 8205b8c7..009df219 100644 --- a/src/lib/Tools/SequelCatcher/List.svelte +++ b/src/lib/Tools/SequelCatcher/List.svelte @@ -1,27 +1,27 @@ <script lang="ts"> - 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 { 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, @@ -33,57 +33,57 @@ specials, etc.) <p /> <ol class="media-list"> - {#each filterRelations( mediaListUnchecked.filter((media) => media.mediaListEntry?.status === 'COMPLETED'), includeSideStories ) as { media, unwatchedRelations }} - {#if unwatchedRelations.filter( (relation) => matchCheck(mediaListUnchecked.find((media) => media.id === relation.node.id)) ).length !== 0 || includeCurrent} - <li> - <a href={outboundLink(media, 'anime', $settings.displayOutboundLinksTo)} target="_blank"> - <MediaTitleDisplay title={media.title} /> - </a> + {#each filterRelations( mediaListUnchecked.filter((media) => media.mediaListEntry?.status === 'COMPLETED'), includeSideStories ) as { media, unwatchedRelations }} + {#if unwatchedRelations.filter( (relation) => matchCheck(mediaListUnchecked.find((media) => media.id === relation.node.id)) ).length !== 0 || includeCurrent} + <li> + <a href={outboundLink(media, 'anime', $settings.displayOutboundLinksTo)} target="_blank"> + <MediaTitleDisplay title={media.title} /> + </a> - <span class="opaque"> - ({media.startDate.year}) - </span> + <span class="opaque"> + ({media.startDate.year}) + </span> - <ol class="unwatched-relations-list"> - {#each unwatchedRelations as relation} - {@const hit = matchCheck( - mediaListUnchecked.find((media) => media.id === relation.node.id), - true - )} + <ol class="unwatched-relations-list"> + {#each unwatchedRelations as relation} + {@const hit = matchCheck( + mediaListUnchecked.find((media) => media.id === relation.node.id), + true + )} - {#if matchCheck(mediaListUnchecked.find((media) => media.id === relation.node.id)) || includeCurrent} - <li> - <a - href={outboundLink(relation.node, 'anime', $settings.displayOutboundLinksTo)} - target="_blank" - > - <MediaTitleDisplay title={relation.node.title} /> - </a> + {#if matchCheck(mediaListUnchecked.find((media) => media.id === relation.node.id)) || includeCurrent} + <li> + <a + href={outboundLink(relation.node, 'anime', $settings.displayOutboundLinksTo)} + target="_blank" + > + <MediaTitleDisplay title={relation.node.title} /> + </a> - {#if hit && hit.mediaListEntry && hit.mediaListEntry.progress > 0} - <span class="opaque">|</span> + {#if hit && hit.mediaListEntry && hit.mediaListEntry.progress > 0} + <span class="opaque">|</span> - {hit.mediaListEntry.progress}/{#if hit.episodes}{hit.episodes}{:else}?{/if} - {/if} + {hit.mediaListEntry.progress}/{#if hit.episodes}{hit.episodes}{:else}?{/if} + {/if} - <span class="opaque"> - ({relation.node.startDate.year}) - </span> - </li> - {/if} - {/each} - </ol> - </li> - {/if} - {/each} + <span class="opaque"> + ({relation.node.startDate.year}) + </span> + </li> + {/if} + {/each} + </ol> + </li> + {/if} + {/each} </ol> <style> - .media-list li { - margin-bottom: 1rem; - } + .media-list li { + margin-bottom: 1rem; + } - .unwatched-relations-list li:not(:last-child) { - margin-bottom: 0 !important; - } + .unwatched-relations-list li:not(:last-child) { + margin-bottom: 0 !important; + } </style> diff --git a/src/lib/Tools/SequelCatcher/Tool.svelte b/src/lib/Tools/SequelCatcher/Tool.svelte index 06d635b3..a954b4d7 100644 --- a/src/lib/Tools/SequelCatcher/Tool.svelte +++ b/src/lib/Tools/SequelCatcher/Tool.svelte @@ -1,80 +1,80 @@ <script lang="ts"> - 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 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} - <LogInRestricted /> + <LogInRestricted /> {:else} - <div class="card"> - {#await mediaList} - <Message message="Cross-checking media ..." /> + <div class="card"> + {#await mediaList} + <Message message="Cross-checking media ..." /> - <Skeleton - card={false} - count={8} - pad={false} - height={'0.9rem'} - width={'100%'} - list - grid={false} - /> - {:then mediaListUnchecked} - {#if mediaListUnchecked} - <List {mediaListUnchecked} /> - {:else} - <Message message="Cross-checking media ..." /> + <Skeleton + card={false} + count={8} + pad={false} + height={'0.9rem'} + width={'100%'} + list + grid={false} + /> + {:then mediaListUnchecked} + {#if mediaListUnchecked} + <List {mediaListUnchecked} /> + {:else} + <Message message="Cross-checking media ..." /> - <Skeleton - card={false} - count={8} - pad={false} - height={'0.9rem'} - width={'100%'} - list - grid={false} - /> - {/if} - {:catch} - <Message message="" loader="ripple" slot withReload fullscreen>Error fetching media.</Message> - {/await} + <Skeleton + card={false} + count={8} + pad={false} + height={'0.9rem'} + width={'100%'} + list + grid={false} + /> + {/if} + {:catch} + <Message message="" loader="ripple" slot withReload fullscreen>Error fetching media.</Message> + {/await} - <p /> + <p /> - <blockquote style="margin: 0 0 0 1.5rem;"> - Thanks to <Username username="sevengirl" /> and <Username username="esthereae" /> for the idea! - </blockquote> - </div> + <blockquote style="margin: 0 0 0 1.5rem;"> + Thanks to <Username username="sevengirl" /> and <Username username="esthereae" /> for the idea! + </blockquote> + </div> {/if} diff --git a/src/lib/Tools/SequelSpy/Prequels.svelte b/src/lib/Tools/SequelSpy/Prequels.svelte index ab1c4ac5..b22db3af 100644 --- a/src/lib/Tools/SequelSpy/Prequels.svelte +++ b/src/lib/Tools/SequelSpy/Prequels.svelte @@ -1,35 +1,35 @@ <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> - {#each currentPrequels.sort((a, b) => new Date(a.startDate.year, a.startDate.month - 1, a.startDate.day).getTime() - new Date(b.startDate.year, b.startDate.month - 1, b.startDate.day).getTime()) as prequel} - <li id={`${prequel.id}`}> - <LinkedTooltip - content={`<img src="${ - $settings.displayDataSaver ? prequel.coverImage.medium : prequel.coverImage.extraLarge - }" style="width: 250px; object-fit: cover; border-radius: 8px;" />`} - pin={`${prequel.id}`} - pinPosition="top" - disable={!$settings.displayHoverCover} - > - <a href={`https://anilist.co/anime/${prequel.id}`} target="_blank"> - <MediaTitleDisplay title={prequel.title} /> - </a> - <span class="opaque">|</span> - {prequel.seen}<span class="opaque">/{prequel.episodes}</span> - {@html prequelAiringTime(prequel)} - </LinkedTooltip> - </li> - {/each} + {#each currentPrequels.sort((a, b) => new Date(a.startDate.year, a.startDate.month - 1, a.startDate.day).getTime() - new Date(b.startDate.year, b.startDate.month - 1, b.startDate.day).getTime()) as prequel} + <li id={`${prequel.id}`}> + <LinkedTooltip + content={`<img src="${ + $settings.displayDataSaver ? prequel.coverImage.medium : prequel.coverImage.extraLarge + }" style="width: 250px; object-fit: cover; border-radius: 8px;" />`} + pin={`${prequel.id}`} + pinPosition="top" + disable={!$settings.displayHoverCover} + > + <a href={`https://anilist.co/anime/${prequel.id}`} target="_blank"> + <MediaTitleDisplay title={prequel.title} /> + </a> + <span class="opaque">|</span> + {prequel.seen}<span class="opaque">/{prequel.episodes}</span> + {@html prequelAiringTime(prequel)} + </LinkedTooltip> + </li> + {/each} </ul> diff --git a/src/lib/Tools/SequelSpy/Tool.svelte b/src/lib/Tools/SequelSpy/Tool.svelte index 8956e00a..caec4a46 100644 --- a/src/lib/Tools/SequelSpy/Tool.svelte +++ b/src/lib/Tools/SequelSpy/Tool.svelte @@ -1,62 +1,62 @@ <script lang="ts"> - 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'])); + 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'])); </script> {#if user === undefined || $identity.id === -2} - <LogInRestricted /> + <LogInRestricted /> {:else} - <div class="card"> - <p> - <select bind:value={season}> - <option value="WINTER">Winter</option> - <option value="SPRING">Spring</option> - <option value="SUMMER">Summer</option> - <option value="FALL">Fall</option> - </select> - <input type="number" bind:value={year} /> - </p> - - {#await currentPrequels} - <Skeleton card={false} count={5} height="0.9rem" list /> - {:then currentPrequels} - <Prequels {currentPrequels} /> - {/await} - - <p /> - - The count ratio is the number of episodes you've seen of any direct prequels, and the total - number of episodes of all direct prequels. - </div> + <div class="card"> + <p> + <select bind:value={season}> + <option value="WINTER">Winter</option> + <option value="SPRING">Spring</option> + <option value="SUMMER">Summer</option> + <option value="FALL">Fall</option> + </select> + <input type="number" bind:value={year} /> + </p> + + {#await currentPrequels} + <Skeleton card={false} count={5} height="0.9rem" list /> + {:then currentPrequels} + <Prequels {currentPrequels} /> + {/await} + + <p /> + + The count ratio is the number of episodes you've seen of any direct prequels, and the total + number of episodes of all direct prequels. + </div> {/if} diff --git a/src/lib/Tools/UmaMusumeBirthdays.svelte b/src/lib/Tools/UmaMusumeBirthdays.svelte index faf962f8..29b1faa6 100644 --- a/src/lib/Tools/UmaMusumeBirthdays.svelte +++ b/src/lib/Tools/UmaMusumeBirthdays.svelte @@ -1,134 +1,134 @@ <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} - <Message message="Loading birthdays ..." /> + <Message message="Loading birthdays ..." /> - <Skeleton grid={true} count={100} width="150px" height="170px" /> + <Skeleton grid={true} count={100} width="150px" height="170px" /> {:then birthdays} - {#if birthdays} - {@const todaysBirthdays = birthdays.filter( - (birthday) => birthday.birth_month === month && birthday.birth_day === day - )} + {#if birthdays} + {@const todaysBirthdays = birthdays.filter( + (birthday) => birthday.birth_month === month && birthday.birth_day === day + )} - <p> - <select bind:value={month}> - {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} - <option value={month}> - {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} - </option> - {/each} - </select> + <p> + <select bind:value={month}> + {#each Array.from({ length: 12 }, (_, i) => i + 1) as month} + <option value={month}> + {new Date(0, month - 1).toLocaleString('default', { month: 'long' })} + </option> + {/each} + </select> - <select bind:value={day}> - {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} - <option value={day}>{day}</option> - {/each} - </select> - </p> + <select bind:value={day}> + {#each Array.from({ length: new Date(new Date().getFullYear(), month, 0).getDate() }, (_, i) => i + 1) as day} + <option value={day}>{day}</option> + {/each} + </select> + </p> - {#if todaysBirthdays.length === 0} - <Message - message={`No birthdays for ${$locale().dayFormatter( - new Date(new Date().getFullYear(), month - 1, day) - )}.`} - fullscreen={false} - loader="ripple" - /> - {:else} - <div class="characters"> - {#each todaysBirthdays as birthday} - {@const name = $settings.displayLanguage === 'en' ? birthday.name_en : birthday.name_jp} - {@const nameOther = - $settings.displayLanguage === 'ja' ? birthday.name_en : birthday.name_jp} + {#if todaysBirthdays.length === 0} + <Message + message={`No birthdays for ${$locale().dayFormatter( + new Date(new Date().getFullYear(), month - 1, day) + )}.`} + fullscreen={false} + loader="ripple" + /> + {:else} + <div class="characters"> + {#each todaysBirthdays as birthday} + {@const name = $settings.displayLanguage === 'en' ? birthday.name_en : birthday.name_jp} + {@const nameOther = + $settings.displayLanguage === 'ja' ? birthday.name_en : birthday.name_jp} - <div class="card card-small"> - <a - href={`https://anilist.co/search/characters?search=${encodeURIComponent( - birthday.name_en - ).replace(/%20/g, '+')}`} - target="_blank" - title={nameOther} - use:tooltip - > - {name} - <img src={birthday.sns_icon} alt="Character" class="character-image" /> - </a> - </div> - {/each} - </div> - {/if} - {:else} - <Message message="Validating birthdays ..." /> + <div class="card card-small"> + <a + href={`https://anilist.co/search/characters?search=${encodeURIComponent( + birthday.name_en + ).replace(/%20/g, '+')}`} + target="_blank" + title={nameOther} + use:tooltip + > + {name} + <img src={birthday.sns_icon} alt="Character" class="character-image" /> + </a> + </div> + {/each} + </div> + {/if} + {:else} + <Message message="Validating birthdays ..." /> - <Skeleton grid={true} count={100} width="150px" height="170px" /> - {/if} + <Skeleton grid={true} count={100} width="150px" height="170px" /> + {/if} {:catch} - <Error type="Character" card /> + <Error type="Character" card /> {/await} <style lang="scss"> - .characters { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); - gap: 1rem; - grid-row-gap: 1rem; - align-items: start; + .characters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: 1rem; + grid-row-gap: 1rem; + align-items: start; - img { - width: 100%; - height: auto; - object-fit: cover; - border-radius: 8px; - margin-top: 0.5rem; - box-shadow: 0 4px 30px var(--base01); - } - } + img { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 8px; + margin-top: 0.5rem; + box-shadow: 0 4px 30px var(--base01); + } + } </style> diff --git a/src/lib/Tools/Wrapped/ActivityHistory.svelte b/src/lib/Tools/Wrapped/ActivityHistory.svelte index 194f5951..3da401d4 100644 --- a/src/lib/Tools/Wrapped/ActivityHistory.svelte +++ b/src/lib/Tools/Wrapped/ActivityHistory.svelte @@ -1,21 +1,21 @@ <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 - class="categories-grid" - style={`padding-${activityHistoryPosition === 'ORIGINAL' ? 'top' : 'bottom'}: 0;`} + class="categories-grid" + style={`padding-${activityHistoryPosition === 'ORIGINAL' ? 'top' : 'bottom'}: 0;`} > - <div class="category-grid bottom-category pure-category category"> - <div id="activity-history"> - <ActivityHistoryGrid {user} activityData={activities} currentYear={year} /> - </div> - </div> + <div class="category-grid bottom-category pure-category category"> + <div id="activity-history"> + <ActivityHistoryGrid {user} activityData={activities} currentYear={year} /> + </div> + </div> </div> diff --git a/src/lib/Tools/Wrapped/Media.svelte b/src/lib/Tools/Wrapped/Media.svelte index f5184bc4..ea8a989b 100644 --- a/src/lib/Tools/Wrapped/Media.svelte +++ b/src/lib/Tools/Wrapped/Media.svelte @@ -1,98 +1,98 @@ <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} - <div class="categories-grid"> - <div class="category-grid pure-category category middle-category"> - <div class="grid-item image-grid"> - <a - href={animeList && animeList[0] ? `https://anilist.co/anime/${animeList[0].id}` : '#'} - target="_blank" - > - <img - src={proxy( - animeList && animeList[0] ? animeList[0].coverImage.extraLarge : wrapped.avatar.large - )} - alt="Highest Rated Anime Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>{animeMostTitle} Anime</b> - <ol> - {#if animeList !== undefined && animeList.length !== 0} - {#each animeList?.slice(0, highestRatedCount) as anime} - <li> - <a href={`https://anilist.co/anime/${anime.id}`} target="_blank"> - <MediaTitleDisplay title={anime.title} /> - </a>{highestRatedMediaPercentage && - anime.mediaListEntry && - anime.mediaListEntry?.score > 0 - ? `: ${anime.mediaListEntry?.score}%` - : ''} - </li> - {/each} - {:else} - <li> - <p class="opaque">(⌣_⌣”)</p> - </li> - {/if} - </ol> - </div> - </div> - </div> - <div class="category-grid pure-category category middle-category"> - <div class="grid-item image-grid"> - <a - href={mangaList && mangaList[0] ? `https://anilist.co/manga/${mangaList[0].id}` : '#'} - target="_blank" - > - <img - src={proxy( - mangaList && mangaList[0] ? mangaList[0].coverImage.extraLarge : wrapped.avatar.large - )} - alt="Highest Rated Manga Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>{mangaMostTitle} Manga</b> - <ol> - {#if mangaList !== undefined && mangaList.length !== 0} - {#each mangaList?.slice(0, highestRatedCount) as manga} - <li> - <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> - <MediaTitleDisplay title={manga.title} /> - </a>{highestRatedMediaPercentage && - manga.mediaListEntry && - manga.mediaListEntry?.score > 0 - ? `: ${manga.mediaListEntry?.score}%` - : ''} - </li> - {/each} - {:else} - <li> - <p class="opaque">(⌣_⌣”)</p> - </li> - {/if} - </ol> - </div> - </div> - </div> - </div> + <div class="categories-grid"> + <div class="category-grid pure-category category middle-category"> + <div class="grid-item image-grid"> + <a + href={animeList && animeList[0] ? `https://anilist.co/anime/${animeList[0].id}` : '#'} + target="_blank" + > + <img + src={proxy( + animeList && animeList[0] ? animeList[0].coverImage.extraLarge : wrapped.avatar.large + )} + alt="Highest Rated Anime Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>{animeMostTitle} Anime</b> + <ol> + {#if animeList !== undefined && animeList.length !== 0} + {#each animeList?.slice(0, highestRatedCount) as anime} + <li> + <a href={`https://anilist.co/anime/${anime.id}`} target="_blank"> + <MediaTitleDisplay title={anime.title} /> + </a>{highestRatedMediaPercentage && + anime.mediaListEntry && + anime.mediaListEntry?.score > 0 + ? `: ${anime.mediaListEntry?.score}%` + : ''} + </li> + {/each} + {:else} + <li> + <p class="opaque">(⌣_⌣”)</p> + </li> + {/if} + </ol> + </div> + </div> + </div> + <div class="category-grid pure-category category middle-category"> + <div class="grid-item image-grid"> + <a + href={mangaList && mangaList[0] ? `https://anilist.co/manga/${mangaList[0].id}` : '#'} + target="_blank" + > + <img + src={proxy( + mangaList && mangaList[0] ? mangaList[0].coverImage.extraLarge : wrapped.avatar.large + )} + alt="Highest Rated Manga Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>{mangaMostTitle} Manga</b> + <ol> + {#if mangaList !== undefined && mangaList.length !== 0} + {#each mangaList?.slice(0, highestRatedCount) as manga} + <li> + <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> + <MediaTitleDisplay title={manga.title} /> + </a>{highestRatedMediaPercentage && + manga.mediaListEntry && + manga.mediaListEntry?.score > 0 + ? `: ${manga.mediaListEntry?.score}%` + : ''} + </li> + {/each} + {:else} + <li> + <p class="opaque">(⌣_⌣”)</p> + </li> + {/if} + </ol> + </div> + </div> + </div> + </div> {/if} diff --git a/src/lib/Tools/Wrapped/MediaExtras.svelte b/src/lib/Tools/Wrapped/MediaExtras.svelte index 9ef8cb65..9e755ea5 100644 --- a/src/lib/Tools/Wrapped/MediaExtras.svelte +++ b/src/lib/Tools/Wrapped/MediaExtras.svelte @@ -1,74 +1,74 @@ <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;"> - {#if topMedia.topGenreMedia && topMedia.genres.length > 0} - <div class="category-grid pure-category category"> - <div class="grid-item image-grid"> - <a - href={`https://anilist.co/${topMedia.topGenreMedia.type.toLowerCase()}/${ - topMedia.topGenreMedia.id - }`} - target="_blank" - > - <img - src={proxy(topMedia.topGenreMedia.coverImage.extraLarge)} - alt="Highest Rated Genre Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>{genreTagTitle} Genres</b> - <ol> - {#each topMedia.genres as genre} - <li> - <a href={`https://anilist.co/search/anime?genres=${genre.genre}`} target="_blank"> - {genre.genre}{highestRatedGenreTagPercentage ? `: ${genre.averageScore}%` : ''} - </a> - </li> - {/each} - </ol> - </div> - </div> - </div> - {/if} - {#if topMedia.topTagMedia && topMedia.tags.length > 0} - <div class="category-grid pure-category category"> - <div class="grid-item image-grid"> - <a - href={`https://anilist.co/${topMedia.topTagMedia.type.toLowerCase()}/${ - topMedia.topTagMedia.id - }`} - target="_blank" - > - <img - src={proxy(topMedia.topTagMedia.coverImage.extraLarge)} - alt="Highest Rated Tag Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>{genreTagTitle} Tags</b> - <ol> - {#each topMedia.tags as tag} - <li> - <a href={`https://anilist.co/search/anime?genres=${tag.tag}`} target="_blank"> - {tag.tag}{highestRatedGenreTagPercentage ? `: ${tag.averageScore}%` : ''} - </a> - </li> - {/each} - </ol> - </div> - </div> - </div> - {/if} + {#if topMedia.topGenreMedia && topMedia.genres.length > 0} + <div class="category-grid pure-category category"> + <div class="grid-item image-grid"> + <a + href={`https://anilist.co/${topMedia.topGenreMedia.type.toLowerCase()}/${ + topMedia.topGenreMedia.id + }`} + target="_blank" + > + <img + src={proxy(topMedia.topGenreMedia.coverImage.extraLarge)} + alt="Highest Rated Genre Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>{genreTagTitle} Genres</b> + <ol> + {#each topMedia.genres as genre} + <li> + <a href={`https://anilist.co/search/anime?genres=${genre.genre}`} target="_blank"> + {genre.genre}{highestRatedGenreTagPercentage ? `: ${genre.averageScore}%` : ''} + </a> + </li> + {/each} + </ol> + </div> + </div> + </div> + {/if} + {#if topMedia.topTagMedia && topMedia.tags.length > 0} + <div class="category-grid pure-category category"> + <div class="grid-item image-grid"> + <a + href={`https://anilist.co/${topMedia.topTagMedia.type.toLowerCase()}/${ + topMedia.topTagMedia.id + }`} + target="_blank" + > + <img + src={proxy(topMedia.topTagMedia.coverImage.extraLarge)} + alt="Highest Rated Tag Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>{genreTagTitle} Tags</b> + <ol> + {#each topMedia.tags as tag} + <li> + <a href={`https://anilist.co/search/anime?genres=${tag.tag}`} target="_blank"> + {tag.tag}{highestRatedGenreTagPercentage ? `: ${tag.averageScore}%` : ''} + </a> + </li> + {/each} + </ol> + </div> + </div> + </div> + {/if} </div> diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte index 81a60016..1484ab5c 100644 --- a/src/lib/Tools/Wrapped/Tool.svelte +++ b/src/lib/Tools/Wrapped/Tool.svelte @@ -1,776 +1,776 @@ <script lang="ts"> - import userIdentity from '$stores/identity'; - import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; - import { onMount } from 'svelte'; - import { tops, wrapped, type TopMedia, SortOptions } from '$lib/Data/AniList/wrapped'; - import { - fullActivityHistory, - activityHistory as getActivityHistory - } 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 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 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; - - $: { - 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()); - - history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); - } - } - $: { - 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; - - 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 - ); - - 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[]); - } - - updateWidth(); - } - $: 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; - - if (!disableActivityHistory && bottomWidths > requiredWidth) requiredWidth = bottomWidths; - - requiredWidth += wrappedContainer.offsetWidth - wrappedContainer.clientWidth; - - wrappedContainer.style.width = `${requiredWidth}px`; - width = requiredWidth; - }; - - reset(); - reset(); - }; - - onMount(async () => { - clearAllParameters([ - 'transparency', - 'lightTheme', - 'watermark', - 'includeMusic', - 'includeSpecials', - 'includeRepeats', - 'forceDark', - 'highestRatedCount', - 'genreTagCount', - 'disableActivityHistory', - 'highestRatedMediaPercentage', - 'highestRatedGenreTagPercentage', - 'genreTagsSort', - 'mediaSort', - 'includeMovies', - 'includeOVAs' - ]); - - 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'; - 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 update = async () => { - if ($userIdentity.id === -1) return; - - let rawAnimeList = await mediaListCollection( - user, - $userIdentity, - Type.Anime, - $anime, - $lastPruneTimes.anime, - { - forcePrune: 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 - ? 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') - ) - .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.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: 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)) - ) - .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; - - for (const media of calculatedAnimeList) { - episodes += media.mediaListEntry?.progress || 0; - minutesWatched += (media.mediaListEntry?.progress || 0) * media.duration || 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); - - // if (wrappedImageButton !== null) { - // wrappedImageButton.href = object; - // } - - const wrappedFinal = document.getElementById('wrapped-final'); - - if (wrappedFinal !== null) { - wrappedFinal.innerHTML = ''; - - wrappedFinal.appendChild(downloadWrapper); - - generated = 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; - } - - 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; - } - - 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]); - // }; + import userIdentity from '$stores/identity'; + import type { AniListAuthorisation } from '$lib/Data/AniList/identity'; + import { onMount } from 'svelte'; + import { tops, wrapped, type TopMedia, SortOptions } from '$lib/Data/AniList/wrapped'; + import { + fullActivityHistory, + activityHistory as getActivityHistory + } 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 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 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; + + $: { + 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()); + + history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); + } + } + $: { + 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; + + 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 + ); + + 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[]); + } + + updateWidth(); + } + $: 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; + + if (!disableActivityHistory && bottomWidths > requiredWidth) requiredWidth = bottomWidths; + + requiredWidth += wrappedContainer.offsetWidth - wrappedContainer.clientWidth; + + wrappedContainer.style.width = `${requiredWidth}px`; + width = requiredWidth; + }; + + reset(); + reset(); + }; + + onMount(async () => { + clearAllParameters([ + 'transparency', + 'lightTheme', + 'watermark', + 'includeMusic', + 'includeSpecials', + 'includeRepeats', + 'forceDark', + 'highestRatedCount', + 'genreTagCount', + 'disableActivityHistory', + 'highestRatedMediaPercentage', + 'highestRatedGenreTagPercentage', + 'genreTagsSort', + 'mediaSort', + 'includeMovies', + 'includeOVAs' + ]); + + 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'; + 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 update = async () => { + if ($userIdentity.id === -1) return; + + let rawAnimeList = await mediaListCollection( + user, + $userIdentity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + forcePrune: 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 + ? 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') + ) + .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.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: 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)) + ) + .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; + + for (const media of calculatedAnimeList) { + episodes += media.mediaListEntry?.progress || 0; + minutesWatched += (media.mediaListEntry?.progress || 0) * media.duration || 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); + + // if (wrappedImageButton !== null) { + // wrappedImageButton.href = object; + // } + + const wrappedFinal = document.getElementById('wrapped-final'); + + if (wrappedFinal !== null) { + wrappedFinal.innerHTML = ''; + + wrappedFinal.appendChild(downloadWrapper); + + generated = 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; + } + + 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; + } + + 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} - <LogInRestricted /> + <LogInRestricted /> {:else if $userIdentity.id !== -1} - {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, $userIdentity, selectedYear) : getActivityHistory($userIdentity)} - <Message message="Loading activity history ..." /> - - <Skeleton count={2} /> - {:then activities} - {#await wrapped(user, $userIdentity, selectedYear)} - <Message message="Loading user data ..." /> - - <Skeleton count={2} /> - {:then wrapped} - <div id="list-container"> - <div class="card"> - <div - id="wrapped" - class:light-theme={lightMode} - style={`width: ${width}px; flex-shrink: 0;`} - class:transparent={transparency} - > - {#if !disableActivityHistory && activityHistoryPosition === 'TOP' && activities.length > 0 && selectedYear === currentYear} - <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> - {/if} - <div class="categories-grid" style="padding-bottom: 0;"> - <Activity - {wrapped} - year={selectedYear} - {activities} - {useFullActivityHistory} - {updateWidth} - /> - <Anime animeList={calculatedAnimeList} {minutesWatched} {episodes} /> - <Manga mangaList={calculatedMangaList} {chapters} /> - </div> - {#if !disableActivityHistory && activityHistoryPosition === 'BELOW_TOP' && activities.length > 0 && selectedYear === currentYear} - <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> - {/if} - <MediaPanel - {animeList} - {mangaList} - {highestRatedMediaPercentage} - {highestRatedCount} - {updateWidth} - {wrapped} - {animeMostTitle} - {mangaMostTitle} - /> - {#if topMedia && topGenresTags && ((topMedia.topGenreMedia && topMedia.genres.length > 0) || (topMedia.topTagMedia && topMedia.tags.length > 0))} - <MediaExtras - {topMedia} - {genreTagTitle} - {highestRatedGenreTagPercentage} - {updateWidth} - /> - {/if} - {#if !disableActivityHistory && activityHistoryPosition === 'ORIGINAL' && activities.length > 0 && selectedYear === currentYear} - <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> - {/if} - {#if watermark} - <Watermark /> - {/if} - </div> - </div> - <div class="list"> - <div class:card={generated}> - <div id="wrapped-final" /> - - {#if generated} - <p /> - - <blockquote style="margin: 0 0 0 1.5rem;"> - Click on the image to download, or right click and select "Save Image As...". - </blockquote> - {/if} - </div> - - {#if generated} - <p /> - {/if} - - <div id="options" class="card"> - <button on:click={screenshot} data-umami-event="Generate Wrapped"> - Generate image - </button> - - <details class="no-shadow" open> - <summary>Display</summary> - - <input type="checkbox" bind:checked={watermark} /> Show watermark<br /> - <input type="checkbox" bind:checked={transparency} /> Enable background transparency<br - /> - <input type="checkbox" bind:checked={lightMode} /> - Enable light mode<br /> - <input type="checkbox" bind:checked={topGenresTags} /> - Show top genres and tags<br /> - <input - type="checkbox" - bind:checked={disableActivityHistory} - disabled={selectedYear !== currentYear} - /> - Hide activity history<br /> - <input type="checkbox" bind:checked={highestRatedMediaPercentage} /> Show highest - rated media percentages<br /> - <input type="checkbox" bind:checked={highestRatedGenreTagPercentage} /> Show highest - rated genre and tag percentages<br /> - <input type="checkbox" bind:checked={includeOngoingMediaFromPreviousYears} /> Show - ongoing media from previous years<br /> - <select bind:value={activityHistoryPosition}> - <option value="TOP">Above Top Row</option> - <option value="BELOW_TOP">Below Top Row</option> - <option value="ORIGINAL">Bottom</option> - </select> - Activity history position<br /> - <select bind:value={highestRatedCount}> - {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} - <option value={count}>{count}</option> - {/each} - </select> - Highest rated media count<br /> - <select bind:value={genreTagCount}> - {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} - <option value={count}>{count}</option> - {/each} - </select> - Highest genre and tag count<br /> - <button on:click={updateWidth}>Find best fit</button> - <button on:click={() => (width -= 25)}>-25px</button> - <button on:click={() => (width += 25)}>+25px</button> - Width adjustment<br /> - </details> - - <details class="no-shadow" open> - <summary>Calculation</summary> - - <input type="checkbox" bind:checked={useFullActivityHistory} /> - Enable full-year activity<button class="smaller-button" on:click={pruneFullYear} - >Refresh data</button - > - <br /> - <select bind:value={selectedYear}> - {#each Array.from({ length: currentYear - 2012 }) as _, i} - <option value={currentYear - i}> - {currentYear - i} - </option> - {/each} - </select> - Calculate for year<br /> - <select bind:value={mediaSort}> - <option value={SortOptions.SCORE}>Score</option> - <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> - </select> - Anime and manga sort<br /> - <select bind:value={genreTagsSort}> - <option value={SortOptions.SCORE}>Score</option> - <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> - <option value={SortOptions.COUNT}>Count</option> - </select> - Genre and tag sort<br /> - <input type="checkbox" bind:checked={includeMusic} /> Include music<br /> - <input type="checkbox" bind:checked={includeRepeats} /> Include rewatches & rereads<br - /> - <input type="checkbox" bind:checked={includeSpecials} /> Include specials<br /> - <input type="checkbox" bind:checked={includeOVAs} /> Include OVAs<br /> - <input type="checkbox" bind:checked={includeMovies} /> Include movies<br /> - <input - type="text" - bind:value={excludedKeywordsInput} - on:keypress={(e) => { - e.key === 'Enter' && submitExcludedKeywords(); - }} - /> - Excluded keywords - <button on:click={submitExcludedKeywords} title="Or click your Enter key" use:tooltip - >Submit</button - > - <br /> - <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> - </details> - </div> - </div> - </div> - {:catch} - <Error type="User" card list={false} /> - {/await} - {:catch} - <Error - card - type={`${useFullActivityHistory ? 'Full-year activity' : 'Activity'} history`} - loginSessionError={!useFullActivityHistory} - list={false} - > - {#if useFullActivityHistory} - <p> - With <b>many</b> activities, it may take multiple attempts to obtain all of your activity history - from AniList. If this occurs, wait one minute and try again to continue populating your local - activity history database. - </p> - {/if} - </Error> - {/await} + {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, $userIdentity, selectedYear) : getActivityHistory($userIdentity)} + <Message message="Loading activity history ..." /> + + <Skeleton count={2} /> + {:then activities} + {#await wrapped(user, $userIdentity, selectedYear)} + <Message message="Loading user data ..." /> + + <Skeleton count={2} /> + {:then wrapped} + <div id="list-container"> + <div class="card"> + <div + id="wrapped" + class:light-theme={lightMode} + style={`width: ${width}px; flex-shrink: 0;`} + class:transparent={transparency} + > + {#if !disableActivityHistory && activityHistoryPosition === 'TOP' && activities.length > 0 && selectedYear === currentYear} + <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> + {/if} + <div class="categories-grid" style="padding-bottom: 0;"> + <Activity + {wrapped} + year={selectedYear} + {activities} + {useFullActivityHistory} + {updateWidth} + /> + <Anime animeList={calculatedAnimeList} {minutesWatched} {episodes} /> + <Manga mangaList={calculatedMangaList} {chapters} /> + </div> + {#if !disableActivityHistory && activityHistoryPosition === 'BELOW_TOP' && activities.length > 0 && selectedYear === currentYear} + <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> + {/if} + <MediaPanel + {animeList} + {mangaList} + {highestRatedMediaPercentage} + {highestRatedCount} + {updateWidth} + {wrapped} + {animeMostTitle} + {mangaMostTitle} + /> + {#if topMedia && topGenresTags && ((topMedia.topGenreMedia && topMedia.genres.length > 0) || (topMedia.topTagMedia && topMedia.tags.length > 0))} + <MediaExtras + {topMedia} + {genreTagTitle} + {highestRatedGenreTagPercentage} + {updateWidth} + /> + {/if} + {#if !disableActivityHistory && activityHistoryPosition === 'ORIGINAL' && activities.length > 0 && selectedYear === currentYear} + <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> + {/if} + {#if watermark} + <Watermark /> + {/if} + </div> + </div> + <div class="list"> + <div class:card={generated}> + <div id="wrapped-final" /> + + {#if generated} + <p /> + + <blockquote style="margin: 0 0 0 1.5rem;"> + Click on the image to download, or right click and select "Save Image As...". + </blockquote> + {/if} + </div> + + {#if generated} + <p /> + {/if} + + <div id="options" class="card"> + <button on:click={screenshot} data-umami-event="Generate Wrapped"> + Generate image + </button> + + <details class="no-shadow" open> + <summary>Display</summary> + + <input type="checkbox" bind:checked={watermark} /> Show watermark<br /> + <input type="checkbox" bind:checked={transparency} /> Enable background transparency<br + /> + <input type="checkbox" bind:checked={lightMode} /> + Enable light mode<br /> + <input type="checkbox" bind:checked={topGenresTags} /> + Show top genres and tags<br /> + <input + type="checkbox" + bind:checked={disableActivityHistory} + disabled={selectedYear !== currentYear} + /> + Hide activity history<br /> + <input type="checkbox" bind:checked={highestRatedMediaPercentage} /> Show highest + rated media percentages<br /> + <input type="checkbox" bind:checked={highestRatedGenreTagPercentage} /> Show highest + rated genre and tag percentages<br /> + <input type="checkbox" bind:checked={includeOngoingMediaFromPreviousYears} /> Show + ongoing media from previous years<br /> + <select bind:value={activityHistoryPosition}> + <option value="TOP">Above Top Row</option> + <option value="BELOW_TOP">Below Top Row</option> + <option value="ORIGINAL">Bottom</option> + </select> + Activity history position<br /> + <select bind:value={highestRatedCount}> + {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} + <option value={count}>{count}</option> + {/each} + </select> + Highest rated media count<br /> + <select bind:value={genreTagCount}> + {#each [3, 4, 5, 6, 7, 8, 9, 10] as count} + <option value={count}>{count}</option> + {/each} + </select> + Highest genre and tag count<br /> + <button on:click={updateWidth}>Find best fit</button> + <button on:click={() => (width -= 25)}>-25px</button> + <button on:click={() => (width += 25)}>+25px</button> + Width adjustment<br /> + </details> + + <details class="no-shadow" open> + <summary>Calculation</summary> + + <input type="checkbox" bind:checked={useFullActivityHistory} /> + Enable full-year activity<button class="smaller-button" on:click={pruneFullYear} + >Refresh data</button + > + <br /> + <select bind:value={selectedYear}> + {#each Array.from({ length: currentYear - 2012 }) as _, i} + <option value={currentYear - i}> + {currentYear - i} + </option> + {/each} + </select> + Calculate for year<br /> + <select bind:value={mediaSort}> + <option value={SortOptions.SCORE}>Score</option> + <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> + </select> + Anime and manga sort<br /> + <select bind:value={genreTagsSort}> + <option value={SortOptions.SCORE}>Score</option> + <option value={SortOptions.MINUTES_WATCHED}>Minutes Watched/Read</option> + <option value={SortOptions.COUNT}>Count</option> + </select> + Genre and tag sort<br /> + <input type="checkbox" bind:checked={includeMusic} /> Include music<br /> + <input type="checkbox" bind:checked={includeRepeats} /> Include rewatches & rereads<br + /> + <input type="checkbox" bind:checked={includeSpecials} /> Include specials<br /> + <input type="checkbox" bind:checked={includeOVAs} /> Include OVAs<br /> + <input type="checkbox" bind:checked={includeMovies} /> Include movies<br /> + <input + type="text" + bind:value={excludedKeywordsInput} + on:keypress={(e) => { + e.key === 'Enter' && submitExcludedKeywords(); + }} + /> + Excluded keywords + <button on:click={submitExcludedKeywords} title="Or click your Enter key" use:tooltip + >Submit</button + > + <br /> + <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> + </details> + </div> + </div> + </div> + {:catch} + <Error type="User" card list={false} /> + {/await} + {:catch} + <Error + card + type={`${useFullActivityHistory ? 'Full-year activity' : 'Activity'} history`} + loginSessionError={!useFullActivityHistory} + list={false} + > + {#if useFullActivityHistory} + <p> + With <b>many</b> activities, it may take multiple attempts to obtain all of your activity history + from AniList. If this occurs, wait one minute and try again to continue populating your local + activity history database. + </p> + {/if} + </Error> + {/await} {:else} - <Message message="Loading user ..." /> + <Message message="Loading user ..." /> - <Skeleton count={2} /> + <Skeleton count={2} /> {/if} <style> - @import './wrapped.css'; + @import './wrapped.css'; </style> diff --git a/src/lib/Tools/Wrapped/Top/Activity.svelte b/src/lib/Tools/Wrapped/Top/Activity.svelte index 03f15b5d..a91bedfb 100644 --- a/src/lib/Tools/Wrapped/Top/Activity.svelte +++ b/src/lib/Tools/Wrapped/Top/Activity.svelte @@ -1,42 +1,42 @@ <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"> - <a href={`https://anilist.co/user/${$identity.name}`} target="_blank"> - <img src={proxy(wrapped.avatar.large)} alt="User Avatar" on:load={updateWidth} /> - </a> - <div> - <div> - <a href={`https://anilist.co/user/${$identity.name}`} target="_blank"> - <b> - {$identity.name} - </b> - </a> - </div> - <div> - Status Posts: {wrapped.activities.statusCount} - </div> - <div> - Messages: {wrapped.activities.messageCount} - </div> - <div> - Days Active: {#if year !== currentYear} - ?/365 - {:else} - {activities.length}/{useFullActivityHistory ? 365 : 189} - {/if} - </div> - </div> + <a href={`https://anilist.co/user/${$identity.name}`} target="_blank"> + <img src={proxy(wrapped.avatar.large)} alt="User Avatar" on:load={updateWidth} /> + </a> + <div> + <div> + <a href={`https://anilist.co/user/${$identity.name}`} target="_blank"> + <b> + {$identity.name} + </b> + </a> + </div> + <div> + Status Posts: {wrapped.activities.statusCount} + </div> + <div> + Messages: {wrapped.activities.messageCount} + </div> + <div> + Days Active: {#if year !== currentYear} + ?/365 + {:else} + {activities.length}/{useFullActivityHistory ? 365 : 189} + {/if} + </div> + </div> </div> diff --git a/src/lib/Tools/Wrapped/Top/Anime.svelte b/src/lib/Tools/Wrapped/Top/Anime.svelte index b33a8c08..08df7fd3 100644 --- a/src/lib/Tools/Wrapped/Top/Anime.svelte +++ b/src/lib/Tools/Wrapped/Top/Anime.svelte @@ -1,20 +1,20 @@ <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"> - <div class="grid-item"> - <b>Anime</b> - </div> - <div class="grid-item"> - Time Watched: {((minutesWatched || 0) / 60 / 24).toFixed(2)} days - </div> - <div class="grid-item"> - Completed: {animeList?.length || 0} - </div> - <div class="grid-item">Episodes: {episodes}</div> + <div class="grid-item"> + <b>Anime</b> + </div> + <div class="grid-item"> + Time Watched: {((minutesWatched || 0) / 60 / 24).toFixed(2)} days + </div> + <div class="grid-item"> + Completed: {animeList?.length || 0} + </div> + <div class="grid-item">Episodes: {episodes}</div> </div> diff --git a/src/lib/Tools/Wrapped/Top/Manga.svelte b/src/lib/Tools/Wrapped/Top/Manga.svelte index 6c505df2..a36f7724 100644 --- a/src/lib/Tools/Wrapped/Top/Manga.svelte +++ b/src/lib/Tools/Wrapped/Top/Manga.svelte @@ -1,22 +1,22 @@ <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"> - <div class="grid-item"> - <b>Manga</b> - </div> - <div class="grid-item"> - Time Read: {estimatedDayReading(chapters).toFixed(2)} days - </div> - <div class="grid-item"> - Completed: {mangaList?.length || 0} - </div> - <div class="grid-item"> - Chapters: {chapters} - </div> + <div class="grid-item"> + <b>Manga</b> + </div> + <div class="grid-item"> + Time Read: {estimatedDayReading(chapters).toFixed(2)} days + </div> + <div class="grid-item"> + Completed: {mangaList?.length || 0} + </div> + <div class="grid-item"> + Chapters: {chapters} + </div> </div> diff --git a/src/lib/Tools/Wrapped/Watermark.svelte b/src/lib/Tools/Wrapped/Watermark.svelte index 2e8dd838..f166d554 100644 --- a/src/lib/Tools/Wrapped/Watermark.svelte +++ b/src/lib/Tools/Wrapped/Watermark.svelte @@ -1,5 +1,5 @@ <div class="categories-grid" style="padding-top: 0;"> - <div class="category-grid pure-category" id="watermark"> - <a href="https://due.moe/wrapped" target="_blank">due.moe/wrapped</a> - </div> + <div class="category-grid pure-category" id="watermark"> + <a href="https://due.moe/wrapped" target="_blank">due.moe/wrapped</a> + </div> </div> diff --git a/src/lib/Tools/Wrapped/wrapped.css b/src/lib/Tools/Wrapped/wrapped.css index be42cd0a..c144a6f0 100644 --- a/src/lib/Tools/Wrapped/wrapped.css +++ b/src/lib/Tools/Wrapped/wrapped.css @@ -2,99 +2,99 @@ @import url('https://proxy.due.moe/?q=https://fonts.googleapis.com/css?family=Overpass:400,600,700,800'); .categories-grid { - display: flex; - flex-wrap: wrap; - row-gap: 1.5em; - column-gap: 1.5em; - padding: 2%; - justify-content: center; - font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, - Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - background-color: #0b1622; + display: flex; + flex-wrap: wrap; + row-gap: 1.5em; + column-gap: 1.5em; + padding: 2%; + justify-content: center; + font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + background-color: #0b1622; } .categories-grid b { - font-family: Overpass, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, - Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - font-weight: 600; + font-family: Overpass, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, + Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + font-weight: 600; } .category-grid, .image-grid { - background-color: #151f2e; - border-radius: 4px; - color: rgb(159, 173, 189); + background-color: #151f2e; + border-radius: 4px; + color: rgb(159, 173, 189); } .pure-category, .avatar-grid { - padding: 1.5%; + padding: 1.5%; } .category-grid { - display: grid; + display: grid; } .image-grid { - display: flex; - column-gap: 1em; - flex-wrap: wrap; + display: flex; + column-gap: 1em; + flex-wrap: wrap; } .image-grid img { - width: 6em; - height: auto; - border-radius: 3px; + width: 6em; + height: auto; + border-radius: 3px; } .categories-grid a { - text-decoration: none; - color: unset; + text-decoration: none; + color: unset; } .transparent .categories-grid { - background-color: transparent !important; + background-color: transparent !important; } .light-theme .categories-grid { - background-color: #edf1f5; + background-color: #edf1f5; } .light-theme .category-grid { - background-color: #fafafa; - color: rgb(92, 114, 138); + background-color: #fafafa; + color: rgb(92, 114, 138); } .light-theme .image-grid { - background-color: #fafafa; - color: rgb(92, 114, 138); + background-color: #fafafa; + color: rgb(92, 114, 138); } ol { - margin: 0 !important; + margin: 0 !important; } #watermark { - color: rgb(61, 180, 242); + color: rgb(61, 180, 242); } #wrapped-final { - height: auto; - width: 50%; + height: auto; + width: 50%; } #list-container { - display: flex; - gap: 1rem; - flex-wrap: wrap; - align-items: start; + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: start; } .list { - flex-grow: 1; - flex-basis: 1%; + flex-grow: 1; + flex-basis: 1%; } #wrapped { - overflow-y: scroll; + overflow-y: scroll; } diff --git a/src/lib/Tools/tools.ts b/src/lib/Tools/tools.ts index 2593ea49..b85ff7f6 100644 --- a/src/lib/Tools/tools.ts +++ b/src/lib/Tools/tools.ts @@ -2,92 +2,92 @@ import locale from '$stores/locale'; import { get } from 'svelte/store'; export const tools: { - [key: string]: { - name: () => string; - short?: string; - description?: () => string; - id: string; - }; + [key: string]: { + name: () => string; + short?: string; + description?: () => string; + id: string; + }; } = { - default: { - name: () => 'Tools', - description: () => 'A collection of tools to help you get the most out of AniList.', - id: 'default' - }, - wrapped: { - name: () => 'AniList Wrapped & Statistics Panel', - short: 'AniList Wrapped', - description: () => - 'Instantly generate an AniList themed Wrapped for your profile, doubling as a statistics panel for your bio', - id: 'wrapped' - }, - birthdays: { - name: () => { - return get(locale)().tools.tool.characterBirthdays.long; - }, - description: () => - 'Find and display the birthdays of all characters for today, or any other day of the year', - id: 'birthdays' - }, - sequel_spy: { - name: () => 'Sequel Spy', - description: () => - "Find media with prequels you haven't seen yet for any given simulcast season", - id: 'sequel_spy' - }, - uma_musume_birthdays: { - name: () => { - return 'Uma Musume: Pretty Derby Character Birthdays'; - }, - description: () => - 'Find and display the birthdays of all Uma Musume characters for today, or any other day of the year', - id: 'uma_musume_birthdays' - }, - hololive_birthdays: { - name: () => 'hololive Birthdays', - description: () => - 'Find and display the birthdays of all hololive talents for today, or any other day of the year', - id: 'hololive_birthdays' - }, - hayai: { - name: () => '早い', - description: () => 'Read light novels at 1.5x speed!', - id: 'hayai' - }, - discussions: { - name: () => 'Episode Discussion Collector', - description: () => 'Find and display all episode discussions created by a given user', - id: 'discussions' - }, - random_follower: { - name: () => 'Random Follower Finder', - description: () => "Generate random followers from any user's following list", - id: 'random_follower' - }, - dump_profile: { - name: () => 'Dump Profile', - description: () => "Dump a user's profile to JSON", - id: 'dump_profile' - }, - likes: { - name: () => 'Likes', - description: () => 'Get all likes of an activity or forum thread', - id: 'likes' - }, - activity_history: { - name: () => 'Activity History Analyser', - id: 'activity_history', - description: () => 'Activity history utilities & image exporter' - }, - girls: { - name: () => 'Anime Girls Holding Programming Books', - id: 'girls', - description: () => 'Find anime girls holding programming books by language' - }, - sequel_catcher: { - name: () => 'Sequel Catcher', - description: () => - 'Check if any completed anime on your lists have sequels you have not yet seen', - id: 'sequel_catcher' - } + default: { + name: () => 'Tools', + description: () => 'A collection of tools to help you get the most out of AniList.', + id: 'default' + }, + wrapped: { + name: () => 'AniList Wrapped & Statistics Panel', + short: 'AniList Wrapped', + description: () => + 'Instantly generate an AniList themed Wrapped for your profile, doubling as a statistics panel for your bio', + id: 'wrapped' + }, + birthdays: { + name: () => { + return get(locale)().tools.tool.characterBirthdays.long; + }, + description: () => + 'Find and display the birthdays of all characters for today, or any other day of the year', + id: 'birthdays' + }, + sequel_spy: { + name: () => 'Sequel Spy', + description: () => + "Find media with prequels you haven't seen yet for any given simulcast season", + id: 'sequel_spy' + }, + uma_musume_birthdays: { + name: () => { + return 'Uma Musume: Pretty Derby Character Birthdays'; + }, + description: () => + 'Find and display the birthdays of all Uma Musume characters for today, or any other day of the year', + id: 'uma_musume_birthdays' + }, + hololive_birthdays: { + name: () => 'hololive Birthdays', + description: () => + 'Find and display the birthdays of all hololive talents for today, or any other day of the year', + id: 'hololive_birthdays' + }, + hayai: { + name: () => '早い', + description: () => 'Read light novels at 1.5x speed!', + id: 'hayai' + }, + discussions: { + name: () => 'Episode Discussion Collector', + description: () => 'Find and display all episode discussions created by a given user', + id: 'discussions' + }, + random_follower: { + name: () => 'Random Follower Finder', + description: () => "Generate random followers from any user's following list", + id: 'random_follower' + }, + dump_profile: { + name: () => 'Dump Profile', + description: () => "Dump a user's profile to JSON", + id: 'dump_profile' + }, + likes: { + name: () => 'Likes', + description: () => 'Get all likes of an activity or forum thread', + id: 'likes' + }, + activity_history: { + name: () => 'Activity History Analyser', + id: 'activity_history', + description: () => 'Activity history utilities & image exporter' + }, + girls: { + name: () => 'Anime Girls Holding Programming Books', + id: 'girls', + description: () => 'Find anime girls holding programming books by language' + }, + sequel_catcher: { + name: () => 'Sequel Catcher', + description: () => + 'Check if any completed anime on your lists have sequels you have not yet seen', + id: 'sequel_catcher' + } }; |