From a7255393ac86b091772189469fc1806ded1595d1 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.me>
Date: Fri, 22 Dec 2023 03:07:04 -0800
Subject: feat(wrapped): full-year activity history

---
 src/lib/Tools/Wrapped.svelte | 378 +++++++++++++++++++++++--------------------
 1 file changed, 199 insertions(+), 179 deletions(-)

(limited to 'src/lib/Tools/Wrapped.svelte')

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;...
-- 
cgit v1.2.3

