aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Tools
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-01-03 22:05:24 -0800
committerFuwn <[email protected]>2024-01-03 22:05:24 -0800
commit1d0ffdba530fa166ac577ef1fba3b5a0a959959a (patch)
treed9264c043a7ee39d982654e88b766e51a217fe16 /src/lib/Tools
parentfeat(badges): put returns badges (diff)
downloaddue.moe-1d0ffdba530fa166ac577ef1fba3b5a0a959959a.tar.xz
due.moe-1d0ffdba530fa166ac577ef1fba3b5a0a959959a.zip
refactor(wrapped): move panels to components
Diffstat (limited to 'src/lib/Tools')
-rw-r--r--src/lib/Tools/Wrapped.svelte403
-rw-r--r--src/lib/Tools/Wrapped/ActivityHistory.svelte21
-rw-r--r--src/lib/Tools/Wrapped/Media.svelte98
-rw-r--r--src/lib/Tools/Wrapped/MediaExtras.svelte74
-rw-r--r--src/lib/Tools/Wrapped/Top/Activity.svelte43
-rw-r--r--src/lib/Tools/Wrapped/Top/Anime.svelte20
-rw-r--r--src/lib/Tools/Wrapped/Top/Manga.svelte22
-rw-r--r--src/lib/Tools/Wrapped/Watermark.svelte5
-rw-r--r--src/lib/Tools/Wrapped/wrapped.css100
9 files changed, 424 insertions, 362 deletions
diff --git a/src/lib/Tools/Wrapped.svelte b/src/lib/Tools/Wrapped.svelte
index 07c722ee..29ef0919 100644
--- a/src/lib/Tools/Wrapped.svelte
+++ b/src/lib/Tools/Wrapped.svelte
@@ -20,12 +20,15 @@
import { page } from '$app/stores';
import { clearAllParameters } from '../Utility/parameters';
import { nbsp } from '../Utility/html';
- import { estimatedDayReading } from '$lib/Media/Manga/time';
- import ActivityHistoryGrid from './ActivityHistory/Grid.svelte';
import SettingHint from '$lib/Settings/SettingHint.svelte';
import { database } from '$lib/Database/activities';
- import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte';
- import proxy from '$lib/Utility/proxy';
+ import Activity from './Wrapped/Top/Activity.svelte';
+ import Anime from './Wrapped/Top/Anime.svelte';
+ import Manga from './Wrapped/Top/Manga.svelte';
+ import ActivityHistory from './Wrapped/ActivityHistory.svelte';
+ import MediaExtras from './Wrapped/MediaExtras.svelte';
+ import MediaPanel from './Wrapped/Media.svelte';
+ import Watermark from './Wrapped/Watermark.svelte';
export let user: AniListAuthorisation;
@@ -497,278 +500,53 @@
class:transparent={transparency}
>
{#if !disableActivityHistory && activityHistoryPosition === 'TOP' && activities.length > 0 && selectedYear === currentYear}
- <div class="categories-grid" style="padding-bottom: 0;">
- <div class="category-grid bottom-category pure-category category">
- <div id="activity-history">
- <ActivityHistoryGrid
- {user}
- activityData={activities}
- currentYear={selectedYear}
- />
- </div>
- </div>
- </div>
+ <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} />
{/if}
<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>
- <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: {#if selectedYear !== currentYear}
- ?/365
- {:else}
- {activities.length}/{useFullActivityHistory ? 365 : 189}
- {/if}
- </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 || 0}
- </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 || 0}
- </div>
- <div class="grid-item">
- Chapters: {chapters}
- </div>
- </div>
+ <Activity
+ {wrapped}
+ identity={currentUserIdentity}
+ year={selectedYear}
+ {activities}
+ {useFullActivityHistory}
+ {updateWidth}
+ />
+ <Anime {animeList} {minutesWatched} {episodes} />
+ <Manga {mangaList} {chapters} />
</div>
{#if !disableActivityHistory && activityHistoryPosition === 'BELOW_TOP' && activities.length > 0 && selectedYear === currentYear}
- <div class="categories-grid" style="padding-bottom: 0;">
- <div class="category-grid bottom-category pure-category category">
- <div id="activity-history">
- <ActivityHistoryGrid
- {user}
- activityData={activities}
- currentYear={selectedYear}
- />
- </div>
- </div>
- </div>
- {/if}
- {#if animeList !== undefined || mangaList !== undefined}
- <div class="categories-grid">
- <div class="category-grid pure-category category middle-category">
- <div class="grid-item image-grid">
- <a
- href={animeList && animeList[0]
- ? `https://anilist.co/anime/${animeList[0].id}`
- : '#'}
- target="_blank"
- >
- <img
- src={proxy(
- animeList && animeList[0]
- ? animeList[0].coverImage.extraLarge
- : wrapped.avatar.large
- )}
- alt="Highest Rated Anime Cover"
- class="cover-image"
- on:load={updateWidth}
- />
- </a>
- <div>
- <b>{animeMostTitle} Anime</b>
- <ol>
- {#if animeList !== undefined && animeList.length !== 0}
- {#each animeList?.slice(0, highestRatedCount) as anime}
- <li>
- <a href={`https://anilist.co/anime/${anime.id}`} target="_blank">
- <MediaTitleDisplay title={anime.title} />
- </a>{highestRatedMediaPercentage &&
- anime.mediaListEntry &&
- anime.mediaListEntry?.score > 0
- ? `: ${anime.mediaListEntry?.score}%`
- : ''}
- </li>
- {/each}
- {:else}
- <li>
- <p style="opacity: 50%;">(⌣_⌣”)</p>
- </li>
- {/if}
- </ol>
- </div>
- </div>
- </div>
- <div class="category-grid pure-category category middle-category">
- <div class="grid-item image-grid">
- <a
- href={mangaList && mangaList[0]
- ? `https://anilist.co/manga/${mangaList[0].id}`
- : '#'}
- target="_blank"
- >
- <img
- src={proxy(
- mangaList && mangaList[0]
- ? mangaList[0].coverImage.extraLarge
- : wrapped.avatar.large
- )}
- alt="Highest Rated Manga Cover"
- class="cover-image"
- on:load={updateWidth}
- />
- </a>
- <div>
- <b>{mangaMostTitle} Manga</b>
- <ol>
- {#if mangaList !== undefined && mangaList.length !== 0}
- {#each mangaList?.slice(0, highestRatedCount) as manga}
- <li>
- <a href={`https://anilist.co/manga/${manga.id}`} target="_blank">
- <MediaTitleDisplay title={manga.title} />
- </a>{highestRatedMediaPercentage &&
- manga.mediaListEntry &&
- manga.mediaListEntry?.score > 0
- ? `: ${manga.mediaListEntry?.score}%`
- : ''}
- </li>
- {/each}
- {:else}
- <li>
- <p style="opacity: 50%;">(⌣_⌣”)</p>
- </li>
- {/if}
- </ol>
- </div>
- </div>
- </div>
- </div>
+ <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))}
- <div class="categories-grid" style="padding-top: 0;">
- {#if topMedia.topGenreMedia && topMedia.genres.length > 0}
- <div class="category-grid pure-category category">
- <div class="grid-item image-grid">
- <a
- href={`https://anilist.co/${topMedia.topGenreMedia.type.toLowerCase()}/${
- topMedia.topGenreMedia.id
- }`}
- target="_blank"
- >
- <img
- src={proxy(topMedia.topGenreMedia.coverImage.extraLarge)}
- alt="Highest Rated Genre Cover"
- class="cover-image"
- on:load={updateWidth}
- />
- </a>
- <div>
- <b>{genreTagTitle} Genres</b>
- <ol>
- {#each topMedia.genres as genre}
- <li>
- <a
- href={`https://anilist.co/search/anime?genres=${genre.genre}`}
- target="_blank"
- >
- {genre.genre}{highestRatedGenreTagPercentage
- ? `: ${genre.averageScore}%`
- : ''}
- </a>
- </li>
- {/each}
- </ol>
- </div>
- </div>
- </div>
- {/if}
- {#if topMedia.topTagMedia && topMedia.tags.length > 0}
- <div class="category-grid pure-category category">
- <div class="grid-item image-grid">
- <a
- href={`https://anilist.co/${topMedia.topTagMedia.type.toLowerCase()}/${
- topMedia.topTagMedia.id
- }`}
- target="_blank"
- >
- <img
- src={proxy(topMedia.topTagMedia.coverImage.extraLarge)}
- alt="Highest Rated Tag Cover"
- class="cover-image"
- on:load={updateWidth}
- />
- </a>
- <div>
- <b>{genreTagTitle} Tags</b>
- <ol>
- {#each topMedia.tags as tag}
- <li>
- <a
- href={`https://anilist.co/search/anime?genres=${tag.tag}`}
- target="_blank"
- >
- {tag.tag}{highestRatedGenreTagPercentage
- ? `: ${tag.averageScore}%`
- : ''}
- </a>
- </li>
- {/each}
- </ol>
- </div>
- </div>
- </div>
- {/if}
- </div>
+ <MediaExtras
+ {topMedia}
+ {genreTagTitle}
+ {highestRatedGenreTagPercentage}
+ {updateWidth}
+ />
{/if}
{#if !disableActivityHistory && activityHistoryPosition === 'ORIGINAL' && activities.length > 0 && selectedYear === currentYear}
- <div class="categories-grid" style="padding-top: 0;">
- <div class="category-grid bottom-category pure-category category">
- <div id="activity-history">
- <ActivityHistoryGrid
- {user}
- activityData={activities}
- currentYear={selectedYear}
- />
- </div>
- </div>
- </div>
+ <ActivityHistory {user} {activities} year={selectedYear} {activityHistoryPosition} />
{/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>
+ <Watermark />
{/if}
</div>
<div class="list">
<p>
- <a href={'#'} on:click={screenshot} data-umami-event="Generate Wrapped"
- >Generate image</a
- >
+ <a href={'#'} on:click={screenshot} data-umami-event="Generate Wrapped">
+ Generate image
+ </a>
</p>
<details open>
@@ -905,104 +683,5 @@
{/if}
<style>
- @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700');
- @import url('https://fonts.googleapis.com/css?family=Overpass:400,600,700,800');
-
- .categories-grid {
- display: flex;
- flex-wrap: wrap;
- row-gap: 1.5em;
- column-gap: 1.5em;
- padding: 2%;
- justify-content: center;
- font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell,
- Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
- background-color: #0b1622;
- }
-
- .categories-grid b {
- font-family: Overpass, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell,
- Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
- font-weight: 600;
- }
-
- .category-grid,
- .image-grid {
- background-color: #151f2e;
- border-radius: 4px;
- color: rgb(159, 173, 189);
- }
-
- .pure-category,
- .avatar-grid {
- padding: 1.5%;
- }
-
- .category-grid {
- display: grid;
- }
-
- .image-grid {
- display: flex;
- column-gap: 1em;
- flex-wrap: wrap;
- }
-
- .image-grid img {
- width: 6em;
- height: auto;
- border-radius: 3px;
- }
-
- .categories-grid a {
- text-decoration: none;
- color: unset;
- }
-
- .transparent .categories-grid {
- background-color: transparent !important;
- }
-
- .light-theme .categories-grid {
- background-color: #edf1f5;
- }
-
- .light-theme .category-grid {
- background-color: #fafafa;
- color: rgb(92, 114, 138);
- }
-
- .light-theme .image-grid {
- background-color: #fafafa;
- color: rgb(92, 114, 138);
- }
-
- ol {
- margin: 0 !important;
- }
-
- #watermark {
- color: rgb(61, 180, 242);
- }
-
- #wrapped-final {
- height: auto;
- width: 50%;
- }
-
- #list-container {
- display: flex;
- gap: 1rem;
- flex-wrap: wrap;
- align-items: start;
- }
-
- .list {
- flex-grow: 1;
- flex-basis: 1%;
- }
-
- #wrapped {
- overflow-y: scroll;
- }
+ @import './Wrapped/wrapped.css';
</style>
diff --git a/src/lib/Tools/Wrapped/ActivityHistory.svelte b/src/lib/Tools/Wrapped/ActivityHistory.svelte
new file mode 100644
index 00000000..7c972eb9
--- /dev/null
+++ b/src/lib/Tools/Wrapped/ActivityHistory.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+ import type { ActivityHistoryEntry } from '$lib/AniList/activity';
+ import type { AniListAuthorisation } from '$lib/AniList/identity';
+ import ActivityHistoryGrid from '../ActivityHistory/Grid.svelte';
+
+ export let user: AniListAuthorisation;
+ export let activities: ActivityHistoryEntry[];
+ export let year: number;
+ export let activityHistoryPosition: 'TOP' | 'BELOW_TOP' | 'ORIGINAL';
+</script>
+
+<div
+ class="categories-grid"
+ style={`padding-${activityHistoryPosition === 'ORIGINAL' ? 'top' : 'bottom'}: 0;`}
+>
+ <div class="category-grid bottom-category pure-category category">
+ <div id="activity-history">
+ <ActivityHistoryGrid {user} activityData={activities} currentYear={year} />
+ </div>
+ </div>
+</div>
diff --git a/src/lib/Tools/Wrapped/Media.svelte b/src/lib/Tools/Wrapped/Media.svelte
new file mode 100644
index 00000000..08092a28
--- /dev/null
+++ b/src/lib/Tools/Wrapped/Media.svelte
@@ -0,0 +1,98 @@
+<script lang="ts">
+ import type { Media } from '$lib/AniList/media';
+ import type { Wrapped } from '$lib/AniList/wrapped';
+ import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte';
+ import proxy from '$lib/Utility/proxy';
+
+ export let animeList: Media[] | undefined;
+ export let mangaList: Media[] | undefined;
+ export let wrapped: Wrapped;
+ export let updateWidth: () => void;
+ export let highestRatedMediaPercentage: boolean;
+ export let highestRatedCount: number;
+ export let animeMostTitle: string;
+ export let mangaMostTitle: string;
+</script>
+
+{#if animeList !== undefined || mangaList !== undefined}
+ <div class="categories-grid">
+ <div class="category-grid pure-category category middle-category">
+ <div class="grid-item image-grid">
+ <a
+ href={animeList && animeList[0] ? `https://anilist.co/anime/${animeList[0].id}` : '#'}
+ target="_blank"
+ >
+ <img
+ src={proxy(
+ animeList && animeList[0] ? animeList[0].coverImage.extraLarge : wrapped.avatar.large
+ )}
+ alt="Highest Rated Anime Cover"
+ class="cover-image"
+ on:load={updateWidth}
+ />
+ </a>
+ <div>
+ <b>{animeMostTitle} Anime</b>
+ <ol>
+ {#if animeList !== undefined && animeList.length !== 0}
+ {#each animeList?.slice(0, highestRatedCount) as anime}
+ <li>
+ <a href={`https://anilist.co/anime/${anime.id}`} target="_blank">
+ <MediaTitleDisplay title={anime.title} />
+ </a>{highestRatedMediaPercentage &&
+ anime.mediaListEntry &&
+ anime.mediaListEntry?.score > 0
+ ? `: ${anime.mediaListEntry?.score}%`
+ : ''}
+ </li>
+ {/each}
+ {:else}
+ <li>
+ <p style="opacity: 50%;">(⌣_⌣”)</p>
+ </li>
+ {/if}
+ </ol>
+ </div>
+ </div>
+ </div>
+ <div class="category-grid pure-category category middle-category">
+ <div class="grid-item image-grid">
+ <a
+ href={mangaList && mangaList[0] ? `https://anilist.co/manga/${mangaList[0].id}` : '#'}
+ target="_blank"
+ >
+ <img
+ src={proxy(
+ mangaList && mangaList[0] ? mangaList[0].coverImage.extraLarge : wrapped.avatar.large
+ )}
+ alt="Highest Rated Manga Cover"
+ class="cover-image"
+ on:load={updateWidth}
+ />
+ </a>
+ <div>
+ <b>{mangaMostTitle} Manga</b>
+ <ol>
+ {#if mangaList !== undefined && mangaList.length !== 0}
+ {#each mangaList?.slice(0, highestRatedCount) as manga}
+ <li>
+ <a href={`https://anilist.co/manga/${manga.id}`} target="_blank">
+ <MediaTitleDisplay title={manga.title} />
+ </a>{highestRatedMediaPercentage &&
+ manga.mediaListEntry &&
+ manga.mediaListEntry?.score > 0
+ ? `: ${manga.mediaListEntry?.score}%`
+ : ''}
+ </li>
+ {/each}
+ {:else}
+ <li>
+ <p style="opacity: 50%;">(⌣_⌣”)</p>
+ </li>
+ {/if}
+ </ol>
+ </div>
+ </div>
+ </div>
+ </div>
+{/if}
diff --git a/src/lib/Tools/Wrapped/MediaExtras.svelte b/src/lib/Tools/Wrapped/MediaExtras.svelte
new file mode 100644
index 00000000..00417c54
--- /dev/null
+++ b/src/lib/Tools/Wrapped/MediaExtras.svelte
@@ -0,0 +1,74 @@
+<script lang="ts">
+ import type { TopMedia } from '$lib/AniList/wrapped';
+ import proxy from '$lib/Utility/proxy';
+
+ export let topMedia: TopMedia;
+ export let updateWidth: () => void;
+ export let highestRatedGenreTagPercentage: boolean;
+ export let genreTagTitle: string;
+</script>
+
+<div class="categories-grid" style="padding-top: 0;">
+ {#if topMedia.topGenreMedia && topMedia.genres.length > 0}
+ <div class="category-grid pure-category category">
+ <div class="grid-item image-grid">
+ <a
+ href={`https://anilist.co/${topMedia.topGenreMedia.type.toLowerCase()}/${
+ topMedia.topGenreMedia.id
+ }`}
+ target="_blank"
+ >
+ <img
+ src={proxy(topMedia.topGenreMedia.coverImage.extraLarge)}
+ alt="Highest Rated Genre Cover"
+ class="cover-image"
+ on:load={updateWidth}
+ />
+ </a>
+ <div>
+ <b>{genreTagTitle} Genres</b>
+ <ol>
+ {#each topMedia.genres as genre}
+ <li>
+ <a href={`https://anilist.co/search/anime?genres=${genre.genre}`} target="_blank">
+ {genre.genre}{highestRatedGenreTagPercentage ? `: ${genre.averageScore}%` : ''}
+ </a>
+ </li>
+ {/each}
+ </ol>
+ </div>
+ </div>
+ </div>
+ {/if}
+ {#if topMedia.topTagMedia && topMedia.tags.length > 0}
+ <div class="category-grid pure-category category">
+ <div class="grid-item image-grid">
+ <a
+ href={`https://anilist.co/${topMedia.topTagMedia.type.toLowerCase()}/${
+ topMedia.topTagMedia.id
+ }`}
+ target="_blank"
+ >
+ <img
+ src={proxy(topMedia.topTagMedia.coverImage.extraLarge)}
+ alt="Highest Rated Tag Cover"
+ class="cover-image"
+ on:load={updateWidth}
+ />
+ </a>
+ <div>
+ <b>{genreTagTitle} Tags</b>
+ <ol>
+ {#each topMedia.tags as tag}
+ <li>
+ <a href={`https://anilist.co/search/anime?genres=${tag.tag}`} target="_blank">
+ {tag.tag}{highestRatedGenreTagPercentage ? `: ${tag.averageScore}%` : ''}
+ </a>
+ </li>
+ {/each}
+ </ol>
+ </div>
+ </div>
+ </div>
+ {/if}
+</div>
diff --git a/src/lib/Tools/Wrapped/Top/Activity.svelte b/src/lib/Tools/Wrapped/Top/Activity.svelte
new file mode 100644
index 00000000..a000389c
--- /dev/null
+++ b/src/lib/Tools/Wrapped/Top/Activity.svelte
@@ -0,0 +1,43 @@
+<script lang="ts">
+ import type { ActivityHistoryEntry } from '$lib/AniList/activity';
+ import type { UserIdentity } from '$lib/AniList/identity';
+ import type { Wrapped } from '$lib/AniList/wrapped';
+ import proxy from '$lib/Utility/proxy';
+
+ export let wrapped: Wrapped;
+ export let identity: UserIdentity;
+ export let year: number;
+ export let activities: ActivityHistoryEntry[];
+ export let useFullActivityHistory: boolean;
+ export let updateWidth: () => void;
+
+ const currentYear = new Date(Date.now()).getFullYear();
+</script>
+
+<div class="grid-item image-grid avatar-grid category top-category">
+ <a href={`https://anilist.co/user/${identity.name}`} target="_blank">
+ <img src={proxy(wrapped.avatar.large)} alt="User Avatar" on:load={updateWidth} />
+ </a>
+ <div>
+ <div>
+ <a href={`https://anilist.co/user/${identity.name}`} target="_blank">
+ <b>
+ {identity.name}
+ </b>
+ </a>
+ </div>
+ <div>
+ Status Posts: {wrapped.activities.statusCount}
+ </div>
+ <div>
+ Messages: {wrapped.activities.messageCount}
+ </div>
+ <div>
+ Days Active: {#if year !== currentYear}
+ ?/365
+ {:else}
+ {activities.length}/{useFullActivityHistory ? 365 : 189}
+ {/if}
+ </div>
+ </div>
+</div>
diff --git a/src/lib/Tools/Wrapped/Top/Anime.svelte b/src/lib/Tools/Wrapped/Top/Anime.svelte
new file mode 100644
index 00000000..8ea1277f
--- /dev/null
+++ b/src/lib/Tools/Wrapped/Top/Anime.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+ import type { Media } from '$lib/AniList/media';
+
+ export let minutesWatched: number;
+ export let animeList: Media[] | undefined;
+ export let episodes: number;
+</script>
+
+<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 || 0}
+ </div>
+ <div class="grid-item">Episodes: {episodes}</div>
+</div>
diff --git a/src/lib/Tools/Wrapped/Top/Manga.svelte b/src/lib/Tools/Wrapped/Top/Manga.svelte
new file mode 100644
index 00000000..b0d78b6e
--- /dev/null
+++ b/src/lib/Tools/Wrapped/Top/Manga.svelte
@@ -0,0 +1,22 @@
+<script lang="ts">
+ import type { Media } from '$lib/AniList/media';
+ import { estimatedDayReading } from '$lib/Media/Manga/time';
+
+ export let mangaList: Media[] | undefined;
+ export let chapters: number;
+</script>
+
+<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 || 0}
+ </div>
+ <div class="grid-item">
+ Chapters: {chapters}
+ </div>
+</div>
diff --git a/src/lib/Tools/Wrapped/Watermark.svelte b/src/lib/Tools/Wrapped/Watermark.svelte
new file mode 100644
index 00000000..2e8dd838
--- /dev/null
+++ b/src/lib/Tools/Wrapped/Watermark.svelte
@@ -0,0 +1,5 @@
+<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>
diff --git a/src/lib/Tools/Wrapped/wrapped.css b/src/lib/Tools/Wrapped/wrapped.css
new file mode 100644
index 00000000..1ac01d85
--- /dev/null
+++ b/src/lib/Tools/Wrapped/wrapped.css
@@ -0,0 +1,100 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700');
+@import url('https://fonts.googleapis.com/css?family=Overpass:400,600,700,800');
+
+.categories-grid {
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: 1.5em;
+ column-gap: 1.5em;
+ padding: 2%;
+ justify-content: center;
+ font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell,
+ Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ background-color: #0b1622;
+}
+
+.categories-grid b {
+ font-family: Overpass, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell,
+ Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-weight: 600;
+}
+
+.category-grid,
+.image-grid {
+ background-color: #151f2e;
+ border-radius: 4px;
+ color: rgb(159, 173, 189);
+}
+
+.pure-category,
+.avatar-grid {
+ padding: 1.5%;
+}
+
+.category-grid {
+ display: grid;
+}
+
+.image-grid {
+ display: flex;
+ column-gap: 1em;
+ flex-wrap: wrap;
+}
+
+.image-grid img {
+ width: 6em;
+ height: auto;
+ border-radius: 3px;
+}
+
+.categories-grid a {
+ text-decoration: none;
+ color: unset;
+}
+
+.transparent .categories-grid {
+ background-color: transparent !important;
+}
+
+.light-theme .categories-grid {
+ background-color: #edf1f5;
+}
+
+.light-theme .category-grid {
+ background-color: #fafafa;
+ color: rgb(92, 114, 138);
+}
+
+.light-theme .image-grid {
+ background-color: #fafafa;
+ color: rgb(92, 114, 138);
+}
+
+ol {
+ margin: 0 !important;
+}
+
+#watermark {
+ color: rgb(61, 180, 242);
+}
+
+#wrapped-final {
+ height: auto;
+ width: 50%;
+}
+
+#list-container {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ align-items: start;
+}
+
+.list {
+ flex-grow: 1;
+ flex-basis: 1%;
+}
+
+#wrapped {
+ overflow-y: scroll;
+}