aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Schedule
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-01-21 17:53:00 -0800
committerFuwn <[email protected]>2024-01-21 17:53:00 -0800
commiteb11e54a4b8a9bf3d648659521906d4cacdd8ae2 (patch)
treed50c146930a51fecd4d8253bc86906a5baf180a1 /src/lib/Schedule
parentrefactor(schedule): move top-level tool to route (diff)
downloaddue.moe-eb11e54a4b8a9bf3d648659521906d4cacdd8ae2.tar.xz
due.moe-eb11e54a4b8a9bf3d648659521906d4cacdd8ae2.zip
refactor(schedule): move module out of tools
Diffstat (limited to 'src/lib/Schedule')
-rw-r--r--src/lib/Schedule/CoverBypass.svelte66
-rw-r--r--src/lib/Schedule/Crunchyroll.svelte100
-rw-r--r--src/lib/Schedule/Days.svelte194
-rw-r--r--src/lib/Schedule/container.css3
4 files changed, 363 insertions, 0 deletions
diff --git a/src/lib/Schedule/CoverBypass.svelte b/src/lib/Schedule/CoverBypass.svelte
new file mode 100644
index 00000000..235979c1
--- /dev/null
+++ b/src/lib/Schedule/CoverBypass.svelte
@@ -0,0 +1,66 @@
+<script lang="ts" ts>
+ import type { Media } from '$lib/AniList/media';
+ import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte';
+ import type { SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease';
+ import HoverCover from '$lib/Media/Cover/HoverCover.svelte';
+ import {
+ onMouseEnter,
+ type HoverCoverResponse,
+ onMouseLeave,
+ onMouseMove
+ } from '$lib/Media/Cover/hoverCover';
+
+ import { outboundLink } from '$lib/Media/links';
+ import settings from '$stores/settings';
+
+ export let media: Media | null;
+ export let entry: SubsPleaseEpisode;
+ export let cover = true;
+
+ let hoverCoverState: HoverCoverResponse = {};
+
+ const titleSelect = (media: Media | null) =>
+ media ? media.title.english || media.title.romaji || media.title.native : null;
+</script>
+
+<a
+ href={media
+ ? outboundLink(media, 'anime', $settings.displayOutboundLinksTo)
+ : outboundLink(
+ null,
+ 'anime',
+ $settings.displayOutboundLinksTo,
+ true,
+ titleSelect(media) || entry.title
+ )}
+ target="_blank"
+ on:mouseenter={() => {
+ const response = onMouseEnter(media, entry);
+
+ hoverCoverState.hovering = response.hovering;
+ hoverCoverState.item = response.item;
+ hoverCoverState.media = response.media;
+ }}
+ on:mouseleave={() => {
+ const response = onMouseLeave();
+
+ hoverCoverState.hovering = response.hovering;
+ hoverCoverState.item = response.item;
+ hoverCoverState.media = response.media;
+ }}
+ on:mousemove={(e) => {
+ const response = onMouseMove(e, 300);
+
+ hoverCoverState.style = response.style;
+ }}
+>
+ {#if media}
+ <MediaTitleDisplay title={media.title} />
+ {:else}
+ {entry.title}
+ {/if}
+</a>
+
+{#if cover}
+ <HoverCover options={hoverCoverState} width={300} />
+{/if}
diff --git a/src/lib/Schedule/Crunchyroll.svelte b/src/lib/Schedule/Crunchyroll.svelte
new file mode 100644
index 00000000..9b6b0509
--- /dev/null
+++ b/src/lib/Schedule/Crunchyroll.svelte
@@ -0,0 +1,100 @@
+<script lang="ts">
+ import crunchyroll from '$lib/Data/crunchyroll.json';
+ import './container.css';
+
+ interface CrunchyrollMedia<T = number | 'soon' | 'continuing'> {
+ year: number;
+ month: number;
+ day: T;
+ title: string;
+ }
+
+ type KnownMedia = { [key: string]: CrunchyrollMedia<number>[] };
+
+ const days: KnownMedia = crunchyroll
+ .filter((media) => media.day !== 'soon' && media.day !== 'continuing')
+ .reduce((acc: KnownMedia, media) => {
+ const date = new Date(media.year, media.month - 1, media.day as number).toLocaleDateString();
+
+ if (!acc[date]) acc[date] = [];
+
+ acc[date].push(media as CrunchyrollMedia<number>);
+
+ return acc;
+ }, {});
+ const continuing: CrunchyrollMedia<number | string>[] = crunchyroll.filter(
+ (media) => media.day === 'continuing'
+ );
+ const soon: CrunchyrollMedia<number | string>[] = crunchyroll.filter(
+ (media) => media.day === 'soon'
+ );
+
+ $: columnCount = Math.ceil(Object.keys(days).length / 2);
+
+ const ordinalSuffix = (i: number) => {
+ const j = i % 10;
+ const k = i % 100;
+
+ if (j === 1 && k !== 11) return i + 'st';
+ if (j === 2 && k !== 12) return i + 'nd';
+ if (j === 3 && k !== 13) return i + 'rd';
+
+ return i + 'th';
+ };
+</script>
+
+<div class="list-container" id="crunchyroll" style={`column-count: ${columnCount};`}>
+ {#each Object.values(days) as day}
+ {@const date = new Date(day[0].year, day[0].month - 1, day[0].day)}
+
+ <div class="card day">
+ <details open class="details-unstyled">
+ <summary>
+ {date.toLocaleString('default', { month: 'long' })}
+ {ordinalSuffix(day[0].day)}, {day[0].year}
+ </summary>
+ <ol>
+ {#each day as media}
+ <li>{media.title}</li>
+ {/each}
+ </ol>
+ </details>
+ </div>
+
+ <p />
+ {/each}
+
+ <div class="card day">
+ <details open class="details-unstyled">
+ <summary>Coming soon</summary>
+
+ <ol>
+ {#each soon as media}
+ <li>{media.title}</li>
+ {/each}
+ </ol>
+ </details>
+ </div>
+
+ <p />
+
+ <div class="card day">
+ <details open class="details-unstyled">
+ <summary>Continuing from previous season</summary>
+
+ <ol>
+ {#each continuing as media}
+ <li>{media.title}</li>
+ {/each}
+ </ol>
+ </details>
+ </div>
+</div>
+
+<style>
+ .day {
+ overflow-y: auto;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+</style>
diff --git a/src/lib/Schedule/Days.svelte b/src/lib/Schedule/Days.svelte
new file mode 100644
index 00000000..ac451507
--- /dev/null
+++ b/src/lib/Schedule/Days.svelte
@@ -0,0 +1,194 @@
+<script lang="ts">
+ import { browser } from '$app/environment';
+ import type { Media } from '$lib/AniList/media';
+ import { findClosestMedia } from '$lib/Media/Anime/Airing/Subtitled/match';
+ import type { SubsPlease, SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease';
+ import { outboundLink } from '$lib/Media/links';
+ import { parseOrDefault } from '$lib/Utility/parameters';
+ import settings from '$stores/settings';
+ import CoverBypass from './CoverBypass.svelte';
+
+ export let subsPlease: SubsPlease;
+ export let scheduledMedia: Partial<Media[]>;
+ export let forceListMode = false;
+
+ const urlParameters = browser ? new URLSearchParams(window.location.search) : null;
+ let day: string | null = parseOrDefault(urlParameters, 'day', null);
+
+ 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[]);
+ });
+
+ Object.entries(shiftedSchedule).forEach(([day, scheduleEntry]) => {
+ if (scheduleEntry.length === 0) {
+ delete shiftedSchedule[day];
+ }
+ });
+
+ return shiftedSchedule;
+ };
+
+ const associateMedia = (media: (Media | undefined)[], title: string) =>
+ findClosestMedia(media as Media[], title);
+
+ const episode = (media: Media, weekday: string) => {
+ if (media.nextAiringEpisode?.episode === 1) return 1;
+
+ if (
+ new Date((media.nextAiringEpisode?.airingAt || 0) * 1000) > new Date() &&
+ weekday === new Date().toLocaleString('en-us', { weekday: 'long' })
+ ) {
+ return (media.nextAiringEpisode?.episode || 1) - 1;
+ } else {
+ return media.nextAiringEpisode?.episode || 0;
+ }
+ };
+</script>
+
+{#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>
+
+ {#if !$settings.displayScheduleListMode && !forceListMode}
+ <div
+ id="covers"
+ style={`grid-template-columns: repeat(auto-fill, minmax(${$settings.displayCoverWidth}px, 1fr))`}
+ >
+ {#each Object.values(scheduleEntry) as entry}
+ {@const media = associateMedia(scheduledMedia, entry.title)}
+
+ <div class="cover-card">
+ <a
+ href={outboundLink(
+ media ? media : null,
+ 'anime',
+ $settings.displayOutboundLinksTo,
+ media === null,
+ entry.title
+ )}
+ target="_blank"
+ >
+ <img
+ class="cover"
+ src={media
+ ? media.coverImage.extraLarge
+ : `https://subsplease.org${entry.image_url}`}
+ alt="Cover"
+ />
+ </a>
+
+ <div class="cover-title">
+ <CoverBypass {media} {entry} cover={false} />
+ <span class:countdown={$settings.displayCountdownRightAligned}>
+ {#if media && media.nextAiringEpisode}
+ <span style="opacity: 50%;">
+ {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
+ </span>
+ {/if}
+ {entry.time}
+ </span>
+ </div>
+ </div>
+ {/each}
+ </div>
+ {:else}
+ <ol>
+ {#each Object.values(scheduleEntry) as entry}
+ {@const media = associateMedia(scheduledMedia, entry.title)}
+
+ <li class="entry">
+ <CoverBypass {media} {entry} />
+ {#if !$settings.displayCountdownRightAligned}
+ <span style="opacity: 50%;">|</span>
+ {/if}
+ <span class:countdown={$settings.displayCountdownRightAligned}>
+ {#if media && media.nextAiringEpisode}
+ <span style="opacity: 50%;">
+ {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
+ </span>
+ {/if}
+ {entry.time}
+ </span>
+ </li>
+ {/each}
+ </ol>
+ {/if}
+ </details>
+
+ <p />
+{/each}
+
+<style>
+ #covers {
+ display: grid;
+ justify-content: center;
+ gap: 0.5rem;
+ row-gap: 1rem;
+ margin-top: 0.5rem;
+ }
+
+ .cover {
+ background-size: cover;
+ background-position: center;
+ border-radius: 8px;
+ width: 7.5rem;
+ }
+
+ .cover-card {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .cover-title {
+ text-align: center;
+ margin: 0.25rem;
+ }
+
+ .entry::after {
+ content: '';
+ display: table;
+ clear: both;
+ }
+
+ .countdown {
+ white-space: nowrap;
+ float: right;
+ }
+
+ .today {
+ box-shadow: 0 2.5px 10px var(--base01), 0 0 0 5px var(--base0E), 0 4px 30px var(--base01);
+ }
+
+ .list {
+ overflow-y: auto;
+ break-inside: avoid-column;
+ page-break-inside: avoid;
+ }
+</style>
diff --git a/src/lib/Schedule/container.css b/src/lib/Schedule/container.css
new file mode 100644
index 00000000..3a3beb1f
--- /dev/null
+++ b/src/lib/Schedule/container.css
@@ -0,0 +1,3 @@
+.list-container {
+ column-width: 250px;
+}