diff options
| author | Fuwn <[email protected]> | 2025-12-16 03:37:26 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-12-16 03:37:26 -0800 |
| commit | 15e8abbcc05a7005da1e5bdecc0a81ec1bed83a2 (patch) | |
| tree | 8dd64e19140fbe706ee614de4bfed03cd33194f7 /src | |
| parent | format: Apply Prettier formatting (diff) | |
| download | due.moe-15e8abbcc05a7005da1e5bdecc0a81ec1bed83a2.tar.xz due.moe-15e8abbcc05a7005da1e5bdecc0a81ec1bed83a2.zip | |
feat(Wrapped): Don't automatically fetch remote data
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/Tools/Wrapped/Tool.svelte | 645 |
1 files changed, 426 insertions, 219 deletions
diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte index bcc33f68..f060f444 100644 --- a/src/lib/Tools/Wrapped/Tool.svelte +++ b/src/lib/Tools/Wrapped/Tool.svelte @@ -2,10 +2,17 @@ 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 { + tops, + wrapped, + type TopMedia, + SortOptions, + type Wrapped + } from '$lib/Data/AniList/wrapped'; import { fullActivityHistory, - activityHistory as getActivityHistory + activityHistory as getActivityHistory, + type ActivityHistoryEntry } from '$lib/Data/AniList/activity'; import { Type, mediaListCollection, type Media } from '$lib/Data/AniList/media'; import anime from '$stores/anime'; @@ -74,6 +81,15 @@ let startDateFilter: Date | null = null; let endDateFilter: Date | null = null; let dateTicked = false; + let shouldFetchData = false; + let needsRefetch = false; + let dataFetched = false; + let fetchKey = 0; + let lastSelectedYear = selectedYear; + let lastUseFullActivityHistory = useFullActivityHistory; + let lastDisableLoopingActivityCounter = disableLoopingActivityCounter; + let lastStartDateFilter: Date | null = startDateFilter; + let lastEndDateFilter: Date | null = endDateFilter; $: { if (browser && mounted) { @@ -108,6 +124,26 @@ history.replaceState(null, '', `?${$page.url.searchParams.toString()}`); } } + + $: { + if (dataFetched) { + const yearChanged = selectedYear !== lastSelectedYear; + const fullActivityChanged = useFullActivityHistory !== lastUseFullActivityHistory; + const loopingChanged = disableLoopingActivityCounter !== lastDisableLoopingActivityCounter; + const startDateChanged = startDateFilter !== lastStartDateFilter; + const endDateChanged = endDateFilter !== lastEndDateFilter; + + if ( + yearChanged || + fullActivityChanged || + loopingChanged || + startDateChanged || + endDateChanged + ) + needsRefetch = true; + } + } + $: { includeMusic = includeMusic; includeSpecials = includeSpecials; @@ -285,6 +321,101 @@ await update().then(() => (mounted = true)); }); + const triggerFetch = () => { + shouldFetchData = true; + needsRefetch = false; + dataFetched = true; + fetchKey += 1; + lastSelectedYear = selectedYear; + lastUseFullActivityHistory = useFullActivityHistory; + lastDisableLoopingActivityCounter = disableLoopingActivityCounter; + lastStartDateFilter = startDateFilter; + lastEndDateFilter = endDateFilter; + }; + + const createDummyMedia = (type: 'ANIME' | 'MANGA'): Media => ({ + id: 0, + idMal: 0, + status: 'FINISHED', + type, + episodes: type === 'ANIME' ? 0 : 0, + chapters: type === 'MANGA' ? 0 : 0, + volumes: 0, + duration: 0, + format: type === 'ANIME' ? 'TV' : 'MANGA', + title: { + romaji: '...', + english: '...', + native: '...' + }, + synonyms: [], + mediaListEntry: { + progress: 0, + progressVolumes: 0, + status: 'COMPLETED', + score: 0, + repeat: 0, + startedAt: { + year: 0, + month: 0, + day: 0 + }, + completedAt: { + year: 0, + month: 0, + day: 0 + }, + createdAt: 0, + updatedAt: 0, + customLists: new Map() + }, + startDate: { + year: 0, + month: 0 + }, + endDate: { + year: 0, + month: 0 + }, + coverImage: { + extraLarge: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg', + medium: 'https://s4.anilist.co/file/anilistcdn/staff/large/default.jpg' + }, + tags: [], + genres: [], + season: 'WINTER', + isAdult: false, + relations: { + edges: [] + } + }); + + const dummyWrapped: Wrapped = { + statistics: { + anime: { + startYears: [], + genres: [], + tags: [] + }, + manga: { + startYears: [], + genres: [], + tags: [] + } + }, + activities: { + statusCount: 0, + messageCount: 0 + }, + avatar: { + large: 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/3.jpg' + } + }; + + const dummyActivities: ActivityHistoryEntry[] = []; + const dummyAnimeList: Media[] = [createDummyMedia('ANIME')]; + const dummyMangaList: Media[] = [createDummyMedia('MANGA')]; + const update = async () => { if ($userIdentity.id === -1) return; @@ -614,235 +745,311 @@ {#if $userIdentity.id === -2 || user === undefined} <LogInRestricted /> {:else if $userIdentity.id !== -1} - {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, $userIdentity, selectedYear, disableLoopingActivityCounter) : getActivityHistory($userIdentity)} - <Message message="Loading activity history ..." /> - - <Skeleton count={2} /> - {:then activities} - {#await wrapped(user, $userIdentity, selectedYear, false, disableLoopingActivityCounter)} - <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 id="list-container"> + {#if shouldFetchData} + {#key fetchKey} + {#await selectedYear !== currentYear || useFullActivityHistory || new Date().getMonth() <= 6 ? fullActivityHistory(user, $userIdentity, selectedYear, disableLoopingActivityCounter) : getActivityHistory($userIdentity)} + <Message message="Loading activity history ..." /> + + <Skeleton count={2} /> + {:then activities} + {#await wrapped(user, $userIdentity, selectedYear, false, disableLoopingActivityCounter)} + <Message message="Loading user data ..." /> + + <Skeleton count={2} /> + {:then wrapped} + <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> - {#if !disableActivityHistory && activityHistoryPosition === 'BELOW_TOP' && activities.length > 0 && selectedYear === currentYear} - <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} /> + {: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} - <MediaPanel - {animeList} - {mangaList} - {highestRatedMediaPercentage} - {highestRatedCount} + </Error> + {/await} + {/key} + {:else} + <div class="card"> + <div + id="wrapped" + class:light-theme={lightMode} + style={`width: ${width}px; flex-shrink: 0;`} + class:transparent={transparency} + > + <div class="categories-grid" style="padding-bottom: 0;"> + <Activity + wrapped={dummyWrapped} + year={selectedYear} + activities={dummyActivities} + {useFullActivityHistory} {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} + <Anime animeList={dummyAnimeList} minutesWatched={0} episodes={0} /> + <Manga mangaList={dummyMangaList} chapters={0} /> </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 /> - <input - type="date" - bind:value={startDateFilter} - placeholder="Start date" - on:change={() => { - dateTicked = true; - - update(); + <MediaPanel + animeList={dummyAnimeList} + mangaList={dummyMangaList} + {highestRatedMediaPercentage} + {highestRatedCount} + {updateWidth} + wrapped={dummyWrapped} + {animeMostTitle} + {mangaMostTitle} + /> + + <div class="categories-grid" style="padding-top: 0;"> + <div class="category-grid pure-category"> + <a + href={'#'} + on:click={(e) => { + e.preventDefault(); + triggerFetch(); }} - /> - Start date filter<br /> - <input - type="date" - bind:value={endDateFilter} - placeholder="End date" - on:change={() => { - dateTicked = true; - - update(); - }} - /> - End date filter<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="checkbox" bind:checked={excludeUnratedUnwatched} /> Excluded unrated & - unwatched<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 + id="watermark" + data-umami-event="Load Wrapped Data" > - <br /> - <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> - </details> - - <details class="no-shadow" open> - <summary>Advanced</summary> - - <input type="checkbox" bind:checked={disableLoopingActivityCounter} /> - Disable detailed activity information - </details> + Click load data! + </a> + </div> </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} + <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} - </Error> - {/await} + + <div id="options" class="card"> + <button on:click={screenshot} data-umami-event="Generate Wrapped"> Generate image </button> + {#if !shouldFetchData} + <button on:click={triggerFetch} data-umami-event="Load Wrapped Data"> Load data </button> + {:else if needsRefetch} + <button on:click={triggerFetch} data-umami-event="Refetch Wrapped Data"> + Reload data + </button> + {/if} + + <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} disabled={needsRefetch} /> + Enable full-year activity<button class="smaller-button" on:click={pruneFullYear} + >Refresh data</button + > + <br /> + <select bind:value={selectedYear} disabled={needsRefetch}> + {#each Array.from({ length: currentYear - 2012 }) as _, i} + <option value={currentYear - i}> + {currentYear - i} + </option> + {/each} + </select> + Calculate for year<br /> + <input + type="date" + bind:value={startDateFilter} + placeholder="Start date" + disabled={needsRefetch} + on:change={() => { + dateTicked = true; + + update(); + }} + /> + Start date filter<br /> + <input + type="date" + bind:value={endDateFilter} + placeholder="End date" + disabled={needsRefetch} + on:change={() => { + dateTicked = true; + + update(); + }} + /> + End date filter<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="checkbox" bind:checked={excludeUnratedUnwatched} /> Excluded unrated & + unwatched<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> + + <details class="no-shadow" open> + <summary>Advanced</summary> + + <input + type="checkbox" + bind:checked={disableLoopingActivityCounter} + disabled={needsRefetch} + /> + Disable detailed activity information + </details> + </div> + </div> + </div> {:else} <Message message="Loading user ..." /> |