diff options
| author | Fuwn <[email protected]> | 2024-01-21 17:53:00 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-01-21 17:53:00 -0800 |
| commit | eb11e54a4b8a9bf3d648659521906d4cacdd8ae2 (patch) | |
| tree | d50c146930a51fecd4d8253bc86906a5baf180a1 /src/lib/Schedule | |
| parent | refactor(schedule): move top-level tool to route (diff) | |
| download | due.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.svelte | 66 | ||||
| -rw-r--r-- | src/lib/Schedule/Crunchyroll.svelte | 100 | ||||
| -rw-r--r-- | src/lib/Schedule/Days.svelte | 194 | ||||
| -rw-r--r-- | src/lib/Schedule/container.css | 3 |
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; +} |