aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-12-22 03:07:04 -0800
committerFuwn <[email protected]>2023-12-22 03:11:24 -0800
commita7255393ac86b091772189469fc1806ded1595d1 (patch)
treec238fcd2d5fa3302f195f9ee76d0d2dbbe1da43f /src
parentfix(wrapped): absolute best updateWidth() (diff)
downloaddue.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.ts95
-rw-r--r--src/lib/Tools/ActivityHistory.svelte22
-rw-r--r--src/lib/Tools/ActivityHistoryGrid.svelte63
-rw-r--r--src/lib/Tools/Wrapped.svelte378
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, '&nbsp;');
</script>
{#if currentUserIdentity.id === -2}
Please log in to view this page.
{:else if currentUserIdentity.id !== -1}
- {#await wrapped(user, currentUserIdentity)}
- Loading&nbsp;...
- {: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&nbsp;...
+ {: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&nbsp;...
- {:then activities}
- {#if activities === undefined}
- Loading&nbsp;...
- {: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&nbsp;...
+ {/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&nbsp;...
- {/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&nbsp;...
+ {/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&nbsp;...
- {/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&nbsp;...