diff options
| author | Fuwn <[email protected]> | 2023-12-22 03:07:04 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2023-12-22 03:11:24 -0800 |
| commit | a7255393ac86b091772189469fc1806ded1595d1 (patch) | |
| tree | c238fcd2d5fa3302f195f9ee76d0d2dbbe1da43f /src | |
| parent | fix(wrapped): absolute best updateWidth() (diff) | |
| download | due.moe-a7255393ac86b091772189469fc1806ded1595d1.tar.xz due.moe-a7255393ac86b091772189469fc1806ded1595d1.zip | |
feat(wrapped): full-year activity history
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/AniList/activity.ts | 95 | ||||
| -rw-r--r-- | src/lib/Tools/ActivityHistory.svelte | 22 | ||||
| -rw-r--r-- | src/lib/Tools/ActivityHistoryGrid.svelte | 63 | ||||
| -rw-r--r-- | src/lib/Tools/Wrapped.svelte | 378 |
4 files changed, 322 insertions, 236 deletions
diff --git a/src/lib/AniList/activity.ts b/src/lib/AniList/activity.ts index 1f99faca..a1bfedc6 100644 --- a/src/lib/AniList/activity.ts +++ b/src/lib/AniList/activity.ts @@ -1,4 +1,4 @@ -import type { UserIdentity } from './identity'; +import type { AniListAuthorisation, UserIdentity } from './identity'; export interface ActivityHistoryEntry { date: number; @@ -90,3 +90,96 @@ export const lastActivityDate = async (userIdentity: UserIdentity): Promise<Date return date; }; + +interface ActivitiesPage { + data: { + Page: { + pageInfo: { + hasNextPage: boolean; + }; + activities: { + createdAt: number; + }[]; + }; + }; +} + +const activitiesPage = async ( + page: number, + anilistAuthorisation: AniListAuthorisation, + userIdentity: UserIdentity +): Promise<ActivitiesPage> => + await ( + await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { + Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ + query: `{ + Page(page: ${page}) { + pageInfo { hasNextPage } + activities(userId: ${userIdentity.id}, createdAt_greater: ${Math.floor( + new Date(new Date().getFullYear(), 0, 1).getTime() / 1000 + )}, createdAt_lesser: ${Math.floor( + new Date(new Date().getFullYear(), 6, 1).getTime() / 1000 + )}) { + ... on TextActivity { createdAt } + ... on ListActivity { createdAt } + ... on MessageActivity { createdAt } + } + } + }` + }) + }) + ).json(); + +export const fullActivityHistory = async ( + anilistAuthorisation: AniListAuthorisation, + userIdentity: UserIdentity +): Promise<ActivityHistoryEntry[]> => { + const activities: ActivityHistoryEntry[] = []; + let page = 1; + let currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity); + + for (const activity of currentPage.data.Page.activities) activities.push(activity); + + while (currentPage['data']['Page']['pageInfo']['hasNextPage']) { + for (const activity of currentPage.data.Page.activities) activities.push(activity); + + page += 1; + currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity); + } + + let fullLocalActivityHistory: ActivityHistoryEntry[] = []; + + for (const activity of activities) { + const date = new Date(activity.createdAt * 1000); + const dateString = date.toDateString(); + + const activityHistoryEntry = fullLocalActivityHistory.find( + (activityHistoryEntry) => + new Date(activityHistoryEntry.date * 1000).toDateString() === dateString + ); + + if (activityHistoryEntry) activityHistoryEntry.amount += 1; + else fullLocalActivityHistory.push({ date: Math.floor(date.getTime() / 1000), amount: 1 }); + } + + fullLocalActivityHistory = fullLocalActivityHistory.filter((a) => !isNaN(a.date)); + + fullLocalActivityHistory.push(...(await activityHistory(userIdentity))); + + fullLocalActivityHistory = fullLocalActivityHistory.filter( + (activityHistoryEntry, index, self) => + self.findIndex( + (a) => + new Date(a.date * 1000).toDateString() === + new Date(activityHistoryEntry.date * 1000).toDateString() + ) === index + ); + + return fullLocalActivityHistory; +}; diff --git a/src/lib/Tools/ActivityHistory.svelte b/src/lib/Tools/ActivityHistory.svelte index 17067ec4..8194ad0a 100644 --- a/src/lib/Tools/ActivityHistory.svelte +++ b/src/lib/Tools/ActivityHistory.svelte @@ -12,6 +12,7 @@ } from '$lib/AniList/identity'; import { clearAllParameters } from './tool.js'; import { domToBlob } from 'modern-screenshot'; + import ActivityHistoryGrid from './ActivityHistoryGrid.svelte'; export let user: AniListAuthorisation; @@ -88,25 +89,8 @@ Loading ... {:else} {@const filledActivities = fillMissingDays(activities)} - {@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" - title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ - activity.amount - }`} - /> - {/each} - </div> + + <ActivityHistoryGrid {user} /> <p /> diff --git a/src/lib/Tools/ActivityHistoryGrid.svelte b/src/lib/Tools/ActivityHistoryGrid.svelte index b0f782b5..45d53342 100644 --- a/src/lib/Tools/ActivityHistoryGrid.svelte +++ b/src/lib/Tools/ActivityHistoryGrid.svelte @@ -1,8 +1,8 @@ <script lang="ts"> import { - activityHistory, fillMissingDays, - type ActivityHistoryEntry + type ActivityHistoryEntry, + activityHistory } from '$lib/AniList/activity.js'; import { onMount } from 'svelte'; import userIdentity from '../../stores/userIdentity.js'; @@ -13,8 +13,9 @@ import { clearAllParameters } from './tool.js'; export let user: AniListAuthorisation; + export let activityData: ActivityHistoryEntry[] | null = null; - let activityHistoryData: Promise<ActivityHistoryEntry[]>; + let activityHistoryData: ActivityHistoryEntry[]; let currentUserIdentity = { name: '', id: -1 }; let baseHue = Math.floor(Math.random() * 360); @@ -26,16 +27,10 @@ currentUserIdentity = JSON.parse($userIdentity); currentUserIdentity.name = currentUserIdentity.name; - activityHistoryData = activityHistory(currentUserIdentity); + activityHistoryData = activityData || (await activityHistory(currentUserIdentity)); } }); - // const incrementDate = (date: Date): Date => { - // date.setDate(date.getDate() + 1); - - // return date; - // }; - const gradientColour = (amount: number, maxAmount: number, baseHue: number) => { const lightness = 100 - Math.round((amount / maxAmount) * 50); @@ -45,35 +40,29 @@ {#if user === undefined} Please log in to view this page. +{:else if activityHistoryData === undefined} + Loading ... {:else} - {#await activityHistoryData} - Loading ... - {:then activities} - {#if activities === undefined} - Loading ... - {:else} - {@const filledActivities = fillMissingDays(activities)} - {@const highestActivity = Math.max(...filledActivities.map((activity) => activity.amount))} + {@const filledActivities = fillMissingDays(activityHistoryData)} + {@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" - title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ - activity.amount - }`} - /> - {/each} - </div> - {/if} - {/await} + <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" + title={`Date: ${new Date(activity.date * 1000).toLocaleDateString()}\nAmount: ${ + activity.amount + }`} + /> + {/each} + </div> {/if} <style> diff --git a/src/lib/Tools/Wrapped.svelte b/src/lib/Tools/Wrapped.svelte index dbd7cc1e..a3db5a06 100644 --- a/src/lib/Tools/Wrapped.svelte +++ b/src/lib/Tools/Wrapped.svelte @@ -6,7 +6,11 @@ } from '$lib/AniList/identity'; import { onMount } from 'svelte'; import { wrapped } from '$lib/AniList/wrapped.js'; - import { activityHistory as getActivityHistory, fillMissingDays } from '$lib/AniList/activity.js'; + import { + activityHistory, + fullActivityHistory, + activityHistory as getActivityHistory + } from '$lib/AniList/activity.js'; import { Type, mediaListCollection, type Media } from '$lib/AniList/media.js'; import anime from '../../stores/anime.js'; import lastPruneTimes from '../../stores/lastPruneTimes.js'; @@ -45,6 +49,7 @@ let disableActivityHistory = true; let excludedKeywordsInput = ''; let excludedKeywords: string[] = []; + let useFullActivityHistory = false; $: { if (browser && mounted) { @@ -99,6 +104,7 @@ const reset = () => { let topWidths = 0; let middleWidths = 0; + let bottomWidths = 0; wrappedContainer.querySelectorAll('.category').forEach((item) => { const category = item as HTMLElement; @@ -116,10 +122,14 @@ topWidths += width; } else if (category.classList.contains('middle-category')) { middleWidths += width; + } else if (category.classList.contains('bottom-category')) { + bottomWidths += width; } }); - const requiredWidth = topWidths > middleWidths ? topWidths : middleWidths; + let requiredWidth = topWidths > middleWidths ? topWidths : middleWidths; + + if (bottomWidths > requiredWidth) requiredWidth = bottomWidths; wrappedContainer.style.width = `${requiredWidth}px`; width = requiredWidth; @@ -330,210 +340,220 @@ return true; }); }; + + const nbsp = (str: string) => str.replace(/ /g, ' '); </script> {#if currentUserIdentity.id === -2} Please log in to view this page. {:else if currentUserIdentity.id !== -1} - {#await wrapped(user, currentUserIdentity)} - Loading ... - {:then wrapped} - <div - id="wrapped" - class:light-theme={lightMode} - style={`width: ${width}px;`} - class:transparent={transparency} - > - <div class="categories-grid" style="padding-bottom: 0;"> - <div class="grid-item image-grid avatar-grid category top-category"> - <a href={`https://anilist.co/user/${currentUserIdentity.name}`} target="_blank"> - <img src={proxy(wrapped.avatar.large)} alt="User Avatar" on:load={updateWidth} /> - </a> - <div> + {#await useFullActivityHistory ? fullActivityHistory(user, currentUserIdentity) : getActivityHistory(currentUserIdentity)} + {@html nbsp(`Loading${useFullActivityHistory ? ' full-year' : ''} activity history ...`)} + {:then activities} + {#await wrapped(user, currentUserIdentity)} + Loading ... + {:then wrapped} + <div + id="wrapped" + class:light-theme={lightMode} + style={`width: ${width}px;`} + class:transparent={transparency} + > + <div class="categories-grid" style="padding-bottom: 0;"> + <div class="grid-item image-grid avatar-grid category top-category"> + <a href={`https://anilist.co/user/${currentUserIdentity.name}`} target="_blank"> + <img src={proxy(wrapped.avatar.large)} alt="User Avatar" on:load={updateWidth} /> + </a> <div> - <a href={`https://anilist.co/user/${currentUserIdentity.name}`} target="_blank"> - <b> - {currentUserIdentity.name} - </b> - </a> + <div> + <a href={`https://anilist.co/user/${currentUserIdentity.name}`} target="_blank"> + <b> + {currentUserIdentity.name} + </b> + </a> + </div> + <div> + Status Posts: {wrapped.activities.statusCount} + </div> + <div> + Messages: {wrapped.activities.messageCount} + </div> + <div> + Days Active: {activities.length}/{useFullActivityHistory ? 365 : 189} + </div> </div> - <div> - Status Posts: {wrapped.activities.statusCount} + </div> + <div class="category-grid pure-category category top-category"> + <div class="grid-item"> + <b>Anime</b> </div> - <div> - Messages: {wrapped.activities.messageCount} + <div class="grid-item"> + Time Watched: {((minutesWatched || 0) / 60 / 24).toFixed(2)} days </div> - <div> - Days Active: {#await getActivityHistory(currentUserIdentity)} - Loading ... - {:then activities} - {#if activities === undefined} - Loading ... - {:else} - {fillMissingDays(activities, true).filter((a) => a.amount !== 0).length}/365 - {/if} - {/await} + <div class="grid-item"> + Completed: {animeList?.length} </div> + <div class="grid-item">Episodes: {episodes}</div> </div> - </div> - <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} + <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} + </div> + <div class="grid-item"> + Chapters: {chapters} + </div> </div> - <div class="grid-item">Episodes: {episodes}</div> </div> - <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} - </div> - <div class="grid-item"> - Chapters: {chapters} + <div class="categories-grid"> + <div class="category-grid pure-category category middle-category"> + <div class="grid-item image-grid"> + {#if animeList !== undefined} + <a href={`https://anilist.co/anime/${animeList[0].id}`} target="_blank"> + <img + src={proxy(animeList[0].coverImage.extraLarge)} + alt="Highest Rated Anime Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>Highest Rated Anime</b> + <ol> + {#each animeList?.slice(0, highestRatedCount) as anime} + <li> + <a href={`https://anilist.co/anime/${anime.id}`} target="_blank"> + {anime.title.english || anime.title.romaji || anime.title.native} + </a> + </li> + {/each} + </ol> + </div> + {:else} + Loading ... + {/if} + </div> </div> - </div> - </div> - <div class="categories-grid"> - <div class="category-grid pure-category category middle-category"> - <div class="grid-item image-grid"> - {#if animeList !== undefined} - <a href={`https://anilist.co/anime/${animeList[0].id}`} target="_blank"> - <img - src={proxy(animeList[0].coverImage.extraLarge)} - alt="Highest Rated Anime Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>Highest Rated Anime</b> - <ol> - {#each animeList?.slice(0, highestRatedCount) as anime} - <li> - <a href={`https://anilist.co/anime/${anime.id}`} target="_blank"> - {anime.title.english || anime.title.romaji || anime.title.native} - </a> - </li> - {/each} - </ol> - </div> - {:else} - Loading ... - {/if} + <div class="category-grid pure-category category middle-category"> + <div class="grid-item image-grid"> + {#if mangaList !== undefined} + <a href={`https://anilist.co/manga/${mangaList[0].id}`} target="_blank"> + <img + src={proxy(mangaList[0].coverImage.extraLarge)} + alt="Highest Rated Manga Cover" + class="cover-image" + on:load={updateWidth} + /> + </a> + <div> + <b>Highest Rated Manga</b> + <ol> + {#each mangaList?.slice(0, highestRatedCount) as manga} + <li> + <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> + {manga.title.english || manga.title.romaji || manga.title.native} + </a> + </li> + {/each} + </ol> + </div> + {:else} + Loading ... + {/if} + </div> </div> </div> - <div class="category-grid pure-category category middle-category"> - <div class="grid-item image-grid"> - {#if mangaList !== undefined} - <a href={`https://anilist.co/manga/${mangaList[0].id}`} target="_blank"> - <img - src={proxy(mangaList[0].coverImage.extraLarge)} - alt="Highest Rated Manga Cover" - class="cover-image" - on:load={updateWidth} - /> - </a> - <div> - <b>Highest Rated Manga</b> - <ol> - {#each mangaList?.slice(0, highestRatedCount) as manga} - <li> - <a href={`https://anilist.co/manga/${manga.id}`} target="_blank"> - {manga.title.english || manga.title.romaji || manga.title.native} - </a> - </li> - {/each} - </ol> + {#if !disableActivityHistory} + <div class="categories-grid bottom-category" style="padding-top: 0;"> + <div class="category-grid pure-category"> + <div id="activity-history"> + <ActivityHistoryGrid {user} activityData={activities} /> </div> - {:else} - Loading ... - {/if} - </div> - </div> - </div> - {#if !disableActivityHistory} - <div class="categories-grid" style="padding-top: 0;"> - <div class="category-grid pure-category"> - <div id="activity-history"> - <ActivityHistoryGrid {user} /> </div> </div> - </div> - {/if} - {#if watermark} - <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> + {/if} + {#if watermark} + <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> - </div> - {/if} - </div> - - <p /> - - <p> - <a href={'#'} on:click={screenshot}>Generate image</a> - </p> - - <details> - <summary>Options</summary> - <div id="options"> - <input type="checkbox" bind:checked={watermark} /> Enable 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={disableActivityHistory} /> Disable activity history<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 and OVAs<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 /> - <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 /> - <input - type="text" - bind:value={excludedKeywordsInput} - on:keypress={(e) => { - e.key === 'Enter' && submitExcludedKeywords(); - }} - /> - Excluded keywords - <a href={`#`} on:click={submitExcludedKeywords} title="Or click your Enter key">Submit</a> - <br /> - <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> + {/if} </div> - </details> - <p /> + <p /> - <div id="wrapped-final" /> + <p> + <a href={'#'} on:click={screenshot}>Generate image</a> + </p> + + <details> + <summary>Options</summary> + <div id="options"> + <input type="checkbox" bind:checked={watermark} /> Enable 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={disableActivityHistory} /> Disable activity history<br + /> + {#if !disableActivityHistory} + <input type="checkbox" bind:checked={useFullActivityHistory} /> + Enable full-year activity<br /> + <SettingHint> + If you have many activities, this <b>will</b> cause heavy rate-limiting + </SettingHint><br /> + {/if} + + <p /> + + <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 and OVAs<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 /> + <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 /> + <input + type="text" + bind:value={excludedKeywordsInput} + on:keypress={(e) => { + e.key === 'Enter' && submitExcludedKeywords(); + }} + /> + Excluded keywords + <a href={`#`} on:click={submitExcludedKeywords} title="Or click your Enter key">Submit</a> + <br /> + <SettingHint>Comma separated list (e.g., "My Hero, Kaguya")</SettingHint> + </div> + </details> - {#if generated} <p /> - <blockquote> - Click on the image to download, or right click and select "Save Image As...". - </blockquote> - {/if} + <div id="wrapped-final" /> + + {#if generated} + <p /> + + <blockquote> + Click on the image to download, or right click and select "Save Image As...". + </blockquote> + {/if} + {:catch} + <Error /> + {/await} {:catch} - <Error /> + <Error type={'Full-year activity history'} loginSessionError={false} /> {/await} {:else} Loading ... |