From a7255393ac86b091772189469fc1806ded1595d1 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Fri, 22 Dec 2023 03:07:04 -0800 Subject: feat(wrapped): full-year activity history --- src/lib/AniList/activity.ts | 95 +++++++- src/lib/Tools/ActivityHistory.svelte | 22 +- src/lib/Tools/ActivityHistoryGrid.svelte | 63 +++--- src/lib/Tools/Wrapped.svelte | 378 ++++++++++++++++--------------- 4 files changed, 322 insertions(+), 236 deletions(-) (limited to 'src/lib') 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 => + 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 => { + 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))} - -
- {#each filledActivities as activity} -
(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} -
+ +

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 @@ {#if currentUserIdentity.id === -2} Please log in to view this page. {:else if currentUserIdentity.id !== -1} - {#await wrapped(user, currentUserIdentity)} - Loading ... - {:then wrapped} -

-
-
- - User Avatar - -
+ {#await useFullActivityHistory ? fullActivityHistory(user, currentUserIdentity) : getActivityHistory(currentUserIdentity)} + {@html nbsp(`Loading${useFullActivityHistory ? ' full-year' : ''} activity history ...`)} + {:then activities} + {#await wrapped(user, currentUserIdentity)} + Loading ... + {:then wrapped} +
+
+
+ + User Avatar +
- - - {currentUserIdentity.name} - - + +
+ Status Posts: {wrapped.activities.statusCount} +
+
+ Messages: {wrapped.activities.messageCount} +
+
+ Days Active: {activities.length}/{useFullActivityHistory ? 365 : 189} +
-
- Status Posts: {wrapped.activities.statusCount} +
+
+
+ Anime
-
- Messages: {wrapped.activities.messageCount} +
+ Time Watched: {((minutesWatched || 0) / 60 / 24).toFixed(2)} days
-
- 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} +
+ Completed: {animeList?.length}
+
Episodes: {episodes}
-
-
-
- Anime -
-
- Time Watched: {((minutesWatched || 0) / 60 / 24).toFixed(2)} days -
-
- Completed: {animeList?.length} +
+
+ Manga +
+
+ Time Read: {estimatedDayReading(chapters).toFixed(2)} days +
+
+ Completed: {mangaList?.length} +
+
+ Chapters: {chapters} +
-
Episodes: {episodes}
-
-
- Manga -
-
- Time Read: {estimatedDayReading(chapters).toFixed(2)} days -
-
- Completed: {mangaList?.length} -
-
- Chapters: {chapters} +
+
+
+ {#if animeList !== undefined} + + Highest Rated Anime Cover + +
+ Highest Rated Anime +
    + {#each animeList?.slice(0, highestRatedCount) as anime} +
  1. + + {anime.title.english || anime.title.romaji || anime.title.native} + +
  2. + {/each} +
+
+ {:else} + Loading ... + {/if} +
-
-
-
-
-
- {#if animeList !== undefined} - - Highest Rated Anime Cover - -
- Highest Rated Anime -
    - {#each animeList?.slice(0, highestRatedCount) as anime} -
  1. - - {anime.title.english || anime.title.romaji || anime.title.native} - -
  2. - {/each} -
-
- {:else} - Loading ... - {/if} +
+
+ {#if mangaList !== undefined} + + Highest Rated Manga Cover + +
+ Highest Rated Manga +
    + {#each mangaList?.slice(0, highestRatedCount) as manga} +
  1. + + {manga.title.english || manga.title.romaji || manga.title.native} + +
  2. + {/each} +
+
+ {:else} + Loading ... + {/if} +
-
-
- {#if mangaList !== undefined} - - Highest Rated Manga Cover - -
- Highest Rated Manga -
    - {#each mangaList?.slice(0, highestRatedCount) as manga} -
  1. - - {manga.title.english || manga.title.romaji || manga.title.native} - -
  2. - {/each} -
+ {#if !disableActivityHistory} +
+
+
+
- {:else} - Loading ... - {/if} -
-
-
- {#if !disableActivityHistory} -
-
-
-
-
- {/if} - {#if watermark} -
-
- due.moe/wrapped + {/if} + {#if watermark} + -
- {/if} -
- -

- -

- Generate image -

- -
- Options -
- Enable watermark
- Enable background transparency
- - Enable light mode
- Disable activity history
- Include music
- Include rewatches & rereads
- Include specials and OVAs
- - Highest rated media count
- - - - Width adjustment
- { - e.key === 'Enter' && submitExcludedKeywords(); - }} - /> - Excluded keywords - Submit -
- Comma separated list (e.g., "My Hero, Kaguya") + {/if}
-
-

+

-

+

+ Generate image +

+ +
+ Options +
+ Enable watermark
+ Enable background transparency
+ + Enable light mode
+ Disable activity history
+ {#if !disableActivityHistory} + + Enable full-year activity
+ + If you have many activities, this will cause heavy rate-limiting +
+ {/if} + +

+ + Include music
+ Include rewatches & rereads
+ Include specials and OVAs
+ + Highest rated media count
+ + + + Width adjustment
+ { + e.key === 'Enter' && submitExcludedKeywords(); + }} + /> + Excluded keywords + Submit +
+ Comma separated list (e.g., "My Hero, Kaguya") +

+
- {#if generated}

-

- Click on the image to download, or right click and select "Save Image As...". -
- {/if} +
+ + {#if generated} +

+ +

+ Click on the image to download, or right click and select "Save Image As...". +
+ {/if} + {:catch} + + {/await} {:catch} - + {/await} {:else} Loading ... -- cgit v1.2.3