From a35ec1a3f7b340faaaf716b4694f52ef721baec5 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.me>
Date: Mon, 1 Jan 2024 21:59:25 -0800
Subject: refactor(schedule): move to lib

---
 src/lib/Tools/Schedule/Tool.svelte | 220 +++++++++++++++++++++++++++++++++++++
 1 file changed, 220 insertions(+)
 create mode 100644 src/lib/Tools/Schedule/Tool.svelte

(limited to 'src/lib/Tools/Schedule/Tool.svelte')

diff --git a/src/lib/Tools/Schedule/Tool.svelte b/src/lib/Tools/Schedule/Tool.svelte
new file mode 100644
index 00000000..4d68d85b
--- /dev/null
+++ b/src/lib/Tools/Schedule/Tool.svelte
@@ -0,0 +1,220 @@
+<script lang="ts">
+	import Error from '$lib/Error/RateLimited.svelte';
+	import type { SubsPlease, SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease';
+	import { onMount } from 'svelte';
+	import settings from '$stores/settings';
+	import { parseOrDefault } from '$lib/Utility/parameters';
+	import { browser } from '$app/environment';
+	import type { Media } from '$lib/AniList/media';
+	import { scheduleMediaListCollection } from '$lib/AniList/schedule';
+	import { season } from '$lib/Media/Anime/season';
+	import { findClosestMedia } from '$lib/Media/Anime/Airing/Subtitled/match';
+	import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte';
+	import { outboundLink } from '$lib/Media/links';
+	import HeadTitle from '$lib/HeadTitle.svelte';
+	import { onMouseEnter, onMouseLeave, onMouseMove } from '$lib/Media/hoverCover';
+	import HoverCover from '$lib/Media/HoverCover.svelte';
+	import Crunchyroll from '$lib/Tools/Schedule/Crunchyroll.svelte';
+
+	let subsPleasePromise: Promise<SubsPlease>;
+	let scheduledMediaPromise: Promise<Partial<Media[]>>;
+	const urlParameters = browser ? new URLSearchParams(window.location.search) : null;
+	let timeZone = parseOrDefault(
+		urlParameters,
+		'tz',
+		Intl.DateTimeFormat().resolvedOptions().timeZone
+	);
+	let day: string | null = parseOrDefault(urlParameters, 'day', null);
+
+	onMount(async () => {
+		subsPleasePromise = fetch(`/api/subsplease?tz=${timeZone}`).then((r) => r.json());
+		scheduledMediaPromise = scheduleMediaListCollection(new Date().getFullYear(), season());
+	});
+
+	let hovering = false;
+	let hoveredItem: SubsPleaseEpisode | null = null;
+	let hoveredMedia: Media | null = null;
+	let imageStyle = '';
+
+	const shiftSubsPleaseSchedule = (schedule: SubsPlease['schedule']) => {
+		const shiftedSchedule: { [key: string]: SubsPleaseEpisode[] } = {};
+
+		if (day && Object.keys(schedule).includes(day)) {
+			shiftedSchedule[day] = schedule[
+				day as keyof typeof schedule
+			] as unknown as SubsPleaseEpisode[];
+
+			return shiftedSchedule;
+		}
+
+		const days = Object.keys(schedule);
+		const currentDayIndex = days.indexOf(new Date().toLocaleString('en-us', { weekday: 'long' }));
+
+		days
+			.slice(currentDayIndex)
+			.concat(days.slice(0, currentDayIndex))
+			.forEach((day) => {
+				const scheduleEntry = schedule[day as keyof typeof schedule];
+				shiftedSchedule[day] = Array.isArray(scheduleEntry)
+					? scheduleEntry
+					: ([scheduleEntry] as unknown as SubsPleaseEpisode[]);
+			});
+
+		return shiftedSchedule;
+	};
+
+	const associateMedia = (media: (Media | undefined)[], title: string) =>
+		findClosestMedia(media as Media[], title);
+
+	const titleSelect = (media: Media | null) =>
+		media ? media.title.english || media.title.romaji || media.title.native : null;
+</script>
+
+<HeadTitle route="Schedule" path="/schedule" />
+
+<blockquote>
+	<select
+		bind:value={timeZone}
+		on:change={() =>
+			(subsPleasePromise = fetch(`/api/subsplease?tz=${timeZone}`).then((r) => r.json()))}
+	>
+		{#each Intl.supportedValuesOf('timeZone') as zone}
+			<option value={zone}>
+				{zone.split('/').reverse().join(', ').replace(/_/g, ' ')}
+			</option>
+		{/each}
+	</select>
+</blockquote>
+
+{#await subsPleasePromise}
+	Loading subtitle release data ... 49.5%
+{:then subsPlease}
+	{#if subsPlease}
+		{#await scheduledMediaPromise}
+			Loading anime schedule ... 82.5%
+		{:then scheduledMedia}
+			{#if scheduledMedia}
+				{@const columnCount = Math.ceil(Object.keys(subsPlease.schedule).length / 2)}
+
+				<div id="list-container" style={`column-count: ${columnCount}`}>
+					{#each Object.entries(shiftSubsPleaseSchedule(subsPlease.schedule)) as [day, scheduleEntry]}
+						<details
+							open
+							class="list"
+							class:today={day === new Date().toLocaleString('en-us', { weekday: 'long' })}
+						>
+							<summary>{day}</summary>
+
+							<ul>
+								{#each Object.values(scheduleEntry) as entry}
+									{@const media = associateMedia(scheduledMedia, entry.title)}
+
+									<li
+										class="entry"
+										on:mouseenter={() => {
+											const response = onMouseEnter(media, entry);
+
+											hovering = response.hovering;
+											hoveredItem = response.item;
+											hoveredMedia = response.media;
+										}}
+										on:mouseleave={() => {
+											const response = onMouseLeave();
+
+											hovering = response.hovering;
+											hoveredItem = response.item;
+											hoveredMedia = response.media;
+										}}
+										on:mousemove={(e) => {
+											const response = onMouseMove(e);
+
+											imageStyle = response.style;
+										}}
+									>
+										<a
+											href={media
+												? outboundLink(media, 'anime', $settings.displayOutboundLinksTo)
+												: outboundLink(
+														null,
+														'anime',
+														$settings.displayOutboundLinksTo,
+														true,
+														titleSelect(media) || entry.title
+												  )}
+											target="_blank"
+										>
+											{#if media}
+												<MediaTitleDisplay title={media.title} />
+											{:else}
+												{entry.title}
+											{/if}
+										</a>
+										{#if !$settings.displayCountdownRightAligned}
+											<span style="opacity: 50%;">|</span>
+										{/if}
+										<span class:countdown={$settings.displayCountdownRightAligned}>
+											{#if media && media.nextAiringEpisode}
+												<span style="opacity: 50%;">
+													&nbsp;
+													{media.nextAiringEpisode?.episode}{media.episodes
+														? `/${media.episodes}`
+														: ''} at
+												</span>
+											{/if}
+											{entry.time}
+										</span>
+									</li>
+								{/each}
+							</ul>
+
+							<p />
+						</details>
+					{/each}
+				</div>
+			{:else}
+				Loading anime schedule ... 66%
+			{/if}
+		{:catch}
+			<Error type="Media" loginSessionError={false} />
+		{/await}
+	{:else}
+		Loading subtitle release data ... 33%
+	{/if}
+{:catch}
+	<Error type="Schedule" loginSessionError={false} />
+{/await}
+
+<p />
+
+<details>
+	<summary>Crunchyroll Simulcast Release Calender</summary>
+
+	<Crunchyroll />
+</details>
+
+<HoverCover {hoveredItem} {hoveredMedia} {imageStyle} {hovering} />
+
+<style>
+	#list-container {
+		column-width: 250px;
+	}
+
+	.list {
+		overflow-y: auto;
+	}
+
+	.entry::after {
+		content: '';
+		display: table;
+		clear: both;
+	}
+
+	.countdown {
+		white-space: nowrap;
+		float: right;
+	}
+
+	.today {
+		font-weight: bold;
+	}
+</style>
-- 
cgit v1.2.3

