diff options
| author | Fuwn <[email protected]> | 2024-01-21 17:56:22 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-01-21 17:56:22 -0800 |
| commit | 9d1f8edc9ea2871fd234a15140a52726b8ffb583 (patch) | |
| tree | d4aec25b367482b838931d30ea0edb339074ec9e /src/lib/Tools/Wrapped/Tool.svelte | |
| parent | refactor(schedule): move module out of tools (diff) | |
| download | due.moe-9d1f8edc9ea2871fd234a15140a52726b8ffb583.tar.xz due.moe-9d1f8edc9ea2871fd234a15140a52726b8ffb583.zip | |
refactor(wrapped): move tool to module
Diffstat (limited to 'src/lib/Tools/Wrapped/Tool.svelte')
| -rw-r--r-- | src/lib/Tools/Wrapped/Tool.svelte | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte new file mode 100644 index 00000000..b2f715cb --- /dev/null +++ b/src/lib/Tools/Wrapped/Tool.svelte @@ -0,0 +1,790 @@ +<script lang="ts"> + import userIdentity from '$stores/userIdentity'; + import { + userIdentity as getUserIdentity, + type AniListAuthorisation + } from '$lib/AniList/identity'; + import { onMount } from 'svelte'; + import { tops, wrapped, type TopMedia, SortOptions } from '$lib/AniList/wrapped'; + import { + fullActivityHistory, + activityHistory as getActivityHistory + } from '$lib/AniList/activity'; + import { Type, mediaListCollection, type Media } from '$lib/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 { nbsp } from '../../Utility/html'; + import SettingHint from '$lib/Settings/SettingHint.svelte'; + import { database } from '$lib/Database/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 Loading from '$lib/Utility/Loading.svelte'; + + export let user: AniListAuthorisation; + + const currentYear = new Date(Date.now()).getFullYear(); + let selectedYear = new Date(Date.now()).getFullYear(); + let currentUserIdentity = { + name: '', + id: -1, + avatar: 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/default.png' + }; + 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'; + } + + if (user !== undefined) { + if ($userIdentity === '') userIdentity.set(JSON.stringify(await getUserIdentity(user))); + + currentUserIdentity = JSON.parse($userIdentity); + currentUserIdentity.name = currentUserIdentity.name; + } else currentUserIdentity.id = -2; + + await update().then(() => (mounted = true)); + }); + + const update = async () => { + if (currentUserIdentity.id === -1) return; + + let rawAnimeList = await mediaListCollection( + user, + currentUserIdentity, + 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, + currentUserIdentity, + 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 currentUserIdentity.id === -2} + <div class="card">Please log in to view this page.</div> +{:else if currentUserIdentity.id !== -1} + {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, currentUserIdentity, selectedYear) : getActivityHistory(currentUserIdentity)} + <Loading> + {@html nbsp(`Loading${useFullActivityHistory ? ' full-year' : ''} activity history ...`)} + </Loading> + {:then activities} + {#await wrapped(user, currentUserIdentity, selectedYear)} + <Loading> + {@html nbsp('Loading user data ...')} + </Loading> + {: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} + identity={currentUserIdentity} + 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" + >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} + <Loading> + {@html nbsp('Loading user identity ...')} + </Loading> +{/if} + +<style> + @import './wrapped.css'; +</style> |