diff options
Diffstat (limited to 'src/routes')
53 files changed, 2285 insertions, 1829 deletions
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index f822d521..71482ffb 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,19 +1,19 @@ <script lang="ts"> - import { page } from '$app/stores'; - import { closest } from '$lib/Error/path'; - import Popup from '$lib/Layout/Popup.svelte'; +import { page } from "$app/stores"; +import { closest } from "$lib/Error/path"; +import Popup from "$lib/Layout/Popup.svelte"; - $: suggestion = closest($page.url.pathname.replace('/', ''), [ - 'birthdays', - 'completed', - 'schedule', - 'hololive', - 'settings', - 'tools', - 'updates', - 'user', - 'wrapped' - ]); +$: suggestion = closest($page.url.pathname.replace("/", ""), [ + "birthdays", + "completed", + "schedule", + "hololive", + "settings", + "tools", + "updates", + "user", + "wrapped", +]); </script> <Popup> diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 1b7bf69a..8357aa17 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,14 +1,16 @@ export const load = ({ locals, url, cookies }) => { - const { user } = locals; + const { user } = locals; - if (cookies.get('logout') === '1') { - cookies.delete('user', { path: '/' }); - cookies.delete('logout', { path: '/' }); - } + if (cookies.get("logout") === "1") { + cookies.delete("user", { path: "/" }); + cookies.delete("logout", { path: "/" }); + } - return { - user, - url: url.pathname, - commit: process.env.VERCEL_GIT_COMMIT_SHA ?? 'ffffffffffffffffffffffffffffffffffffffff' - }; + return { + user, + url: url.pathname, + commit: + process.env.VERCEL_GIT_COMMIT_SHA ?? + "ffffffffffffffffffffffffffffffffffffffff", + }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4d78b29f..39a2e40b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,164 +1,198 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import type { SubsPleaseEpisode } from '$lib/Media/Anime/Airing/Subtitled/subsPlease'; - import { env } from '$env/dynamic/public'; - import { userIdentity as getUserIdentity } from '$lib/Data/AniList/identity'; - import { onDestroy, onMount } from 'svelte'; - import userIdentity from '$stores/identity'; - import settings from '$stores/settings'; - import { browser } from '$app/environment'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import '../app.css'; - import { readable, type Readable } from 'svelte/store'; - import { navigating } from '$app/stores'; - import NotificationsProvider from '$lib/Notification/NotificationsProvider.svelte'; - import Root from '$lib/Home/Root.svelte'; - import root from '$lib/Utility/root'; - import { addMessages, init, locale as i18nLocale, locales } from 'svelte-i18n'; - import english from '$lib/Locale/english'; - import japanese from '$lib/Locale/japanese'; - import type { LocaleDictionary } from '$lib/Locale/layout'; - import locale from '$stores/locale'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import subsPlease from '$stores/subsPlease'; - import Dropdown from '$lib/Layout/Dropdown.svelte'; - import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit'; - import subtitles from '$lib/Data/Static/subtitles.json'; - import settingsSyncPulled from '$stores/settingsSyncPulled'; - import settingsSyncTimes from '$stores/settingsSyncTimes'; - import Announcement from '$lib/Announcement.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import { requestNotifications } from '$lib/Utility/notifications'; - import { database as userDatabase } from '$lib/Database/IDB/user'; - import CommandPalette from '$lib/CommandPalette/CommandPalette.svelte'; - import { defaultActions } from '$lib/CommandPalette/actions'; - import { toolsAsCommandPaletteActions } from '$lib/Tools/tools'; - import localforage from 'localforage'; - import { dev } from '$app/environment'; - import { injectAnalytics } from '@vercel/analytics/sveltekit'; - - injectSpeedInsights(); - injectAnalytics({ mode: dev ? 'development' : 'production' }); - - export let data; - - let isHeaderVisible = true; - let previousScrollPosition = 0; - let notificationInterval: ReturnType<typeof setInterval> | undefined = undefined; - - addMessages('en', english as unknown as LocaleDictionary); - addMessages('ja', japanese as unknown as LocaleDictionary); - init({ fallbackLocale: 'en', initialLocale: $settings.displayLanguage }); - - $: i18nLocale.set($settings.displayLanguage); - - const navigationOrder = ['/', '/completed', '/schedule', '/updates', '/tools', '/settings']; - const previousPage: Readable<string | null> = readable(null, (set) => { - const unsubscribe = navigating.subscribe(($navigating) => { - if ($navigating && $navigating.from) set($navigating.from.url.pathname as unknown as null); - }); - - return () => unsubscribe(); - }); - - $: way = data.url.includes('/user') - ? 200 - : $previousPage && $previousPage.includes('/user') - ? -200 - : navigationOrder.indexOf(data.url) > navigationOrder.indexOf($previousPage ?? '/') - ? 200 - : -200; - - const handleScroll = () => { - const currentScrollPosition = window.scrollY; - - isHeaderVisible = - currentScrollPosition <= 100 || currentScrollPosition < previousScrollPosition; - previousScrollPosition = currentScrollPosition; - }; - - onMount(async () => { - if (browser) { - if (await localforage.getItem('redirect')) { - window.location.href = (await localforage.getItem('redirect')) ?? '/'; - - await localforage.removeItem('redirect'); - } - - window.addEventListener('scroll', handleScroll); - - if ((await localforage.getItem('commit')) !== data.commit) { - await localforage.removeItem('identity'); - await localforage.removeItem('anime'); - await localforage.removeItem('manga'); - await localforage.removeItem('anime'); - await localforage.removeItem('manga'); - await localforage.removeItem('lastPruneTimes'); - await localforage.setItem('commit', data.commit); - } - } - - settings.get(); - - if (data.user !== undefined && $userIdentity.id === -2) - getUserIdentity(data.user).then((h) => userIdentity.set(h)); - - if ($settings.settingsSync && $userIdentity.id !== -2) - fetch(root(`/api/configuration?id=${$userIdentity.id}`)).then((response) => { - if (response.ok) - response.json().then((data) => { - if (data && data.configuration) { - console.log('Pulled remote configuration'); - settings.set(data.configuration); - settingsSyncPulled.set(true); - settingsSyncTimes.set({ - lastPull: new Date(), - lastPush: new Date(data.updated_at + 'Z') - }); - } - }); - }); - - await userDatabase.users.where('id').below(0).delete(); - - if (!(await userDatabase.users.get($userIdentity.id)) && $userIdentity.id > 0) - await userDatabase.users.put({ - id: $userIdentity.id, - user: data.user, - lastNotificationID: null - }); - - if ($settings.displayAniListNotifications && data.user !== undefined) - if ('Notification' in window && navigator.serviceWorker) requestNotifications(); - }); - - onDestroy(() => { - if (browser) window.removeEventListener('scroll', handleScroll); - - if (notificationInterval) clearInterval(notificationInterval); - }); - - $: { - if ((data.url === '/' || data.url === '/completed' || data.url === '/schedule') && !$subsPlease) - fetch(root(`/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`)) - .then((r) => r.json()) - .then((r) => { - for (const day in subtitles) { - if (!r.schedule[day]) r.schedule[day] = []; - - (subtitles[day as keyof typeof subtitles] as SubsPleaseEpisode[]).forEach((episode) => { - r.schedule[day].push({ - title: episode.title, - page: episode.page || '', - image_url: episode.image_url || '', - time: episode.time - }); - }); - } - - subsPlease.set(r); - }); - } +import Spacer from "$lib/Layout/Spacer.svelte"; +import type { SubsPleaseEpisode } from "$lib/Media/Anime/Airing/Subtitled/subsPlease"; +import { env } from "$env/dynamic/public"; +import { userIdentity as getUserIdentity } from "$lib/Data/AniList/identity"; +import { onDestroy, onMount } from "svelte"; +import userIdentity from "$stores/identity"; +import settings from "$stores/settings"; +import { browser } from "$app/environment"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import "../app.css"; +import { readable, type Readable } from "svelte/store"; +import { navigating } from "$app/stores"; +import NotificationsProvider from "$lib/Notification/NotificationsProvider.svelte"; +import Root from "$lib/Home/Root.svelte"; +import root from "$lib/Utility/root"; +import { addMessages, init, locale as i18nLocale, locales } from "svelte-i18n"; +import english from "$lib/Locale/english"; +import japanese from "$lib/Locale/japanese"; +import type { LocaleDictionary } from "$lib/Locale/layout"; +import locale from "$stores/locale"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import subsPlease from "$stores/subsPlease"; +import Dropdown from "$lib/Layout/Dropdown.svelte"; +import { injectSpeedInsights } from "@vercel/speed-insights/sveltekit"; +import subtitles from "$lib/Data/Static/subtitles.json"; +import settingsSyncPulled from "$stores/settingsSyncPulled"; +import settingsSyncTimes from "$stores/settingsSyncTimes"; +import Announcement from "$lib/Announcement.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import { requestNotifications } from "$lib/Utility/notifications"; +import { database as userDatabase } from "$lib/Database/IDB/user"; +import CommandPalette from "$lib/CommandPalette/CommandPalette.svelte"; +import { defaultActions } from "$lib/CommandPalette/actions"; +import { toolsAsCommandPaletteActions } from "$lib/Tools/tools"; +import localforage from "localforage"; +import { dev } from "$app/environment"; +import { injectAnalytics } from "@vercel/analytics/sveltekit"; +import type { LayoutData } from "./$types"; + +injectSpeedInsights(); +injectAnalytics({ mode: dev ? "development" : "production" }); + +export let data: LayoutData; + +let isHeaderVisible = true; +let previousScrollPosition = 0; +let notificationInterval: ReturnType<typeof setInterval> | undefined = + undefined; + +addMessages("en", english as unknown as LocaleDictionary); +addMessages("ja", japanese as unknown as LocaleDictionary); +init({ fallbackLocale: "en", initialLocale: $settings.displayLanguage }); + +$: i18nLocale.set($settings.displayLanguage); + +const navigationOrder = [ + "/", + "/completed", + "/schedule", + "/updates", + "/tools", + "/settings", +]; +const previousPage: Readable<string | null> = readable(null, (set) => { + const unsubscribe = navigating.subscribe(($navigating) => { + if ($navigating && $navigating.from) + set($navigating.from.url.pathname as unknown as null); + }); + + return () => unsubscribe(); +}); + +$: way = data.url.includes("/user") + ? 200 + : $previousPage && $previousPage.includes("/user") + ? -200 + : navigationOrder.indexOf(data.url) > + navigationOrder.indexOf($previousPage ?? "/") + ? 200 + : -200; + +const handleScroll = () => { + const currentScrollPosition = window.scrollY; + + isHeaderVisible = + currentScrollPosition <= 100 || + currentScrollPosition < previousScrollPosition; + previousScrollPosition = currentScrollPosition; +}; + +onMount(async () => { + if (browser) { + if (await localforage.getItem("redirect")) { + window.location.href = (await localforage.getItem("redirect")) ?? "/"; + + await localforage.removeItem("redirect"); + } + + window.addEventListener("scroll", handleScroll); + + if ((await localforage.getItem("commit")) !== data.commit) { + await localforage.removeItem("identity"); + await localforage.removeItem("anime"); + await localforage.removeItem("manga"); + await localforage.removeItem("anime"); + await localforage.removeItem("manga"); + await localforage.removeItem("lastPruneTimes"); + await localforage.setItem("commit", data.commit); + } + } + + settings.get(); + + const currentIdentity = data.user + ? await getUserIdentity(data.user) + : undefined; + + if (currentIdentity) userIdentity.set(currentIdentity); + else if ($userIdentity.id !== -2) userIdentity.reset(); + + if ($settings.settingsSync && currentIdentity) + fetch(root(`/api/configuration?id=${currentIdentity.id}`)).then( + (response) => { + if (response.ok) + response.json().then((data) => { + if (data && data.configuration) { + console.log("Pulled remote configuration"); + settings.set(data.configuration); + settingsSyncPulled.set(true); + settingsSyncTimes.set({ + lastPull: new Date(), + lastPush: new Date(data.updated_at + "Z"), + }); + } + }); + }, + ); + + await userDatabase.users.where("id").below(0).delete(); + + if ( + currentIdentity && + !(await userDatabase.users.get(currentIdentity.id)) && + currentIdentity.id > 0 + ) + await userDatabase.users.put({ + id: currentIdentity.id, + user: data.user, + lastNotificationID: null, + }); + + if ($settings.displayAniListNotifications && currentIdentity) + if ("Notification" in window && navigator.serviceWorker) + requestNotifications(); +}); + +onDestroy(() => { + if (browser) window.removeEventListener("scroll", handleScroll); + + if (notificationInterval) clearInterval(notificationInterval); +}); + +$: { + if ( + (data.url === "/" || + data.url === "/completed" || + data.url === "/schedule") && + !$subsPlease + ) + fetch( + root( + `/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`, + ), + ) + .then((r) => r.json()) + .then((r) => { + for (const day in subtitles) { + if (!r.schedule[day]) r.schedule[day] = []; + + ( + subtitles[day as keyof typeof subtitles] as SubsPleaseEpisode[] + ).forEach((episode) => { + r.schedule[day].push({ + title: episode.title, + page: episode.page || "", + image_url: episode.image_url || "", + time: episode.time, + }); + }); + } + + subsPlease.set(r); + }); +} </script> <HeadTitle /> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index fb80bbdd..ad3971d8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,37 +1,185 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { onDestroy, onMount } from 'svelte'; - import MangaListTemplate from '$lib/List/Manga/MangaListTemplate.svelte'; - import UpcomingAnimeList from '$lib/List/Anime/UpcomingAnimeList.svelte'; - import userIdentity from '$stores/identity.js'; - import settings from '$stores/settings'; - import ListTitle from '$lib/List/ListTitle.svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import LastActivity from '$lib/Home/LastActivity.svelte'; - import { createHeightObserver } from '$lib/Utility/html.js'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import locale from '$stores/locale.js'; - import Landing from '$lib/Landing.svelte'; - import LandingHero from '$lib/LandingHero.svelte'; - import IndexColumn from '$lib/List/Anime/DueIndexColumn.svelte'; - import stateBin from '$stores/stateBin.js'; - - export let data; - - let heightObserver: ReturnType<typeof setInterval>; - - onMount(() => { - heightObserver = setInterval(() => createHeightObserver(), 0); - $stateBin.upcomingAnimeListOpen ??= true; - $stateBin.dueMangaListOpen ??= true; - }); - - onDestroy(() => clearInterval(heightObserver)); +import type { Component } from "svelte"; +import { browser } from "$app/environment"; +import Spacer from "$lib/Layout/Spacer.svelte"; +import { onDestroy, onMount } from "svelte"; +import userIdentity from "$stores/identity.js"; +import settings from "$stores/settings"; +import ListTitle from "$lib/List/ListTitle.svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import { createHeightObserver } from "$lib/Utility/html.js"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import locale from "$stores/locale.js"; +import Landing from "$lib/Landing.svelte"; +import LandingHero from "$lib/LandingHero.svelte"; +import stateBin, { hydrateStateBin } from "$stores/stateBin.js"; +import type { PageData } from "./$types"; + +let { data }: { data: PageData } = $props(); + +type UserProp = PageData["user"]; +type LastActivitySvelteComponent = Component<{ + user: UserProp; +}>; +type UpcomingAnimeListSvelteComponent = Component<{ + user: UserProp; +}>; +type IndexColumnSvelteComponent = Component<{ + user: UserProp; + userIdentity: { id: number }; +}>; +type MangaListTemplateSvelteComponent = Component<{ + user: UserProp; + displayUnresolved: boolean; + due: boolean; +}>; + +let removeHeightObserver: (() => void) | undefined; +let LastActivityComponent: LastActivitySvelteComponent | null = $state(null); +let UpcomingAnimeListComponent: UpcomingAnimeListSvelteComponent | null = + $state(null); +let IndexColumnComponent: IndexColumnSvelteComponent | null = $state(null); +let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null = + $state(null); +let authenticatedHomeSurfaceImport: Promise<void> | null = null; +let balancedListFlowElement: HTMLDivElement | undefined = $state(); +let balancedListResizeObserver: ResizeObserver | undefined; +let balancedListMeasurementFrame = 0; +const balancedListPanelElements = new Map<string, HTMLElement>(); +let balancedListPanelAssignments = $state<Record<string, "left" | "right">>({}); + +const resetBalancedListLayout = () => { + balancedListPanelAssignments = {}; +}; + +const updateBalancedListLayout = () => { + if (!balancedListFlowElement) return; + + const balancedListFlowStyles = getComputedStyle(balancedListFlowElement); + const balancedListRowGap = parseFloat( + balancedListFlowStyles.getPropertyValue("row-gap"), + ); + const balancedListColumnCount = balancedListFlowStyles + .getPropertyValue("grid-template-columns") + .split(" ") + .filter(Boolean).length; + + if (balancedListColumnCount <= 1 || !Number.isFinite(balancedListRowGap)) { + balancedListPanelAssignments = Object.fromEntries( + Array.from(balancedListPanelElements.keys()).map((key) => [key, "left"]), + ); + + return; + } + + const balancedListKeys = ["upcoming", "due", "manga"].filter((key) => + balancedListPanelElements.has(key), + ); + const balancedListHeights = { left: 0, right: 0 }; + const nextBalancedListAssignments: Record<string, "left" | "right"> = {}; + + balancedListKeys.forEach((key) => { + const balancedListPanel = balancedListPanelElements.get(key); + if (!balancedListPanel) return; + + const balancedListColumn = + balancedListHeights.left <= balancedListHeights.right ? "left" : "right"; + const balancedListPanelHeight = + balancedListPanel.getBoundingClientRect().height; + + nextBalancedListAssignments[key] = balancedListColumn; + balancedListHeights[balancedListColumn] += + balancedListPanelHeight + + (balancedListHeights[balancedListColumn] > 0 ? balancedListRowGap : 0); + }); + + balancedListPanelAssignments = nextBalancedListAssignments; +}; + +const queueBalancedListLayout = () => { + if (!browser) return; + + cancelAnimationFrame(balancedListMeasurementFrame); + + balancedListMeasurementFrame = requestAnimationFrame(() => { + updateBalancedListLayout(); + }); +}; + +const observeBalancedListPanel = (panel: HTMLElement, key: string) => { + balancedListPanelElements.set(key, panel); + balancedListResizeObserver?.observe(panel); + queueBalancedListLayout(); + + return { + destroy: () => { + balancedListPanelElements.delete(key); + balancedListResizeObserver?.unobserve(panel); + queueBalancedListLayout(); + }, + }; +}; + +const isBalancedListPanelInColumn = (key: string, column: "left" | "right") => + (balancedListPanelAssignments[key] ?? "left") === column; + +const loadAuthenticatedHomeSurface = () => { + if (data.user === undefined) return null; + if (authenticatedHomeSurfaceImport) return authenticatedHomeSurfaceImport; + + authenticatedHomeSurfaceImport = Promise.all([ + import("$lib/Home/LastActivity.svelte"), + import("$lib/List/Anime/UpcomingAnimeList.svelte"), + import("$lib/List/Anime/DueIndexColumn.svelte"), + import("$lib/List/Manga/MangaListTemplate.svelte"), + ]).then( + ([lastActivity, upcomingAnimeList, indexColumn, mangaListTemplate]) => { + LastActivityComponent = lastActivity.default; + UpcomingAnimeListComponent = upcomingAnimeList.default; + IndexColumnComponent = indexColumn.default; + MangaListTemplateComponent = mangaListTemplate.default; + }, + ); + + return authenticatedHomeSurfaceImport; +}; + +onMount(async () => { + removeHeightObserver = createHeightObserver(); + await hydrateStateBin(); + $stateBin.upcomingAnimeListOpen ??= true; + $stateBin.dueMangaListOpen ??= true; + + balancedListResizeObserver = new ResizeObserver(() => { + queueBalancedListLayout(); + }); + + balancedListPanelElements.forEach((panel) => { + balancedListResizeObserver?.observe(panel); + }); + + window.addEventListener("resize", queueBalancedListLayout); + void loadAuthenticatedHomeSurface(); + + queueBalancedListLayout(); +}); + +onDestroy(() => { + removeHeightObserver?.(); + balancedListResizeObserver?.disconnect(); + if (browser) { + cancelAnimationFrame(balancedListMeasurementFrame); + window.removeEventListener("resize", queueBalancedListLayout); + } + resetBalancedListLayout(); +}); </script> <HeadTitle /> -<LastActivity user={data.user} /> +{#if LastActivityComponent} + <LastActivityComponent user={data.user} /> +{/if} {#if data.user === undefined} <LandingHero /> @@ -40,57 +188,140 @@ <Landing /> {:else} + {@const balancedListColumnCount = + [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] + .map(Number) + .reduce((a, b) => a + b) > 1 + ? 2 + : 1} <div - class="grid-container" + bind:this={balancedListFlowElement} + class="balanced-list-flow" style={` - grid-template-columns: ${ - [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] - .map(Number) - .reduce((a, b) => a + b) > 1 - ? '1fr 1fr' - : '1fr' - } + --balanced-list-columns: ${balancedListColumnCount} `} > - <div class="left-column"> - {#if !$settings.disableUpcomingAnime} - <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> - {#if $userIdentity.id !== -2} - <UpcomingAnimeList user={data.user} /> + <div class="balanced-list-column"> + {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}> + <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> + {#if $userIdentity.id !== -2} + {#if UpcomingAnimeListComponent} + <UpcomingAnimeListComponent user={data.user} /> + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> + {/if} + + {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}> + {#if IndexColumnComponent} + <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> {:else} - <ListTitle title={$locale().lists.upcoming.episodes} /> + <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> + <ListTitle title={$locale().lists.due.episodes} /> - <Skeleton card={false} count={5} height="0.9rem" list /> + <Skeleton card={false} count={5} height="0.9rem" list /> + </details> {/if} - </details> + </div> {/if} - {#if !$settings.disableAnime && !$settings.disableManga} - <IndexColumn user={data.user} userIdentity={$userIdentity} /> + {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}> + <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> + {#if $userIdentity.id !== -2} + {#if MangaListTemplateComponent} + <MangaListTemplateComponent + user={data.user} + displayUnresolved={$settings.displayUnresolved} + due={true} + /> + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> {/if} </div> - <div class="right-column"> - {#if !$settings.disableAnime && $settings.disableManga} - <IndexColumn user={data.user} userIdentity={$userIdentity} /> - {/if} + {#if balancedListColumnCount > 1} + <div class="balanced-list-column"> + {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}> + <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> + {#if $userIdentity.id !== -2} + {#if UpcomingAnimeListComponent} + <UpcomingAnimeListComponent user={data.user} /> + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> - {#if !$settings.disableManga} - <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> - {#if $userIdentity.id !== -2} - <MangaListTemplate - user={data.user} - displayUnresolved={$settings.displayUnresolved} - due={true} - /> - {:else} - <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> - {/if} - </div> + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> + {/if} + + {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}> + {#if IndexColumnComponent} + <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> + {:else} + <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> + <ListTitle title={$locale().lists.due.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + </details> + {/if} + </div> + {/if} + + {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}> + <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> + {#if $userIdentity.id !== -2} + {#if MangaListTemplateComponent} + <MangaListTemplateComponent + user={data.user} + displayUnresolved={$settings.displayUnresolved} + due={true} + /> + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> + {/if} + </div> + {/if} {#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga} <video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop> @@ -101,30 +332,32 @@ {/if} <style> - .grid-container { + .balanced-list-flow { display: grid; + grid-template-columns: repeat(var(--balanced-list-columns), minmax(0, 1fr)); gap: 1rem; + align-items: start; } - .left-column { + .balanced-list-column { display: grid; gap: 1rem; align-content: start; + min-width: 0; } - .right-column { - align-self: start; + .balanced-list-panel { + min-width: 0; } .list { overflow-y: auto; - break-inside: avoid; - page-break-inside: avoid; + margin: 0; } @media (max-width: 800px) { - .grid-container { - grid-template-columns: 1fr !important; + .balanced-list-flow { + grid-template-columns: 1fr; } } </style> diff --git a/src/routes/anilist-badges-easter-event-2025/+page.svelte b/src/routes/anilist-badges-easter-event-2025/+page.svelte index d1a18967..b9c1295d 100644 --- a/src/routes/anilist-badges-easter-event-2025/+page.svelte +++ b/src/routes/anilist-badges-easter-event-2025/+page.svelte @@ -1,42 +1,42 @@ <script lang="ts"> - import { onMount } from 'svelte'; - import ClickableAreaPage from '$lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte'; - import MultipleChoicePage from '$lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte'; - import RiddlePage from '$lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte'; - import '$lib/Events/AniListBadges/EasterEvent2025/event.css'; +import { onMount } from "svelte"; +import ClickableAreaPage from "$lib/Events/AniListBadges/EasterEvent2025/ClickableAreaPage.svelte"; +import MultipleChoicePage from "$lib/Events/AniListBadges/EasterEvent2025/MultipleChoicePage.svelte"; +import RiddlePage from "$lib/Events/AniListBadges/EasterEvent2025/RiddlePage.svelte"; +import "$lib/Events/AniListBadges/EasterEvent2025/event.css"; - const multipleChoiceAnswers = [ - 'The Beginning After the End', - 'Domestic Girlfriend', - 'Spy Classroom', - 'Kaguya-sama: Love is War' - ]; - const clickableAreaAnswers = [ - 'https://media1.tenor.com/m/RkgfXhcewvoAAAAC/rui-tachibana-tachibana-rui.gif', - 'https://media1.tenor.com/m/fOBbcJOFZ-kAAAAC/hina-hina-tachibana.gif', - 'https://media1.tenor.com/m/GQT7FHW-w-IAAAAC/anime-domestic-girlfriend.gif' - ]; - let page = 0; +const multipleChoiceAnswers = [ + "The Beginning After the End", + "Domestic Girlfriend", + "Spy Classroom", + "Kaguya-sama: Love is War", +]; +const clickableAreaAnswers = [ + "https://media1.tenor.com/m/RkgfXhcewvoAAAAC/rui-tachibana-tachibana-rui.gif", + "https://media1.tenor.com/m/fOBbcJOFZ-kAAAAC/hina-hina-tachibana.gif", + "https://media1.tenor.com/m/GQT7FHW-w-IAAAAC/anime-domestic-girlfriend.gif", +]; +let page = 0; - onMount(() => { - const urlParameters = new URLSearchParams(window.location.search); - const pageParameter = urlParameters.get('page'); +onMount(() => { + const urlParameters = new URLSearchParams(window.location.search); + const pageParameter = urlParameters.get("page"); - if (pageParameter) page = parseInt(pageParameter) || 0; - }); + if (pageParameter) page = parseInt(pageParameter) || 0; +}); - const updatePage = (to: number | undefined = undefined) => { - const url = new URL(window.location.href); +const updatePage = (to: number | undefined = undefined) => { + const url = new URL(window.location.href); - if (to) { - page = to; - } else { - page += 1; - } + if (to) { + page = to; + } else { + page += 1; + } - url.searchParams.set('page', page.toString()); - window.history.replaceState(null, '', url.toString()); - }; + url.searchParams.set("page", page.toString()); + window.history.replaceState(null, "", url.toString()); +}; </script> <div class="card main-content"> diff --git a/src/routes/api/animeschedule/oauth/callback/+server.ts b/src/routes/api/animeschedule/oauth/callback/+server.ts index 2b96ab81..294abc05 100644 --- a/src/routes/api/animeschedule/oauth/callback/+server.ts +++ b/src/routes/api/animeschedule/oauth/callback/+server.ts @@ -1,17 +1,17 @@ -import { callback } from '$lib/Utility/oauth.js'; -import { env } from '$env/dynamic/private'; -import { env as env2 } from '$env/dynamic/public'; +import { callback } from "$lib/Utility/oauth.js"; +import { env } from "$env/dynamic/private"; +import { env as env2 } from "$env/dynamic/public"; export const GET = async ({ url, cookies }) => - callback({ - url, - cookies, - cookie: 'animeschedule', - authorise: 'https://animeschedule.net/api/v3/oauth2/token', - redirect: '/settings', - client: { - id: env2.PUBLIC_ANIMESCHEDULE_CLIENT_ID, - secret: env.ANIMESCHEDULE_CLIENT_SECRET, - redirectURI: env2.PUBLIC_ANIMESCHEDULE_REDIRECT_URI - } - }); + callback({ + url, + cookies, + cookie: "animeschedule", + authorise: "https://animeschedule.net/api/v3/oauth2/token", + redirect: "/settings", + client: { + id: env2.PUBLIC_ANIMESCHEDULE_CLIENT_ID, + secret: env.ANIMESCHEDULE_CLIENT_SECRET, + redirectURI: env2.PUBLIC_ANIMESCHEDULE_REDIRECT_URI, + }, + }); diff --git a/src/routes/api/authentication/log-out/+server.ts b/src/routes/api/authentication/log-out/+server.ts index 26b5dd2c..c04fa5c5 100644 --- a/src/routes/api/authentication/log-out/+server.ts +++ b/src/routes/api/authentication/log-out/+server.ts @@ -1,15 +1,15 @@ -import root from '$lib/Utility/root.js'; -import { redirect } from '@sveltejs/kit'; +import root from "$lib/Utility/root.js"; +import { redirect } from "@sveltejs/kit"; export const GET = ({ cookies }) => { - cookies.delete('user', { path: '/' }); - cookies.set('logout', '1', { - path: '/', - maxAge: 60 * 60 * 24 * 7, - httpOnly: false, - sameSite: 'lax', - secure: false - }); + cookies.delete("user", { path: "/" }); + cookies.set("logout", "1", { + path: "/", + maxAge: 60 * 60 * 24 * 7, + httpOnly: false, + sameSite: "lax", + secure: false, + }); - redirect(303, root('/')); + redirect(303, root("/")); }; diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts index 35ed4512..46b98cbc 100644 --- a/src/routes/api/badges/+server.ts +++ b/src/routes/api/badges/+server.ts @@ -1,149 +1,179 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; +import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; import { - removeAllUserBadges, - removeUserBadge, - updateUserBadge, - getUserBadges, - addUserBadge, - type Badge, - migrateCategory, - setShadowHidden, - setShadowHiddenBadge, - incrementClickCount -} from '$lib/Database/SB/User/badges'; -import privilegedUser from '$lib/Utility/privilegedUser'; - -const unauthorised = new Response('Unauthorised', { status: 401 }); + removeAllUserBadges, + removeUserBadge, + updateUserBadge, + getUserBadges, + addUserBadge, + type Badge, + type BadgeInput, + migrateCategory, + setShadowHidden, + setShadowHiddenBadge, + incrementClickCount, +} from "$lib/Database/SB/User/badges"; +import { Schema } from "effect"; +import { appOrigin, appOriginHeaders } from "$lib/Utility/appOrigin"; +import privilegedUser from "$lib/Utility/privilegedUser"; + +const unauthorised = () => new Response("Unauthorised", { status: 401 }); +const importedBadgeSchema = Schema.Record(Schema.String, Schema.Unknown); const badges = async (id: number) => - Response.json(await getUserBadges(id), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); + Response.json(await getUserBadges(id), { + headers: appOriginHeaders(), + }); export const GET = async ({ url }) => { - return await badges(Number(url.searchParams.get('id') || 0)); + return await badges(Number(url.searchParams.get("id") || 0)); }; export const DELETE = async ({ url, cookies }) => { - const userCookie = cookies.get('user'); + const userCookie = cookies.get("user"); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised(); - const user = JSON.parse(userCookie); - const identity = await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }); + const user = decodeAuthCookieOrNull(userCookie); - if ((url.searchParams.get('prune') || 0) === 'true') { - await removeAllUserBadges(identity.id); - } else { - await removeUserBadge(identity.id, Number(url.searchParams.get('id'))); - } + if (!user) return unauthorised(); - return await badges(identity.id); + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised(); + + if ((url.searchParams.get("prune") || 0) === "true") { + await removeAllUserBadges(identity.id); + } else { + await removeUserBadge(identity.id, Number(url.searchParams.get("id"))); + } + + return await badges(identity.id); }; export const PUT = async ({ cookies, url, request }) => { - if (url.searchParams.get('incrementClickCount') || undefined) { - await incrementClickCount(Number(url.searchParams.get('incrementClickCount'))); - - return new Response('Incremented', { status: 200 }); - } - - const userCookie = cookies.get('user'); - - if (!userCookie) return unauthorised; - - const user = JSON.parse(userCookie); - const identity = await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }); - const authorised = privilegedUser(identity.id); - - if (url.searchParams.get('shadowHide')) - setShadowHidden(Number(url.searchParams.get('shadowHide')), authorised); - - if (url.searchParams.get('import') || undefined) { - await Promise.all( - (await request.json()).map(async (badge: Badge) => await addUserBadge(identity.id, badge)) - ); - - return await badges(identity.id); - } else if (url.searchParams.get('migrate') || undefined) { - await migrateCategory( - identity.id, - url.searchParams.get('original') || '', - url.searchParams.get('new') || '' - ); - - return await badges(identity.id); - } - - if (url.searchParams.get('hide') || undefined) { - const allBadges = await getUserBadges(identity.id); - - await Promise.all( - allBadges - .filter((badge) => badge.category === (url.searchParams.get('category') || '')) - .map(async (badge) => { - await updateUserBadge(identity.id, badge.id as number, { - ...badge, - hidden: - allBadges - .filter((badge) => badge.category === (url.searchParams.get('category') || '')) - .filter((badge) => badge.hidden).length > - allBadges.filter( - (badge) => badge.category === (url.searchParams.get('category') || '') - ).length / - 2 - ? false - : true - }); - }) - ); - - return await badges(identity.id); - } - - if (url.searchParams.get('shadowHideBadge') || undefined) { - if (!authorised) return unauthorised; - - await setShadowHiddenBadge( - Number(url.searchParams.get('shadowHideBadge')), - url.searchParams.get('status') == 'true' ? false : true - ); - - return await badges(Number(url.searchParams.get('id'))); - } - - const badge = { - post: url.searchParams.get('post') || undefined, - image: url.searchParams.get('image') || undefined, - description: url.searchParams.get('description') || null, - time: url.searchParams.get('time') || undefined, - category: url.searchParams.get('category') || null, - hidden: url.searchParams.get('hidden') || false, - source: url.searchParams.get('source') || null, - designer: url.searchParams.get('designer') || null - }; - - if ( - (await getUserBadges(identity.id)).find( - (badge) => Number(badge.id) === Number(url.searchParams.get('update')) - ) - ) { - await updateUserBadge(identity.id, Number(url.searchParams.get('update')), badge as Badge); - } else { - await addUserBadge(identity.id, badge as Badge); - } - - return await badges(identity.id); + if (url.searchParams.get("incrementClickCount") || undefined) { + if (request.headers.get("origin") !== appOrigin()) return unauthorised(); + + await incrementClickCount( + Number(url.searchParams.get("incrementClickCount")), + ); + + return new Response("Incremented", { status: 200 }); + } + + const userCookie = cookies.get("user"); + + if (!userCookie) return unauthorised(); + + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised(); + + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised(); + const authorised = privilegedUser(identity.id); + + if (url.searchParams.get("shadowHide")) + await setShadowHidden( + Number(url.searchParams.get("shadowHide")), + authorised, + ); + + if (url.searchParams.get("import") || undefined) { + const importedBadges = await decodeRequestJsonOrThrow( + request, + Schema.Array(importedBadgeSchema), + ); + + await Promise.all( + importedBadges.map( + async (badge) => + await addUserBadge(identity.id, badge as unknown as BadgeInput), + ), + ); + + return await badges(identity.id); + } else if (url.searchParams.get("migrate") || undefined) { + await migrateCategory( + identity.id, + url.searchParams.get("original") || "", + url.searchParams.get("new") || "", + ); + + return await badges(identity.id); + } + + if (url.searchParams.get("hide") || undefined) { + const allBadges = await getUserBadges(identity.id); + + await Promise.all( + allBadges + .filter( + (badge) => + badge.category === (url.searchParams.get("category") || ""), + ) + .map(async (badge) => { + await updateUserBadge(identity.id, badge.id as number, { + ...badge, + hidden: + allBadges + .filter( + (badge) => + badge.category === (url.searchParams.get("category") || ""), + ) + .filter((badge) => badge.hidden).length > + allBadges.filter( + (badge) => + badge.category === (url.searchParams.get("category") || ""), + ).length / + 2 + ? false + : true, + }); + }), + ); + + return await badges(identity.id); + } + + if (url.searchParams.get("shadowHideBadge") || undefined) { + if (!authorised) return unauthorised(); + + await setShadowHiddenBadge( + Number(url.searchParams.get("shadowHideBadge")), + url.searchParams.get("status") === "true" ? false : true, + ); + + return await badges(Number(url.searchParams.get("id"))); + } + + const badge = { + post: url.searchParams.get("post") || undefined, + image: url.searchParams.get("image") || undefined, + description: url.searchParams.get("description") || null, + time: url.searchParams.get("time") || undefined, + category: url.searchParams.get("category") || null, + hidden: url.searchParams.get("hidden") || false, + source: url.searchParams.get("source") || null, + designer: url.searchParams.get("designer") || null, + }; + + if ( + (await getUserBadges(identity.id)).find( + (badge) => Number(badge.id) === Number(url.searchParams.get("update")), + ) + ) { + await updateUserBadge( + identity.id, + Number(url.searchParams.get("update")), + badge as Badge, + ); + } else { + await addUserBadge(identity.id, badge as Badge); + } + + return await badges(identity.id); }; diff --git a/src/routes/api/birthdays/primary/+server.ts b/src/routes/api/birthdays/primary/+server.ts index 774d47e1..109961f8 100644 --- a/src/routes/api/birthdays/primary/+server.ts +++ b/src/routes/api/birthdays/primary/+server.ts @@ -1,40 +1,40 @@ -import { JSDOM } from 'jsdom'; +import { JSDOM } from "jsdom"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; export const GET = async ({ url }: { url: URL }) => { - const document = new JSDOM( - await ( - await fetch( - `https://www.anisearch.com/character/birthdays?month=${url.searchParams.get('month')}` - ) - ).text() - ).window.document; - const section = document.querySelector(`#day-${url.searchParams.get('day')}`); - - if (!section) return Response.json([]); - - const ul = section.querySelector('ul.covers.simple'); - - if (!ul) return Response.json([]); - - return Response.json( - Array.from(ul.querySelectorAll('li')).map((li) => { - const anchor = li.querySelector('a'); - const title = li.querySelector('.title'); - - if (!anchor || !title) return { image: '', title: '' }; - - const image = li.getElementsByClassName('item-cover')[0]; - - return { - image: image ? image.getAttribute('src') : '', - name: title.textContent?.trim() - }; - }), - { - headers: { - 'Cache-Control': 'max-age=10800, s-maxage=10800', - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + const document = new JSDOM( + await ( + await fetch( + `https://www.anisearch.com/character/birthdays?month=${url.searchParams.get("month")}`, + ) + ).text(), + ).window.document; + const section = document.querySelector(`#day-${url.searchParams.get("day")}`); + + if (!section) return Response.json([]); + + const ul = section.querySelector("ul.covers.simple"); + + if (!ul) return Response.json([]); + + return Response.json( + Array.from(ul.querySelectorAll("li")).map((li) => { + const anchor = li.querySelector("a"); + const title = li.querySelector(".title"); + + if (!anchor || !title) return { image: "", title: "" }; + + const image = li.getElementsByClassName("item-cover")[0]; + + return { + image: image ? image.getAttribute("src") : "", + name: title.textContent?.trim(), + }; + }), + { + headers: appOriginHeaders({ + "Cache-Control": "max-age=10800, s-maxage=10800", + }), + }, + ); }; diff --git a/src/routes/api/birthdays/secondary/+server.ts b/src/routes/api/birthdays/secondary/+server.ts index 5036622f..4619828f 100644 --- a/src/routes/api/birthdays/secondary/+server.ts +++ b/src/routes/api/birthdays/secondary/+server.ts @@ -1,28 +1,29 @@ -import { env } from '$env/dynamic/private'; +import { env } from "$env/dynamic/private"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; export const GET = async ({ url }: { url: URL }) => { - return Response.json( - await ( - await fetch( - `https://www.animecharactersdatabase.com/api_series_characters.php?month=${url.searchParams.get( - 'month' - )}&day=${url.searchParams.get('day')}`, - { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0', - 'Cache-Control': 'max-age=10, s-maxage=10', - Cookie: `USTATS=${env.ACDB_COOKIE}` - } - } - ) - ).json(), - { - headers: { - 'Cache-Control': 'max-age=10800, s-maxage=10800', - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return Response.json( + await ( + await fetch( + `https://www.animecharactersdatabase.com/api_series_characters.php?month=${url.searchParams.get( + "month", + )}&day=${url.searchParams.get("day")}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", + "Cache-Control": "max-age=10, s-maxage=10", + Cookie: `USTATS=${env.ACDB_COOKIE}`, + }, + }, + ) + ).json(), + { + headers: appOriginHeaders({ + "Cache-Control": "max-age=10800, s-maxage=10800", + }), + }, + ); }; diff --git a/src/routes/api/configuration/+server.ts b/src/routes/api/configuration/+server.ts index 41a70f7b..306e1285 100644 --- a/src/routes/api/configuration/+server.ts +++ b/src/routes/api/configuration/+server.ts @@ -1,70 +1,65 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; +import { Schema } from "effect"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { - deleteUserConfiguration, - getUserConfiguration, - setUserConfiguration -} from '$lib/Database/SB/User/configuration'; + deleteUserConfiguration, + getUserConfiguration, + setUserConfiguration, +} from "$lib/Database/SB/User/configuration"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; +import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; -const unauthorised = new Response('Unauthorised', { status: 401 }); +const unauthorised = new Response("Unauthorised", { status: 401 }); -export const GET = async ({ url }) => - Response.json(await getUserConfiguration(Number(url.searchParams.get('id') || 0)), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); +const authenticatedUserId = async (cookies: { + get: (name: string) => string | undefined; +}) => { + const userCookie = cookies.get("user"); -export const PUT = async ({ cookies, request }) => { - const userCookie = cookies.get('user'); + if (!userCookie) return null; - if (!userCookie) return unauthorised; + const user = decodeAuthCookieOrNull(userCookie); - const user = JSON.parse(userCookie); + if (!user) return null; - return Response.json( - await setUserConfiguration( - ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id, - { - configuration: await request.json() - } - ), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return (await safeUserIdentity(user))?.id ?? null; }; -export const DELETE = async ({ cookies }) => { - const userCookie = cookies.get('user'); +export const GET = async ({ cookies, url }) => { + const userId = await authenticatedUserId(cookies); + const requestedUserId = Number(url.searchParams.get("id") || 0); + + if (!userId || requestedUserId !== userId) return unauthorised; - if (!userCookie) return unauthorised; + return Response.json(await getUserConfiguration(requestedUserId), { + headers: appOriginHeaders(), + }); +}; + +export const PUT = async ({ cookies, request }) => { + const userId = await authenticatedUserId(cookies); + + if (!userId) return unauthorised; + + return Response.json( + await setUserConfiguration(userId, { + configuration: await decodeRequestJsonOrThrow( + request, + Schema.Record(Schema.String, Schema.Unknown), + ), + }), + { + headers: appOriginHeaders(), + }, + ); +}; + +export const DELETE = async ({ cookies }) => { + const userId = await authenticatedUserId(cookies); - const user = JSON.parse(userCookie); + if (!userId) return unauthorised; - return Response.json( - await deleteUserConfiguration( - ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id - ), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return Response.json(await deleteUserConfiguration(userId), { + headers: appOriginHeaders(), + }); }; diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts index 22280394..f7fee7b8 100644 --- a/src/routes/api/events/+server.ts +++ b/src/routes/api/events/+server.ts @@ -1,8 +1,8 @@ -import { getEvents, getGroupEvents } from '$lib/Database/SB/events'; +import { getEvents, getGroupEvents } from "$lib/Database/SB/events"; export const GET = async ({ url }) => - Response.json( - url.searchParams.get('group') - ? await getGroupEvents(url.searchParams.get('group') || '') - : await getEvents() - ); + Response.json( + url.searchParams.get("group") + ? await getGroupEvents(url.searchParams.get("group") || "") + : await getEvents(), + ); diff --git a/src/routes/api/events/group/+server.ts b/src/routes/api/events/group/+server.ts index 9bd4c33a..b95db775 100644 --- a/src/routes/api/events/group/+server.ts +++ b/src/routes/api/events/group/+server.ts @@ -1,4 +1,4 @@ -import { getGroup } from '$lib/Database/SB/groups'; +import { getGroup } from "$lib/Database/SB/groups"; export const GET = async ({ url }) => - Response.json(await getGroup(url.searchParams.get('slug') || '')); + Response.json(await getGroup(url.searchParams.get("slug") || "")); diff --git a/src/routes/api/events/groups/+server.ts b/src/routes/api/events/groups/+server.ts index db50d0ad..d74b872c 100644 --- a/src/routes/api/events/groups/+server.ts +++ b/src/routes/api/events/groups/+server.ts @@ -1,3 +1,3 @@ -import { getGroups } from '$lib/Database/SB/groups'; +import { getGroups } from "$lib/Database/SB/groups"; export const GET = async () => Response.json(await getGroups()); diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts index 66a62024..9161b5c7 100644 --- a/src/routes/api/health/+server.ts +++ b/src/routes/api/health/+server.ts @@ -1 +1 @@ -export const GET = () => new Response('OK', { status: 200 }); +export const GET = () => new Response("OK", { status: 200 }); diff --git a/src/routes/api/myanimelist/oauth/callback/+server.ts b/src/routes/api/myanimelist/oauth/callback/+server.ts index 0bb78a64..57a5fbe4 100644 --- a/src/routes/api/myanimelist/oauth/callback/+server.ts +++ b/src/routes/api/myanimelist/oauth/callback/+server.ts @@ -1,18 +1,18 @@ -import { callback } from '$lib/Utility/oauth.js'; -import { env } from '$env/dynamic/private'; -import { env as env2 } from '$env/dynamic/public'; +import { callback } from "$lib/Utility/oauth.js"; +import { env } from "$env/dynamic/private"; +import { env as env2 } from "$env/dynamic/public"; export const GET = async ({ url, cookies }) => - callback({ - url, - cookies, - cookie: 'myanimelist', - authorise: 'https://myanimelist.net/v1/oauth2/token', - redirect: '/settings', - verifier: env.CODE_VERIFIER, - client: { - id: env2.PUBLIC_MYANIMELIST_CLIENT_ID, - secret: env.MYANIMELIST_CLIENT_SECRET, - redirectURI: env2.PUBLIC_MYANIMLIST_REDIRECT_URI - } - }); + callback({ + url, + cookies, + cookie: "myanimelist", + authorise: "https://myanimelist.net/v1/oauth2/token", + redirect: "/settings", + verifier: env.CODE_VERIFIER, + client: { + id: env2.PUBLIC_MYANIMELIST_CLIENT_ID, + secret: env.MYANIMELIST_CLIENT_SECRET, + redirectURI: env2.PUBLIC_MYANIMLIST_REDIRECT_URI, + }, + }); diff --git a/src/routes/api/notifications/subscribe/+server.ts b/src/routes/api/notifications/subscribe/+server.ts index d410dc9d..203470e0 100644 --- a/src/routes/api/notifications/subscribe/+server.ts +++ b/src/routes/api/notifications/subscribe/+server.ts @@ -1,27 +1,33 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; -import { setUserSubscription } from '$lib/Database/SB/User/notifications'; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { setUserSubscription } from "$lib/Database/SB/User/notifications"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; +import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; +import { Schema } from "effect"; -const unauthorised = new Response('Unauthorised', { status: 401 }); +const unauthorised = new Response("Unauthorised", { status: 401 }); export const POST = async ({ cookies, request, url }) => { - const userCookie = cookies.get('user'); - const fingerprint = url.searchParams.get('p'); + const userCookie = cookies.get("user"); + const fingerprint = url.searchParams.get("p"); - if (!userCookie || !fingerprint) return unauthorised; + if (!userCookie || !fingerprint) return unauthorised; - const user = JSON.parse(userCookie); - const userId = ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id; + const user = decodeAuthCookieOrNull(userCookie); - if (!userId) return unauthorised; + if (!user) return unauthorised; - await setUserSubscription(userId, await request.json(), fingerprint); + const userId = (await safeUserIdentity(user))?.id; - return new Response(null, { status: 200 }); + if (!userId) return unauthorised; + + await setUserSubscription( + userId, + (await decodeRequestJsonOrThrow( + request, + Schema.Record(Schema.String, Schema.Unknown), + )) as unknown as JSON, + fingerprint, + ); + + return new Response(null, { status: 200 }); }; diff --git a/src/routes/api/notifications/unsubscribe/+server.ts b/src/routes/api/notifications/unsubscribe/+server.ts index ded228f3..94bbd497 100644 --- a/src/routes/api/notifications/unsubscribe/+server.ts +++ b/src/routes/api/notifications/unsubscribe/+server.ts @@ -1,27 +1,24 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; -import { deleteUserSubscription } from '$lib/Database/SB/User/notifications'; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { deleteUserSubscription } from "$lib/Database/SB/User/notifications"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; -const unauthorised = new Response('Unauthorised', { status: 401 }); +const unauthorised = new Response("Unauthorised", { status: 401 }); export const POST = async ({ cookies, url }) => { - const userCookie = cookies.get('user'); - const fingerprint = url.searchParams.get('p'); + const userCookie = cookies.get("user"); + const fingerprint = url.searchParams.get("p"); - if (!userCookie || !fingerprint) return unauthorised; + if (!userCookie || !fingerprint) return unauthorised; - const user = JSON.parse(userCookie); - const userId = ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id; + const user = decodeAuthCookieOrNull(userCookie); - if (!userId) return unauthorised; + if (!user) return unauthorised; - await deleteUserSubscription(userId, fingerprint); + const userId = (await safeUserIdentity(user))?.id; - return new Response(null, { status: 200 }); + if (!userId) return unauthorised; + + await deleteUserSubscription(userId, fingerprint); + + return new Response(null, { status: 200 }); }; diff --git a/src/routes/api/oauth/callback/+server.ts b/src/routes/api/oauth/callback/+server.ts index 986520f9..c5faa859 100644 --- a/src/routes/api/oauth/callback/+server.ts +++ b/src/routes/api/oauth/callback/+server.ts @@ -1,16 +1,16 @@ -import { callback } from '$lib/Utility/oauth.js'; -import { env } from '$env/dynamic/private'; -import { env as env2 } from '$env/dynamic/public'; +import { callback } from "$lib/Utility/oauth.js"; +import { env } from "$env/dynamic/private"; +import { env as env2 } from "$env/dynamic/public"; export const GET = async ({ url, cookies }) => - callback({ - url, - cookies, - cookie: 'user', - authorise: 'https://anilist.co/api/v2/oauth/token', - client: { - id: env2.PUBLIC_ANILIST_CLIENT_ID, - secret: env.ANILIST_CLIENT_SECRET, - redirectURI: env2.PUBLIC_ANILIST_REDIRECT_URI - } - }); + callback({ + url, + cookies, + cookie: "user", + authorise: "https://anilist.co/api/v2/oauth/token", + client: { + id: env2.PUBLIC_ANILIST_CLIENT_ID, + secret: env.ANILIST_CLIENT_SECRET, + redirectURI: env2.PUBLIC_ANILIST_REDIRECT_URI, + }, + }); diff --git a/src/routes/api/oauth/refresh/+server.ts b/src/routes/api/oauth/refresh/+server.ts index 13f4400c..13e7ab09 100644 --- a/src/routes/api/oauth/refresh/+server.ts +++ b/src/routes/api/oauth/refresh/+server.ts @@ -1,30 +1,30 @@ -import { env } from '$env/dynamic/private'; -import { env as env2 } from '$env/dynamic/public'; -import { redirect } from '@sveltejs/kit'; +import { env } from "$env/dynamic/private"; +import { env as env2 } from "$env/dynamic/public"; +import { redirect } from "@sveltejs/kit"; export const GET = async ({ url, cookies }) => { - const formData = new FormData(); + const formData = new FormData(); - formData.append('grant_type', 'refresh_token'); - formData.append('client_id', env2.PUBLIC_ANILIST_CLIENT_ID as string); - formData.append('client_secret', env.ANILIST_CLIENT_SECRET as string); - formData.append('refresh_token', url.searchParams.get('token') || ''); + formData.append("grant_type", "refresh_token"); + formData.append("client_id", env2.PUBLIC_ANILIST_CLIENT_ID as string); + formData.append("client_secret", env.ANILIST_CLIENT_SECRET as string); + formData.append("refresh_token", url.searchParams.get("token") || ""); - const newUser = await ( - await fetch('https://anilist.co/api/v2/oauth/token', { - method: 'POST', - body: formData - }) - ).json(); + const newUser = await ( + await fetch("https://anilist.co/api/v2/oauth/token", { + method: "POST", + body: formData, + }) + ).json(); - cookies.set('user', JSON.stringify(newUser), { - path: '/', - maxAge: 60 * 60 * 24 * 7, - httpOnly: false, - sameSite: 'lax', - secure: false - }); + cookies.set("user", JSON.stringify(newUser), { + path: "/", + maxAge: 60 * 60 * 24 * 7, + httpOnly: false, + sameSite: "lax", + secure: false, + }); - if (url.searchParams.get('redirect')) redirect(303, '/'); - else return Response.json(newUser); + if (url.searchParams.get("redirect")) redirect(303, "/"); + else return Response.json(newUser); }; diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts index 0fb91f45..47ce442b 100644 --- a/src/routes/api/preferences/+server.ts +++ b/src/routes/api/preferences/+server.ts @@ -1,89 +1,100 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; +import { Schema } from "effect"; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; import { - getUserPreferences, - toggleHideMissingBadges, - setCSS, - setBiography, - toggleHideAWCBadges, - togglePinnedBadgeWallCategory, - setPinnedBadgeWallCategories -} from '$lib/Database/SB/User/preferences'; + getUserPreferences, + setBiography, + setCSS, + setPinnedBadgeWallCategories, + toggleHideAWCBadges, + toggleHideMissingBadges, + togglePinnedBadgeWallCategory, +} from "$lib/Database/SB/User/preferences"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; +import { decodeRequestJsonOrThrow } from "$lib/Effect/requestBody"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; -const unauthorised = new Response('Unauthorised', { status: 401 }); +const unauthorised = new Response("Unauthorised", { status: 401 }); + +const authenticatedUserId = async (cookies: { + get: (name: string) => string | undefined; +}) => { + const userCookie = cookies.get("user"); + + if (!userCookie) return null; + + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return null; + + return (await safeUserIdentity(user))?.id ?? null; +}; export const GET = async ({ url }) => { - const preferences = await getUserPreferences(Number(url.searchParams.get('id') || 0)); + const requestedUserId = Number(url.searchParams.get("id") || 0); - return Response.json(preferences ? preferences : {}, { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); + const preferences = await getUserPreferences(requestedUserId); + + return Response.json(preferences ? preferences : {}, { + headers: appOriginHeaders(), + }); }; export const PUT = async ({ url, cookies, request }) => { - const userCookie = cookies.get('user'); - - if (!userCookie) return unauthorised; - - const user = JSON.parse(userCookie); - const userId = ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id; - - if (url.searchParams.get('toggleHideMissingBadges') !== null) - return Response.json(await toggleHideMissingBadges(userId), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); - - if (url.searchParams.get('toggleHideAWCBadges') !== null) - return Response.json(await toggleHideAWCBadges(userId), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); - - if (url.searchParams.get('badgeWallCSS') !== null) - return Response.json(await setCSS(userId, await request.text()), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); - - if (url.searchParams.get('toggleCategory') !== null) - return Response.json( - await togglePinnedBadgeWallCategory(userId, url.searchParams.get('toggleCategory') || ''), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); - - if (url.searchParams.get('setCategories') !== null) - return Response.json(await setPinnedBadgeWallCategories(userId, await request.json()), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); - - if (url.searchParams.get('biography') !== null) - return Response.json(await setBiography(userId, (await request.text()).slice(0, 3000)), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); - - return Response.json(await getUserPreferences(Number(url.searchParams.get('id') || 0)), { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); + const userId = await authenticatedUserId(cookies); + + if (!userId) return unauthorised; + + if (url.searchParams.get("toggleHideMissingBadges") !== null) + return Response.json(await toggleHideMissingBadges(userId), { + headers: appOriginHeaders(), + }); + + if (url.searchParams.get("toggleHideAWCBadges") !== null) + return Response.json(await toggleHideAWCBadges(userId), { + headers: appOriginHeaders(), + }); + + if (url.searchParams.get("badgeWallCSS") !== null) + return Response.json(await setCSS(userId, await request.text()), { + headers: appOriginHeaders(), + }); + + if (url.searchParams.get("toggleCategory") !== null) + return Response.json( + await togglePinnedBadgeWallCategory( + userId, + url.searchParams.get("toggleCategory") || "", + ), + { + headers: appOriginHeaders(), + }, + ); + + if (url.searchParams.get("setCategories") !== null) + return Response.json( + await setPinnedBadgeWallCategories(userId, [ + ...(await decodeRequestJsonOrThrow( + request, + Schema.Array(Schema.String), + )), + ]), + { + headers: appOriginHeaders(), + }, + ); + + if (url.searchParams.get("biography") !== null) + return Response.json( + await setBiography(userId, (await request.text()).slice(0, 3000)), + { + headers: appOriginHeaders(), + }, + ); + + return Response.json( + await getUserPreferences(Number(url.searchParams.get("id") || 0)), + { + headers: appOriginHeaders(), + }, + ); }; diff --git a/src/routes/api/preferences/pin/+server.ts b/src/routes/api/preferences/pin/+server.ts index 28398cf0..b69a8142 100644 --- a/src/routes/api/preferences/pin/+server.ts +++ b/src/routes/api/preferences/pin/+server.ts @@ -1,32 +1,32 @@ -import { userIdentity } from '$lib/Data/AniList/identity'; -import { toggleHololiveStreamPinning } from '$lib/Database/SB/User/preferences'; +import { safeUserIdentity } from "$lib/Data/AniList/identity"; +import { decodeAuthCookieOrNull } from "$lib/Effect/authCookie"; +import { toggleHololiveStreamPinning } from "$lib/Database/SB/User/preferences"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; -const unauthorised = new Response('Unauthorised', { status: 401 }); +const unauthorised = new Response("Unauthorised", { status: 401 }); export const PUT = async ({ cookies, url }) => { - const userCookie = cookies.get('user'); - - if (!userCookie) return unauthorised; - - const user = JSON.parse(userCookie); - - return Response.json( - await toggleHololiveStreamPinning( - ( - await userIdentity({ - tokenType: user['token_type'], - expiresIn: user['expires_in'], - accessToken: user['access_token'], - refreshToken: user['refresh_token'] - }) - ).id, - url.searchParams.get('stream') || '' - ), - { - headers: { - method: 'PUT', - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + const userCookie = cookies.get("user"); + + if (!userCookie) return unauthorised; + + const user = decodeAuthCookieOrNull(userCookie); + + if (!user) return unauthorised; + + const identity = await safeUserIdentity(user); + + if (!identity) return unauthorised; + + return Response.json( + await toggleHololiveStreamPinning( + identity.id, + url.searchParams.get("stream") || "", + ), + { + headers: appOriginHeaders({ + method: "PUT", + }), + }, + ); }; diff --git a/src/routes/api/subsplease/+server.ts b/src/routes/api/subsplease/+server.ts index 93e734c2..1f678d8c 100644 --- a/src/routes/api/subsplease/+server.ts +++ b/src/routes/api/subsplease/+server.ts @@ -1,16 +1,18 @@ -export const GET = async ({ url }) => - Response.json( - await ( - await fetch( - `https://subsplease.org/api/?f=schedule&tz=${ - url.searchParams.get('tz') || 'America/Los_Angeles' - }` - ) - ).json(), - { - headers: { - 'Cache-Control': 'max-age=86400, s-maxage=86400', - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); +import { appOriginHeaders } from "$lib/Utility/appOrigin"; + +export const GET = async ({ url }) => { + const timezone = url.searchParams.get("tz") || "America/Los_Angeles"; + + return Response.json( + await ( + await fetch( + `https://subsplease.org/api/?f=schedule&tz=${encodeURIComponent(timezone)}`, + ) + ).json(), + { + headers: appOriginHeaders({ + "Cache-Control": "max-age=86400, s-maxage=86400", + }), + }, + ); +}; diff --git a/src/routes/api/updates/all-novels/+server.ts b/src/routes/api/updates/all-novels/+server.ts index 5dd2aaad..10a0a436 100644 --- a/src/routes/api/updates/all-novels/+server.ts +++ b/src/routes/api/updates/all-novels/+server.ts @@ -1,20 +1,24 @@ +import { appOriginHeaders } from "$lib/Utility/appOrigin"; + export const GET = async ({ setHeaders }) => { - setHeaders({ - 'Cache-Control': 'public, max-age=600, s-maxage=600' - }); + setHeaders({ + "Cache-Control": "public, max-age=600, s-maxage=600", + }); - return Response.json( - await ( - await fetch('https://www.wlnupdates.com/api', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': 'https://due.moe' - }, - body: JSON.stringify({ - mode: 'get-releases' - }) - }) - ).json() - ); + return Response.json( + await ( + await fetch("https://www.wlnupdates.com/api", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + mode: "get-releases", + }), + }) + ).json(), + { + headers: appOriginHeaders(), + }, + ); }; diff --git a/src/routes/api/updates/manga/+server.ts b/src/routes/api/updates/manga/+server.ts index af078c05..f8f90c91 100644 --- a/src/routes/api/updates/manga/+server.ts +++ b/src/routes/api/updates/manga/+server.ts @@ -1,18 +1,17 @@ -import Parser from 'rss-parser'; +import Parser from "rss-parser"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; export const GET = async ({ setHeaders }) => { - setHeaders({ - 'Cache-Control': 'public, max-age=600, s-maxage=600' - }); + setHeaders({ + "Cache-Control": "public, max-age=600, s-maxage=600", + }); - return Response.json( - await new Parser().parseString( - await (await fetch('https://www.mangaupdates.com/rss.php')).text() - ), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return Response.json( + await new Parser().parseString( + await (await fetch("https://www.mangaupdates.com/rss.php")).text(), + ), + { + headers: appOriginHeaders(), + }, + ); }; diff --git a/src/routes/api/updates/novels/+server.ts b/src/routes/api/updates/novels/+server.ts index 7ca6ca43..5e33a2ff 100644 --- a/src/routes/api/updates/novels/+server.ts +++ b/src/routes/api/updates/novels/+server.ts @@ -1,18 +1,17 @@ -import Parser from 'rss-parser'; +import Parser from "rss-parser"; +import { appOriginHeaders } from "$lib/Utility/appOrigin"; export const GET = async ({ setHeaders }) => { - setHeaders({ - 'Cache-Control': 'public, max-age=600, s-maxage=600' - }); + setHeaders({ + "Cache-Control": "public, max-age=600, s-maxage=600", + }); - return Response.json( - await new Parser().parseString( - await (await fetch('https://api.syosetu.com/allnovel.Atom')).text() - ), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return Response.json( + await new Parser().parseString( + await (await fetch("https://api.syosetu.com/allnovel.Atom")).text(), + ), + { + headers: appOriginHeaders(), + }, + ); }; diff --git a/src/routes/completed/+page.svelte b/src/routes/completed/+page.svelte index 3c6b3b4d..bcad912b 100644 --- a/src/routes/completed/+page.svelte +++ b/src/routes/completed/+page.svelte @@ -1,35 +1,74 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import { onDestroy, onMount } from 'svelte'; - import userIdentity from '$stores/identity.js'; - import settings from '$stores/settings'; - import WatchingAnimeList from '$lib/List/Anime/CompletedAnimeList.svelte'; - import ListTitle from '$lib/List/ListTitle.svelte'; - import MangaListTemplate from '$lib/List/Manga/MangaListTemplate.svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import LastActivity from '$lib/Home/LastActivity.svelte'; - import { createHeightObserver } from '$lib/Utility/html.js'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import locale from '$stores/locale.js'; - import Landing from '$lib/Landing.svelte'; - import stateBin from '$stores/stateBin.js'; - - export let data; - - let heightObserver: ReturnType<typeof setInterval>; - - onMount(() => { - heightObserver = setInterval(() => createHeightObserver(), 0); - $stateBin.completedAnimeListOpen ??= true; - $stateBin.completedMangaListOpen ??= true; - }); - - onDestroy(() => clearInterval(heightObserver)); +import type { Component } from "svelte"; +import Spacer from "$lib/Layout/Spacer.svelte"; +import { onDestroy, onMount } from "svelte"; +import userIdentity from "$stores/identity.js"; +import settings from "$stores/settings"; +import ListTitle from "$lib/List/ListTitle.svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import { createHeightObserver } from "$lib/Utility/html.js"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import locale from "$stores/locale.js"; +import Landing from "$lib/Landing.svelte"; +import stateBin, { hydrateStateBin } from "$stores/stateBin.js"; +import type { PageData } from "./$types"; + +let { data }: { data: PageData } = $props(); + +type UserProp = PageData["user"]; +type LastActivitySvelteComponent = Component<{ + user: UserProp; +}>; +type WatchingAnimeListSvelteComponent = Component<{ + user: UserProp; +}>; +type MangaListTemplateSvelteComponent = Component<{ + user: UserProp; + displayUnresolved: boolean; + due: boolean; +}>; + +let removeHeightObserver: (() => void) | undefined; +let LastActivityComponent: LastActivitySvelteComponent | null = $state(null); +let WatchingAnimeListComponent: WatchingAnimeListSvelteComponent | null = + $state(null); +let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null = + $state(null); +let completedSurfaceImport: Promise<void> | null = null; + +const loadCompletedSurface = () => { + if (data.user === undefined) return null; + if (completedSurfaceImport) return completedSurfaceImport; + + completedSurfaceImport = Promise.all([ + import("$lib/Home/LastActivity.svelte"), + import("$lib/List/Anime/CompletedAnimeList.svelte"), + import("$lib/List/Manga/MangaListTemplate.svelte"), + ]).then(([lastActivity, watchingAnimeList, mangaListTemplate]) => { + LastActivityComponent = lastActivity.default; + WatchingAnimeListComponent = watchingAnimeList.default; + MangaListTemplateComponent = mangaListTemplate.default; + }); + + return completedSurfaceImport; +}; + +onMount(async () => { + removeHeightObserver = createHeightObserver(); + await hydrateStateBin(); + $stateBin.completedAnimeListOpen ??= true; + $stateBin.completedMangaListOpen ??= true; + void loadCompletedSurface(); +}); + +onDestroy(() => removeHeightObserver?.()); </script> <HeadTitle route="Completed" path="/completed" /> -<LastActivity user={data.user} /> +{#if LastActivityComponent} + <LastActivityComponent user={data.user} /> +{/if} {#if data.user === undefined} <div class="card">Please log in to view completed media.</div> @@ -42,7 +81,13 @@ {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableAnime} <details bind:open={$stateBin.completedAnimeListOpen} class="list"> {#if $userIdentity.id !== -2} - <WatchingAnimeList user={data.user} /> + {#if WatchingAnimeListComponent} + <WatchingAnimeListComponent user={data.user} /> + {:else} + <ListTitle title={$locale().lists.completed.anime} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} {:else} <ListTitle title={$locale().lists.completed.anime} /> @@ -54,11 +99,17 @@ {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableManga} <details bind:open={$stateBin.completedMangaListOpen} class="list"> {#if $userIdentity.id !== -2} - <MangaListTemplate - user={data.user} - displayUnresolved={$settings.displayUnresolved} - due={false} - /> + {#if MangaListTemplateComponent} + <MangaListTemplateComponent + user={data.user} + displayUnresolved={$settings.displayUnresolved} + due={false} + /> + {:else} + <ListTitle title={$locale().lists.completed.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} {:else} <ListTitle title={$locale().lists.completed.mangaAndLightNovels} /> diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index fa833c1f..88a3da9e 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -1,9 +1,9 @@ <script> - import Spacer from '$lib/Layout/Spacer.svelte'; - import Event from '$lib/Events/Event.svelte'; - import Message from '$lib/Loading/Message.svelte'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import Event from "$lib/Events/Event.svelte"; +import Message from "$lib/Loading/Message.svelte"; - import root from '$lib/Utility/root'; +import root from "$lib/Utility/root"; </script> {#await fetch(root(`/api/events`))} diff --git a/src/routes/events/group/[group]/+page.server.ts b/src/routes/events/group/[group]/+page.server.ts index 3df284fb..233aaab8 100644 --- a/src/routes/events/group/[group]/+page.server.ts +++ b/src/routes/events/group/[group]/+page.server.ts @@ -1,5 +1,5 @@ export const load = ({ params }) => { - return { - group: params.group - }; + return { + group: params.group, + }; }; diff --git a/src/routes/events/group/[group]/+page.svelte b/src/routes/events/group/[group]/+page.svelte index 6265fb75..c02c1b51 100644 --- a/src/routes/events/group/[group]/+page.svelte +++ b/src/routes/events/group/[group]/+page.svelte @@ -1,24 +1,25 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import type { Group as GroupType } from '$lib/Database/SB/groups.js'; - import type { Event as EventType } from '$lib/Database/SB/events.js'; - import Message from '$lib/Loading/Message.svelte'; - import root from '$lib/Utility/root'; - import { onMount } from 'svelte'; - import Group from '$lib/Events/Group.svelte'; - import Event from '$lib/Events/Event.svelte'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import type { Group as GroupType } from "$lib/Database/SB/groups.js"; +import type { Event as EventType } from "$lib/Database/SB/events.js"; +import Message from "$lib/Loading/Message.svelte"; +import root from "$lib/Utility/root"; +import { onMount } from "svelte"; +import Group from "$lib/Events/Group.svelte"; +import Event from "$lib/Events/Event.svelte"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; - let groupsResponse: Promise<Response>; +let groupsResponse: Promise<Response>; - onMount(async () => { - groupsResponse = fetch(root(`/api/events/group?slug=${data.group}`)); - }); +onMount(async () => { + groupsResponse = fetch(root(`/api/events/group?slug=${data.group}`)); +}); - const asGroup = (group: unknown) => group as GroupType; +const asGroup = (group: unknown) => group as GroupType; - const asEvent = (event: unknown) => event as EventType; +const asEvent = (event: unknown) => event as EventType; </script> {#await groupsResponse} diff --git a/src/routes/events/groups/+page.svelte b/src/routes/events/groups/+page.svelte index 685225be..b6181ad8 100644 --- a/src/routes/events/groups/+page.svelte +++ b/src/routes/events/groups/+page.svelte @@ -1,18 +1,18 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import type { Group as GroupType } from '$lib/Database/SB/groups'; - import Message from '$lib/Loading/Message.svelte'; - import root from '$lib/Utility/root'; - import { onMount } from 'svelte'; - import Group from '$lib/Events/Group.svelte'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import type { Group as GroupType } from "$lib/Database/SB/groups"; +import Message from "$lib/Loading/Message.svelte"; +import root from "$lib/Utility/root"; +import { onMount } from "svelte"; +import Group from "$lib/Events/Group.svelte"; - let groupsResponse: Promise<Response>; +let groupsResponse: Promise<Response>; - onMount(async () => { - groupsResponse = fetch(root('/api/events/groups')); - }); +onMount(async () => { + groupsResponse = fetch(root("/api/events/groups")); +}); - const asGroup = (group: unknown) => group as GroupType; +const asGroup = (group: unknown) => group as GroupType; </script> {#await groupsResponse} diff --git a/src/routes/feeds/activity-notifications/+server.ts b/src/routes/feeds/activity-notifications/+server.ts index 37190255..70b24a20 100644 --- a/src/routes/feeds/activity-notifications/+server.ts +++ b/src/routes/feeds/activity-notifications/+server.ts @@ -1,54 +1,65 @@ -import { notifications, type Notification } from '$lib/Data/AniList/notifications'; -import root from '$lib/Utility/root'; +import { + notifications, + type Notification, +} from "$lib/Data/AniList/notifications"; +import { siteUrl } from "$lib/Utility/appOrigin"; +import root from "$lib/Utility/root"; const htmlEncode = (input: string) => { - return input.replace(/[\u00A0-\u9999<>&]/g, (i) => '&#' + i.charCodeAt(0) + ';'); + return input.replace( + /[\u00A0-\u9999<>&]/g, + (i) => "&#" + i.charCodeAt(0) + ";", + ); }; -const render = (posts: Notification[] = []) => `<?xml version="1.0" encoding="UTF-8" ?> +const render = ( + posts: Notification[] = [], +) => `<?xml version="1.0" encoding="UTF-8" ?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" - xmlns:content="http://purl.org/rss/1.0/modules/content/" - xmlns:snf="http://www.smartnews.be/snf" - xmlns:media="http://search.yahoo.com/mrss/"> +xmlns:content="http://purl.org/rss/1.0/modules/content/" +xmlns:snf="http://www.smartnews.be/snf" +xmlns:media="http://search.yahoo.com/mrss/"> <channel> - <atom:link href="https://due.moe/feeds/activity-notifications" rel="self" type="application/rss+xml" /> + <atom:link href="${siteUrl("/feeds/activity-notifications")}" rel="self" type="application/rss+xml" /> <title>AniList Notifications • due.moe</title> - <link>https://due.moe</link> + <link>${siteUrl("/")}</link> <description>Instantly view your AniList notifications via RSS!</description> <pubDate>${new Date().toUTCString()}</pubDate> <lastBuildDate>${new Date().toUTCString()}</lastBuildDate> <language>en-US</language> - <snf:logo><url>https://due.moe/favicon-196x196.png</url></snf:logo> + <snf:logo><url>${siteUrl("/favicon-196x196.png")}</url></snf:logo> ${posts - .filter((notification: Notification) => notification.type !== undefined) - .map((notification: Notification) => { - let title = `${notification.user.name}${notification.context}`; - let link = `https://anilist.co/user/${notification.user.name}`; - const prettyType = notification.type - .toString() - .replace(/_/g, ' ') - .toLowerCase() - .replace(/\w\S*/g, (text) => { - return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); - }); + .filter((notification: Notification) => notification.type !== undefined) + .map((notification: Notification) => { + let title = `${notification.user.name}${notification.context}`; + let link = `https://anilist.co/user/${notification.user.name}`; + const prettyType = notification.type + .toString() + .replace(/_/g, " ") + .toLowerCase() + .replace(/\w\S*/g, (text) => { + return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); + }); - try { - if ( - !['FOLLOWING', 'ACTIVITY_MESSAGE'].includes(notification.type.toString()) && - !notification.type.toString().includes('THREAD') - ) { - link = `https://anilist.co/activity/${notification.activity.id}`; - } else if (notification.type.toString().includes('THREAD')) { - title += `${notification.thread.title}`; - link = `https://anilist.co/forum/thread/${notification.thread.id}`; - } - } catch { - return ''; - } + try { + if ( + !["FOLLOWING", "ACTIVITY_MESSAGE"].includes( + notification.type.toString(), + ) && + !notification.type.toString().includes("THREAD") + ) { + link = `https://anilist.co/activity/${notification.activity.id}`; + } else if (notification.type.toString().includes("THREAD")) { + title += `${notification.thread.title}`; + link = `https://anilist.co/forum/thread/${notification.thread.id}`; + } + } catch { + return ""; + } - return `<item> + return `<item> <guid isPermaLink="false">${notification.id}</guid> <title>${htmlEncode(title)}</title> <link>${link}</link> @@ -57,29 +68,29 @@ const render = (posts: Notification[] = []) => `<?xml version="1.0" encoding="UT <category>${prettyType}</category> <pubDate>${new Date(notification.createdAt * 1000).toUTCString()}</pubDate> </item>`; - }) - .join('')} + }) + .join("")} </channel> </rss> `; export const GET = async ({ url }) => { - let token = url.searchParams.get('token'); - const refresh = url.searchParams.get('refresh'); - let notification = await notifications(token || ''); + let token = url.searchParams.get("token"); + const refresh = url.searchParams.get("refresh"); + let notification = await notifications(token || ""); - if (notification === null) { - token = (await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json())[ - 'access_token' - ]; + if (notification === null) { + token = ( + await (await fetch(root(`/api/oauth/refresh?token=${refresh}`))).json() + )["access_token"]; - notification = await notifications(token as string); - } + notification = await notifications(token as string); + } - return new Response(token ? render(notification || []) : render(), { - headers: { - 'Cache-Control': `max-age=0`, - 'Content-Type': 'application/xml' - } - }); + return new Response(token ? render(notification || []) : render(), { + headers: { + "Cache-Control": `max-age=0`, + "Content-Type": "application/xml", + }, + }); }; diff --git a/src/routes/girls/+page.svelte b/src/routes/girls/+page.svelte index 0dcf75c4..88f06460 100644 --- a/src/routes/girls/+page.svelte +++ b/src/routes/girls/+page.svelte @@ -1,11 +1,11 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import Senpy from '$lib/Data/senpy'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import root from '$lib/Utility/root'; - import '$styles/girls.scss'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import Senpy from "$lib/Data/senpy"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import root from "$lib/Utility/root"; +import "$styles/girls.scss"; </script> <HeadTitle route="Anime Girls Holding Programming Books" path="/girls" /> diff --git a/src/routes/girls/[language]/+page.server.ts b/src/routes/girls/[language]/+page.server.ts index 62fbb311..71595b9e 100644 --- a/src/routes/girls/[language]/+page.server.ts +++ b/src/routes/girls/[language]/+page.server.ts @@ -1,5 +1,5 @@ export const load = ({ params }) => { - return { - language: params.language - }; + return { + language: params.language, + }; }; diff --git a/src/routes/girls/[language]/+page.svelte b/src/routes/girls/[language]/+page.svelte index 91b18628..2a97605f 100644 --- a/src/routes/girls/[language]/+page.svelte +++ b/src/routes/girls/[language]/+page.svelte @@ -1,10 +1,11 @@ <script lang="ts"> - import Senpy from '$lib/Data/senpy'; - import Message from '$lib/Loading/Message.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import '$styles/girls.scss'; +import Senpy from "$lib/Data/senpy"; +import Message from "$lib/Loading/Message.svelte"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import "$styles/girls.scss"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; </script> <div class="card"> diff --git a/src/routes/graphql/+server.ts b/src/routes/graphql/+server.ts index f8e47dc5..2b19b3ff 100644 --- a/src/routes/graphql/+server.ts +++ b/src/routes/graphql/+server.ts @@ -1 +1,5 @@ -export { default as GET, default as POST, default as OPTIONS } from '$graphql/server'; +export { + default as GET, + default as POST, + default as OPTIONS, +} from "$graphql/server"; diff --git a/src/routes/hololive/[[stream]]/+page.server.ts b/src/routes/hololive/[[stream]]/+page.server.ts index 6eb5dad1..04184eb3 100644 --- a/src/routes/hololive/[[stream]]/+page.server.ts +++ b/src/routes/hololive/[[stream]]/+page.server.ts @@ -1,5 +1,5 @@ export const load = ({ params }) => { - return { - stream: params.stream - }; + return { + stream: params.stream, + }; }; diff --git a/src/routes/hololive/[[stream]]/+page.svelte b/src/routes/hololive/[[stream]]/+page.svelte index 15d2125c..573c16aa 100644 --- a/src/routes/hololive/[[stream]]/+page.svelte +++ b/src/routes/hololive/[[stream]]/+page.svelte @@ -1,50 +1,52 @@ <script lang="ts"> - import { onMount } from 'svelte'; - import Message from '$lib/Loading/Message.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import { parseScheduleHtml } from '$lib/Data/hololive'; - import proxy from '$lib/Utility/proxy'; - import locale from '$stores/locale'; - import root from '$lib/Utility/root'; - import identity from '$stores/identity'; - import Lives from '$lib/Hololive/Lives.svelte'; - import { typeSchedule } from '$lib/Hololive/hololive'; +import { onMount } from "svelte"; +import Message from "$lib/Loading/Message.svelte"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import { parseScheduleHtml } from "$lib/Data/hololive"; +import proxy from "$lib/Utility/proxy"; +import locale from "$stores/locale"; +import root from "$lib/Utility/root"; +import identity from "$stores/identity"; +import Lives from "$lib/Hololive/Lives.svelte"; +import { typeSchedule } from "$lib/Hololive/hololive"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; - let schedulePromise: Promise<Response>; - let pinnedStreams: string[] = []; +let schedulePromise: Promise<Response>; +let pinnedStreams: string[] = []; - onMount(() => getPinnedStreams()); +onMount(() => getPinnedStreams()); - const getPinnedStreams = () => { - let streams: string[] = []; +const getPinnedStreams = () => { + let streams: string[] = []; - const setSchedule = () => { - pinnedStreams = streams; - schedulePromise = fetch(proxy('https://schedule.hololive.tv'), { - headers: { - Cookie: 'timezone=Asia/Tokyo' - } - }); - }; + const setSchedule = () => { + pinnedStreams = streams; + schedulePromise = fetch(proxy("https://schedule.hololive.tv"), { + headers: { + Cookie: "timezone=Asia/Tokyo", + }, + }); + }; - if ($identity.id !== -2) { - fetch(root(`/api/preferences?id=${$identity.id}`)).then((response) => { - if (response.ok) - response.json().then((data) => { - if (data && data.pinned_hololive_streams) streams = data.pinned_hololive_streams; + if ($identity.id !== -2) { + fetch(root(`/api/preferences?id=${$identity.id}`)).then((response) => { + if (response.ok) + response.json().then((data) => { + if (data && data.pinned_hololive_streams) + streams = data.pinned_hololive_streams; - setSchedule(); - }); - }); + setSchedule(); + }); + }); - return; - } + return; + } - setSchedule(); - }; + setSchedule(); +}; </script> <HeadTitle route="hololive Schedule" path="/hololive" /> diff --git a/src/routes/reader/+page.svelte b/src/routes/reader/+page.svelte index 775d3659..b6cce066 100644 --- a/src/routes/reader/+page.svelte +++ b/src/routes/reader/+page.svelte @@ -1,14 +1,19 @@ <script> - import Notice from '$lib/Error/Notice.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import MangaDexChapters from '$lib/Reader/Chapters/MangaDex.svelte'; - import RawkumaChapters from '$lib/Reader/Chapters/Rawkuma.svelte'; - import { decodeResource, fetchResource, identify, Resource } from '$lib/Reader/resource'; - import InputTemplate from '$lib/Tools/InputTemplate.svelte'; +import Notice from "$lib/Error/Notice.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import MangaDexChapters from "$lib/Reader/Chapters/MangaDex.svelte"; +import RawkumaChapters from "$lib/Reader/Chapters/Rawkuma.svelte"; +import { + decodeResource, + fetchResource, + identify, + Resource, +} from "$lib/Reader/resource"; +import InputTemplate from "$lib/Tools/InputTemplate.svelte"; - let submission = ''; +let submission = ""; - $: resourceIdentity = identify(submission); +$: resourceIdentity = identify(submission); </script> <InputTemplate field="Manga URL" bind:submission submitText="Read" preserveCase> diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte index 5ff212ef..a1674c8f 100644 --- a/src/routes/schedule/+page.svelte +++ b/src/routes/schedule/+page.svelte @@ -1,29 +1,36 @@ <script lang="ts"> - import Error from '$lib/Error/RateLimited.svelte'; - import { onMount } from 'svelte'; - import { parseOrDefault } from '$lib/Utility/parameters'; - import { browser } from '$app/environment'; - import type { Media } from '$lib/Data/AniList/media'; - import { scheduleMediaListCollection } from '$lib/Data/AniList/schedule'; - import { season } from '$lib/Media/Anime/season'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - // import Crunchyroll from '$lib/Schedule/Crunchyroll.svelte'; - import '$lib/Schedule/container.css'; - import Days from '$lib/Schedule/Days.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import subsPlease from '$stores/subsPlease'; +import RateLimitedError from "$lib/Error/RateLimited.svelte"; +import { onMount } from "svelte"; +import { parseOrDefault } from "$lib/Utility/parameters"; +import { browser } from "$app/environment"; +import type { Media } from "$lib/Data/AniList/media"; +import { scheduleMediaListCollection } from "$lib/Data/AniList/schedule"; +import { season } from "$lib/Media/Anime/season"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +// import Crunchyroll from '$lib/Schedule/Crunchyroll.svelte'; +import "$lib/Schedule/container.css"; +import Days from "$lib/Schedule/Days.svelte"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import subsPlease from "$stores/subsPlease"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; - let scheduledMediaPromise: Promise<Partial<Media[]>>; - const urlParameters = browser ? new URLSearchParams(window.location.search) : null; - // let crunchyrollExpanded = false; - let forceListMode = parseOrDefault(urlParameters, 'list', false); +let scheduledMediaPromise: Promise<Partial<Media[]>>; +const urlParameters = browser + ? new URLSearchParams(window.location.search) + : null; +// let crunchyrollExpanded = false; +let forceListMode = parseOrDefault(urlParameters, "list", false); - onMount(async () => { - scheduledMediaPromise = scheduleMediaListCollection(new Date().getFullYear(), season(), true); - }); +onMount(async () => { + scheduledMediaPromise = scheduleMediaListCollection( + new Date().getFullYear(), + season(), + true, + ); +}); </script> <HeadTitle route="Schedule" path="/schedule" /> @@ -74,7 +81,7 @@ <Skeleton grid={true} count={7} height="15em" width="49.5%" /> {/if} {:catch} - <Error type="Media" loginSessionError={false} card list={false} /> + <RateLimitedError type="Media" loginSessionError={false} card list={false} /> {/await} {/if} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index a6223b5f..33c7b096 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,31 +1,31 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - /* eslint svelte/no-at-html-tags: "off" */ +import Spacer from "$lib/Layout/Spacer.svelte"; - import Attributions from '$lib/Settings/Categories/Attributions.svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import Display from '$lib/Settings/Categories/Display.svelte'; - import Calculation from '$lib/Settings/Categories/Calculation.svelte'; - import Debug from '$lib/Settings/Categories/Debug.svelte'; - import Cache from '$lib/Settings/Categories/Cache.svelte'; - import Category from '$lib/Settings/Category.svelte'; - import tooltip from '$lib/Tooltip/tooltip'; - import locale from '$stores/locale.js'; - import settings from '$stores/settings'; - import LogInRestricted from '$lib/Error/LogInRestricted.svelte'; - import SettingSync from '$lib/Settings/Categories/SettingSync.svelte'; - import RssFeeds from '$lib/Settings/Categories/RSSFeeds.svelte'; +import Attributions from "$lib/Settings/Categories/Attributions.svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import Display from "$lib/Settings/Categories/Display.svelte"; +import Calculation from "$lib/Settings/Categories/Calculation.svelte"; +import Debug from "$lib/Settings/Categories/Debug.svelte"; +import Cache from "$lib/Settings/Categories/Cache.svelte"; +import Category from "$lib/Settings/Category.svelte"; +import tooltip from "$lib/Tooltip/tooltip"; +import locale from "$stores/locale.js"; +import settings from "$stores/settings"; +import LogInRestricted from "$lib/Error/LogInRestricted.svelte"; +import SettingSync from "$lib/Settings/Categories/SettingSync.svelte"; +import RssFeeds from "$lib/Settings/Categories/RSSFeeds.svelte"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; - // const pruneUnresolved = async () => { - // const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray(); - // const ids = unresolved.map((m) => m.id); +// const pruneUnresolved = async () => { +// const unresolved = await chapterDatabase.chapters.where('chapters').equals(-1).toArray(); +// const ids = unresolved.map((m) => m.id); - // manga.set(''); - // anime.set(''); - // await chapterDatabase.chapters.bulkDelete(ids); - // }; +// manga.set(''); +// anime.set(''); +// await chapterDatabase.chapters.bulkDelete(ids); +// }; </script> <HeadTitle route="Settings" path="/settings" /> diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index 56ef8a10..572f4fc5 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -1,11 +1,11 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import Picker from '$lib/Tools/Picker.svelte'; - import { tools } from '$lib/Tools/tools.js'; - import root from '$lib/Utility/root'; +import Spacer from "$lib/Layout/Spacer.svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import Picker from "$lib/Tools/Picker.svelte"; +import { tools } from "$lib/Tools/tools.js"; +import root from "$lib/Utility/root"; - let tool = 'default'; +let tool = "default"; </script> <Picker {tool} /> diff --git a/src/routes/tools/[tool]/+page.server.ts b/src/routes/tools/[tool]/+page.server.ts index 11ba6d15..c4941121 100644 --- a/src/routes/tools/[tool]/+page.server.ts +++ b/src/routes/tools/[tool]/+page.server.ts @@ -1,5 +1,5 @@ export const load = ({ params }) => { - return { - tool: params.tool - }; + return { + tool: params.tool, + }; }; diff --git a/src/routes/tools/[tool]/+page.svelte b/src/routes/tools/[tool]/+page.svelte index 68eb0c07..ff764add 100644 --- a/src/routes/tools/[tool]/+page.svelte +++ b/src/routes/tools/[tool]/+page.svelte @@ -1,37 +1,38 @@ <script lang="ts"> - import Hayai from './../../../lib/Tools/Hayai.svelte'; - import UmaMusumeBirthdays from './../../../lib/Tools/UmaMusumeBirthdays.svelte'; - import ActivityHistory from '$lib/Tools/ActivityHistory/Tool.svelte'; - import Wrapped from '$lib/Tools/Wrapped/Tool.svelte'; - import EpisodeDiscussionCollector from '$lib/Tools/EpisodeDiscussionCollector.svelte'; - import CharacterBirthdays from '$lib/Tools/Birthdays.svelte'; - import SequelSpy from '$lib/Tools/SequelSpy/Tool.svelte'; - import { closest } from '$lib/Error/path'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import RandomFollower from '$lib/Tools/RandomFollower.svelte'; - // import DumpProfile from '$lib/Tools/DumpProfile.svelte'; - import { tools } from '$lib/Tools/tools.js'; - import { onMount } from 'svelte'; - import { goto } from '$app/navigation'; - import Picker from '$lib/Tools/Picker.svelte'; - import Likes from '$lib/Tools/Likes.svelte'; - import root from '$lib/Utility/root.js'; - import Popup from '$lib/Layout/Popup.svelte'; - import SequelCatcher from '$lib/Tools/SequelCatcher/Tool.svelte'; - import Tracker from '$lib/Tools/Tracker/Tool.svelte'; - import BirthdaysTemplate from '$lib/Tools/BirthdaysTemplate.svelte'; +import Hayai from "./../../../lib/Tools/Hayai.svelte"; +import UmaMusumeBirthdays from "./../../../lib/Tools/UmaMusumeBirthdays.svelte"; +import ActivityHistory from "$lib/Tools/ActivityHistory/Tool.svelte"; +import Wrapped from "$lib/Tools/Wrapped/Tool.svelte"; +import EpisodeDiscussionCollector from "$lib/Tools/EpisodeDiscussionCollector.svelte"; +import CharacterBirthdays from "$lib/Tools/Birthdays.svelte"; +import SequelSpy from "$lib/Tools/SequelSpy/Tool.svelte"; +import { closest } from "$lib/Error/path"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import RandomFollower from "$lib/Tools/RandomFollower.svelte"; +// import DumpProfile from '$lib/Tools/DumpProfile.svelte'; +import { tools } from "$lib/Tools/tools.js"; +import { onMount } from "svelte"; +import { goto } from "$app/navigation"; +import Picker from "$lib/Tools/Picker.svelte"; +import Likes from "$lib/Tools/Likes.svelte"; +import root from "$lib/Utility/root.js"; +import Popup from "$lib/Layout/Popup.svelte"; +import SequelCatcher from "$lib/Tools/SequelCatcher/Tool.svelte"; +import Tracker from "$lib/Tools/Tracker/Tool.svelte"; +import BirthdaysTemplate from "$lib/Tools/BirthdaysTemplate.svelte"; +import type { PageData } from "./$types"; - export let data; +export let data: PageData; - let tool = data.tool ?? 'default'; +let tool = data.tool ?? "default"; - onMount(() => { - if (tool === 'default') goto(root('/tools')); - }); +onMount(() => { + if (tool === "default") goto(root("/tools")); +}); - $: suggestion = closest(tool, Object.keys(tools)); +$: suggestion = closest(tool, Object.keys(tools)); - $: if (tool == 'girls') goto(root('/girls')); +$: if (tool === "girls") goto(root("/girls")); </script> <Picker bind:tool /> diff --git a/src/routes/updates/+page.svelte b/src/routes/updates/+page.svelte index 22fcd3a7..7dc628ca 100644 --- a/src/routes/updates/+page.svelte +++ b/src/routes/updates/+page.svelte @@ -1,59 +1,67 @@ <script lang="ts"> - /* eslint svelte/no-at-html-tags: "off" */ - - import { browser } from '$app/environment'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import { createHeightObserver } from '$lib/Utility/html'; - import root from '$lib/Utility/root'; - import { onDestroy, onMount } from 'svelte'; - - let feed: { items: { title: string; link: string; content: string }[] } | null | undefined = - undefined; - let novelFeed: - | { - data: { - items: { srcurl: string; postfix?: string; chapter: number; series: { name: string } }[]; - }; - } - | undefined = undefined; - let startTime: number; - let mangaEndTime: number; - let novelEndTime: number; - let directLink = browser ? new URLSearchParams(window.location.search).has('d') : false; - let heightObserver: ReturnType<typeof setInterval>; - - onMount(async () => { - heightObserver = setInterval(() => createHeightObserver(false), 0); - - startTime = performance.now(); - novelFeed = await (await fetch(root('/api/updates/all-novels'))).json(); - novelEndTime = performance.now() - startTime; - startTime = performance.now(); - feed = await (await fetch(root('/api/updates/manga'))).json(); - mangaEndTime = performance.now() - startTime; - }); - - onDestroy(() => clearInterval(heightObserver)); - - const reformatChapter = (title: string) => - title - .replace(/\[.*?\]\s/, '') - .replace(/c\.Oneshot/, 'Oneshot') - .replace(/c\.(\d+-\d+)/, 'Ch. $1') - .replace(/v\.(\d+)\s/, 'Vol. $1 ') - .replace(/c\.(\d+)/, 'Ch. $1'); - - const clipTitle = (title: string) => - title - .replace(/(Vol\. \d+ )?Ch\. \d+(-\d+(\.\d+)?)?$/, '') - .replace(/\? ~.*$/, '') - .trim(); - - // const italicTitle = (title: string) => - // title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '<i>$1</i> $2'); - - const chapterTitle = (title: string) => title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '$2'); +import { browser } from "$app/environment"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import { createHeightObserver } from "$lib/Utility/html"; +import root from "$lib/Utility/root"; +import { onDestroy, onMount } from "svelte"; + +let feed: + | { items: { title: string; link: string; content: string }[] } + | null + | undefined = undefined; +let novelFeed: + | { + data: { + items: { + srcurl: string; + postfix?: string; + chapter: number; + series: { name: string }; + }[]; + }; + } + | undefined = undefined; +let startTime: number; +let mangaEndTime: number; +let novelEndTime: number; +let directLink = browser + ? new URLSearchParams(window.location.search).has("d") + : false; +let removeHeightObserver: (() => void) | undefined; + +onMount(async () => { + removeHeightObserver = createHeightObserver(false); + + startTime = performance.now(); + novelFeed = await (await fetch(root("/api/updates/all-novels"))).json(); + novelEndTime = performance.now() - startTime; + startTime = performance.now(); + feed = await (await fetch(root("/api/updates/manga"))).json(); + mangaEndTime = performance.now() - startTime; +}); + +onDestroy(() => removeHeightObserver?.()); + +const reformatChapter = (title: string) => + title + .replace(/\[.*?\]\s/, "") + .replace(/c\.Oneshot/, "Oneshot") + .replace(/c\.(\d+-\d+)/, "Ch. $1") + .replace(/v\.(\d+)\s/, "Vol. $1 ") + .replace(/c\.(\d+)/, "Ch. $1"); + +const clipTitle = (title: string) => + title + .replace(/(Vol\. \d+ )?Ch\. \d+(-\d+(\.\d+)?)?$/, "") + .replace(/\? ~.*$/, "") + .trim(); + +// const italicTitle = (title: string) => +// title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, '<i>$1</i> $2'); + +const chapterTitle = (title: string) => + title.replace(/^(.*?) (Vol\.|Ch\.|\bOneshot\b)/, "$2"); </script> <HeadTitle route="Updates" path="/updates" /> diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte index eab089c6..20a8d390 100644 --- a/src/routes/user/+page.svelte +++ b/src/routes/user/+page.svelte @@ -1,31 +1,32 @@ <script lang="ts"> - import { browser } from '$app/environment'; - import { goto } from '$app/navigation'; - import type { UserIdentity } from '$lib/Data/AniList/identity'; - import { onMount } from 'svelte'; - import { env } from '$env/dynamic/public'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import root from '$lib/Utility/root'; - import { page } from '$app/stores'; - import localforage from 'localforage'; +import { browser } from "$app/environment"; +import { goto } from "$app/navigation"; +import type { UserIdentity } from "$lib/Data/AniList/identity"; +import { onMount } from "svelte"; +import { env } from "$env/dynamic/public"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import root from "$lib/Utility/root"; +import { page } from "$app/stores"; +import localforage from "localforage"; - onMount(async () => { - if (browser) { - const user = ((await localforage.getItem('identity')) as UserIdentity).name; +onMount(async () => { + if (browser) { + const identity = await localforage.getItem<UserIdentity>("identity"); + const user = identity?.name; - if (user) { - if (browser && $page.url.searchParams.get('badges') !== null) { - goto(root(`/user/${user}/badges`)); - } else { - goto(root(`/user/${user}`)); - } - } else { - goto( - `https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code` - ); - } - } - }); + if (user) { + if (browser && $page.url.searchParams.get("badges") !== null) { + goto(root(`/user/${user}/badges`)); + } else { + goto(root(`/user/${user}`)); + } + } else { + goto( + `https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`, + ); + } + } +}); </script> <HeadTitle route="Profile" path="/user" /> diff --git a/src/routes/user/[user]/+page.gql b/src/routes/user/[user]/+page.gql index 491290aa..fd31248b 100644 --- a/src/routes/user/[user]/+page.gql +++ b/src/routes/user/[user]/+page.gql @@ -1,18 +1,18 @@ query Profile($id: Int!) { - User(id: $id) { - id - badgesCount + User(id: $id) { + id + badgesCount - preferences { - created_at - updated_at - user_id - pinned_hololive_streams - hide_missing_badges - biography - badge_wall_css - hide_awc_badges - pinned_badge_wall_categories - } - } + preferences { + created_at + updated_at + user_id + pinned_hololive_streams + hide_missing_badges + biography + badge_wall_css + hide_awc_badges + pinned_badge_wall_categories + } + } } diff --git a/src/routes/user/[user]/+page.svelte b/src/routes/user/[user]/+page.svelte index 3c581c38..fd1e2d7e 100644 --- a/src/routes/user/[user]/+page.svelte +++ b/src/routes/user/[user]/+page.svelte @@ -1,36 +1,49 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import settings from '$stores/settings'; - import ParallaxImage from '../../../lib/Image/ParallaxImage.svelte'; - import { typeSchedule, type ParseResult } from '$lib/Hololive/hololive'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import { estimatedDayReading } from '$lib/Media/Manga/time'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import root from '$lib/Utility/root'; - import locale from '$stores/locale'; - import { onMount } from 'svelte'; - import authorisedUsers from '$lib/Data/Static/authorised.json'; - import tooltip from '$lib/Tooltip/tooltip'; - import AnimeRateLimited from '$lib/Error/AnimeRateLimited.svelte'; - import identity from '$stores/identity'; - import SettingHint from '$lib/Settings/SettingHint.svelte'; - import proxy from '$lib/Utility/proxy'; - import { parseScheduleHtml } from '$lib/Data/hololive'; - import type { Preferences } from '../../../graphql/$types'; - import SvelteMarkdown from '@humanspeak/svelte-markdown'; - import MarkdownLink from '$lib/MarkdownLink.svelte'; - import LinkedTooltip from '$lib/Tooltip/LinkedTooltip.svelte'; - import { graphql } from '$houdini'; - - export let data; - - $: ({ Profile } = data); - $: preferences = $Profile.fetching - ? undefined - : ($Profile.data?.User?.preferences as Preferences | undefined); - - const setCategoriesQuery = graphql(` +import Spacer from "$lib/Layout/Spacer.svelte"; +import settings from "$stores/settings"; +import ParallaxImage from "../../../lib/Image/ParallaxImage.svelte"; +import { typeSchedule, type ParseResult } from "$lib/Hololive/hololive"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import { estimatedDayReading } from "$lib/Media/Manga/time"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import root from "$lib/Utility/root"; +import locale from "$stores/locale"; +import { onMount } from "svelte"; +import authorisedUsers from "$lib/Data/Static/authorised.json"; +import tooltip from "$lib/Tooltip/tooltip"; +import AnimeRateLimited from "$lib/Error/AnimeRateLimited.svelte"; +import identity from "$stores/identity"; +import SettingHint from "$lib/Settings/SettingHint.svelte"; +import proxy from "$lib/Utility/proxy"; +import { parseScheduleHtml } from "$lib/Data/hololive"; +import type { Preferences } from "../../../graphql/$types"; +import SvelteMarkdown from "@humanspeak/svelte-markdown"; +import MarkdownLink from "$lib/MarkdownLink.svelte"; +import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte"; +import { graphql } from "$houdini"; +import type { PageData } from "./$types"; + +export let data: PageData; + +$: ({ Profile } = data); +$: preferences = $Profile.fetching + ? undefined + : ($Profile.data?.User?.preferences as Preferences | undefined); +$: isOwner = Boolean(userData && userData.id === $identity.id); +$: ownerPreferences = preferences ?? { + created_at: "", + updated_at: "", + user_id: userData?.id ?? 0, + pinned_hololive_streams: [], + hide_missing_badges: false, + biography: null, + badge_wall_css: "", + hide_awc_badges: false, + pinned_badge_wall_categories: [], +}; + +const setCategoriesQuery = graphql(` mutation SetCategories($categories: [String!]!) { setPinnedBadgeWallCategories(categories: $categories) { id @@ -42,7 +55,7 @@ } `); - const toggleCategoryQuery = graphql(` +const toggleCategoryQuery = graphql(` mutation ToggleCategory($category: String!) { togglePinnedBadgeWallCategory(category: $category) { id @@ -54,7 +67,7 @@ } `); - const toggleHideMissingBadgesQuery = graphql(` +const toggleHideMissingBadgesQuery = graphql(` mutation ToggleHideMissingBadges { toggleHideMissingBadges { id @@ -66,7 +79,7 @@ } `); - const toggleHideAWCBadgesQuery = graphql(` +const toggleHideAWCBadgesQuery = graphql(` mutation ToggleHideAWCBadges { toggleHideAWCBadges { id @@ -78,7 +91,7 @@ } `); - const setBiographyQuery = graphql(` +const setBiographyQuery = graphql(` mutation SetBiography($biography: String!) { setBiography(biography: $biography) { id @@ -90,7 +103,7 @@ } `); - const setBadgeWallCSSQuery = graphql(` +const setBadgeWallCSSQuery = graphql(` mutation SetBadgeWallCSS($css: String!) { setBadgeWallCSS(css: $css) { id @@ -102,121 +115,126 @@ } `); - $: userData = data.userData; - - let error = false; - let schedule: ParseResult | undefined = undefined; - let draggedCategory: string | null = null; - let draggedOverCategory: string | null = null; - - $: displayBadges = (username: string, badges: number | string) => - $locale({ - values: { - badges: badges, - username - } - }).user.profile.badges; - - const handleDragStart = ( - event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, - category: string | null - ) => { - draggedCategory = category; - - if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; - }; - - const handleDragOver = (event: DragEvent) => { - event.preventDefault(); - - if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; - }; - - const handleDragEnter = ( - event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, - category: string | null - ) => { - event.preventDefault(); - - if (draggedCategory !== category && preferences && draggedCategory) { - draggedOverCategory = category; - - const categories = preferences.pinned_badge_wall_categories; - const draggedIndex = categories.indexOf(draggedCategory); - const targetIndex = categories.indexOf(category || ''); - - categories.splice(draggedIndex, 1); - categories.splice(targetIndex, 0, draggedCategory); - - preferences.pinned_badge_wall_categories = categories; - } - }; - - const handleDragLeave = ( - event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, - category: string - ) => { - event.preventDefault(); - - if (draggedOverCategory === category && preferences && draggedCategory) { - draggedOverCategory = null; - - const categories = preferences.pinned_badge_wall_categories; - const draggedIndex = categories.indexOf(draggedCategory); - - categories.splice(draggedIndex, 1); - categories.splice(categories.indexOf(category) + 1, 0, draggedCategory); - - preferences.pinned_badge_wall_categories = categories; - } - }; - - const handleDrop = (event: { preventDefault: () => void }) => { - event.preventDefault(); - - if (userData && preferences) - setCategoriesQuery - .mutate({ - categories: preferences.pinned_badge_wall_categories - }) - .then(); - - draggedCategory = null; - draggedOverCategory = null; - }; - - onMount(async () => { - schedule = typeSchedule( - parseScheduleHtml( - await ( - await fetch(proxy('https://schedule.hololive.tv'), { - headers: { - Cookie: 'timezone=Asia/Tokyo' - } - }) - ).text() - ) - ); - }); - - const getBadgeWallCSS = () => - (document.getElementById('badgeWallCSS') as HTMLTextAreaElement).value; - - const getBiography = () => - (document.getElementById('biography') as HTMLTextAreaElement).value.slice(0, 3000); - - const toggleCategory = () => { - if (!userData) return; - - const categoryElement = document.getElementById('category') as HTMLInputElement; - const category = categoryElement.value; - - toggleCategoryQuery.mutate({ category }).then(); - - categoryElement.value = ''; - }; - - // 8.5827814569536423841e0 +$: userData = data.userData; + +let error = false; +let schedule: ParseResult | undefined = undefined; +let draggedCategory: string | null = null; +let draggedOverCategory: string | null = null; + +$: displayBadges = (username: string, badges: number | string) => + $locale({ + values: { + badges: badges, + username, + }, + }).user.profile.badges; + +const handleDragStart = ( + event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, + category: string | null, +) => { + draggedCategory = category; + + if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; +}; + +const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + + if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; +}; + +const handleDragEnter = ( + event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, + category: string | null, +) => { + event.preventDefault(); + + if (draggedCategory !== category && draggedCategory) { + draggedOverCategory = category; + + const categories = ownerPreferences.pinned_badge_wall_categories; + const draggedIndex = categories.indexOf(draggedCategory); + const targetIndex = categories.indexOf(category || ""); + + categories.splice(draggedIndex, 1); + categories.splice(targetIndex, 0, draggedCategory); + + ownerPreferences.pinned_badge_wall_categories = categories; + } +}; + +const handleDragLeave = ( + event: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, + category: string, +) => { + event.preventDefault(); + + if (draggedOverCategory === category && draggedCategory) { + draggedOverCategory = null; + + const categories = ownerPreferences.pinned_badge_wall_categories; + const draggedIndex = categories.indexOf(draggedCategory); + + categories.splice(draggedIndex, 1); + categories.splice(categories.indexOf(category) + 1, 0, draggedCategory); + + ownerPreferences.pinned_badge_wall_categories = categories; + } +}; + +const handleDrop = (event: { preventDefault: () => void }) => { + event.preventDefault(); + + if (userData) + setCategoriesQuery + .mutate({ + categories: ownerPreferences.pinned_badge_wall_categories, + }) + .then(); + + draggedCategory = null; + draggedOverCategory = null; +}; + +onMount(async () => { + schedule = typeSchedule( + parseScheduleHtml( + await ( + await fetch(proxy("https://schedule.hololive.tv"), { + headers: { + Cookie: "timezone=Asia/Tokyo", + }, + }) + ).text(), + ), + ); +}); + +const getBadgeWallCSS = () => + (document.getElementById("badgeWallCSS") as HTMLTextAreaElement).value; + +const getBiography = () => + (document.getElementById("biography") as HTMLTextAreaElement).value.slice( + 0, + 3000, + ); + +const toggleCategory = () => { + if (!userData) return; + + const categoryElement = document.getElementById( + "category", + ) as HTMLInputElement; + const category = categoryElement.value; + + toggleCategoryQuery.mutate({ category }).then(); + + categoryElement.value = ""; +}; + +// 8.5827814569536423841e0 </script> <HeadTitle route={`${data.username}'s Profile`} path={`/user/${data.username}`} /> @@ -354,7 +372,7 @@ </div> {/if} - {#if preferences && userData && userData.id === $identity.id} + {#if isOwner} <Spacer /> <details open> @@ -365,7 +383,7 @@ onchange={() => { if (userData) toggleHideMissingBadgesQuery.mutate(null).then(); }} - checked={preferences.hide_missing_badges} + checked={ownerPreferences.hide_missing_badges} /> {$locale().user.preferences.hideMissingBadges.title} <SettingHint lineBreak>{$locale().user.preferences.hideMissingBadges.hint}</SettingHint> @@ -377,7 +395,7 @@ onchange={() => { if (userData) toggleHideAWCBadgesQuery.mutate(null).then(); }} - checked={preferences.hide_awc_badges} + checked={ownerPreferences.hide_awc_badges} /> {$locale().user.preferences.hideAWCBadges.title} @@ -386,7 +404,7 @@ Pinned Categories <div class="pinned-categories"> - {#each preferences.pinned_badge_wall_categories as category} + {#each ownerPreferences.pinned_badge_wall_categories as category} <div class="card card-small pinned-category" draggable="true" @@ -434,7 +452,7 @@ }}>Save</button > <textarea - value={preferences.biography} + value={ownerPreferences.biography} rows="5" cols="100" id="biography" @@ -456,7 +474,7 @@ }}>Save</button > <textarea - value={preferences.badge_wall_css} + value={ownerPreferences.badge_wall_css} rows="10" cols="100" id="badgeWallCSS" diff --git a/src/routes/user/[user]/+page.ts b/src/routes/user/[user]/+page.ts index ca16077f..6ec3c845 100644 --- a/src/routes/user/[user]/+page.ts +++ b/src/routes/user/[user]/+page.ts @@ -1,21 +1,21 @@ -import { load_Profile } from '$houdini'; -import { user } from '$lib/Data/AniList/user'; -import type { LoadEvent } from '@sveltejs/kit'; +import { load_Profile } from "$houdini"; +import { user } from "$lib/Data/AniList/user"; +import type { LoadEvent } from "@sveltejs/kit"; export const load = async (event: LoadEvent) => { - const username = event.params.user as string; - const userData = await user(username, /^\d+$/.test(username)); + const username = event.params.user as string; + const userData = await user(username, /^\d+$/.test(username)); - if (!userData) throw new Error(`User not found: ${username}`); + if (!userData) throw new Error(`User not found: ${username}`); - return { - ...(await load_Profile({ - event, - variables: { - id: userData.id - } - })), - username, - userData - }; + return { + ...(await load_Profile({ + event, + variables: { + id: userData.id, + }, + })), + username, + userData, + }; }; diff --git a/src/routes/user/[user]/badges/+page.gql b/src/routes/user/[user]/badges/+page.gql index 060b38e7..afdd797d 100644 --- a/src/routes/user/[user]/badges/+page.gql +++ b/src/routes/user/[user]/badges/+page.gql @@ -1,31 +1,31 @@ query BadgeWallUser($id: Int!) { - User(id: $id) { - id + User(id: $id) { + id - badges { - post - image - description - id - time - category - hidden - source - designer - shadow_hidden - click_count - } + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } - preferences { - created_at - updated_at - user_id - pinned_hololive_streams - hide_missing_badges - biography - badge_wall_css - hide_awc_badges - pinned_badge_wall_categories - } - } + preferences { + created_at + updated_at + user_id + pinned_hololive_streams + hide_missing_badges + biography + badge_wall_css + hide_awc_badges + pinned_badge_wall_categories + } + } } diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte index 386fe7b1..9ff81118 100644 --- a/src/routes/user/[user]/badges/+page.svelte +++ b/src/routes/user/[user]/badges/+page.svelte @@ -1,59 +1,64 @@ <script lang="ts"> - import Spacer from '$lib/Layout/Spacer.svelte'; - import AWC from './../../../../lib/User/BadgeWall/AWC.svelte'; - import { user, type User } from '$lib/Data/AniList/user'; - import type { Badge } from '../../../../graphql/$types'; - import { onDestroy, onMount } from 'svelte'; - import HeadTitle from '$lib/Home/HeadTitle.svelte'; - import { databaseTimeToDate, dateToInputTime, inputTimeToDatabaseTime } from '$lib/Utility/time'; - import proxy from '$lib/Utility/proxy'; - import locale from '$stores/locale'; - import Skeleton from '$lib/Loading/Skeleton.svelte'; - import Message from '$lib/Loading/Message.svelte'; - import Dropdown from '$lib/Layout/Dropdown.svelte'; - import { activityText } from '$lib/Data/AniList/activity'; - import SettingHint from '$lib/Settings/SettingHint.svelte'; - import Popup from '$lib/Layout/Popup.svelte'; - import { page } from '$app/stores'; - import { browser } from '$app/environment'; - import BadgePreview from '$lib/User/BadgeWall/BadgePreview.svelte'; - import authorisedJson from '$lib/Data/Static/authorised.json'; - import identity from '$stores/identity'; - import '$lib/User/BadgeWall/badges.css'; - import Badges from '$lib/User/BadgeWall/Badges.svelte'; - import type { IndexedBadge } from '$lib/User/BadgeWall/badge'; - import { graphql } from '$houdini'; - import type { Preferences } from '../../../../graphql/user/$types'; - import localforage from 'localforage'; - - export let data; - - $: ({ BadgeWallUser } = data); - $: preferences = $BadgeWallUser.fetching - ? undefined - : ($BadgeWallUser.data?.User?.preferences as Preferences | undefined); - - $: if (browser && preferences && preferences.badge_wall_css) { - const sanitise = (css: string) => - css - .replace(/\/\*[\s\S]*?\*\//g, '') - .replace(/<\/?[^>]+(>|$)/g, '') - .replace( - /(expression|javascript|vbscript|onerror|onload|onclick|onmouseover|onmouseout|onmouseup|onmousedown|onkeydown|onkeyup|onkeypress|onblur|onfocus|onsubmit|onreset|onselect|onchange|ondblclick):/gi, - '' - ) - .replace(/(behaviour|behavior|moz-binding|content):/gi, '') - .replace(/\s+/g, ' ') - .trim(); - const style = document.createElement('style'); - - style.dataset.badgeWall = 'true'; - style.innerHTML = sanitise(preferences.badge_wall_css); - - document.head.appendChild(style); - } - - const updateBadgeQuery = graphql(` +import Spacer from "$lib/Layout/Spacer.svelte"; +import AWC from "./../../../../lib/User/BadgeWall/AWC.svelte"; +import { user, type User } from "$lib/Data/AniList/user"; +import type { Badge } from "../../../../graphql/$types"; +import { onDestroy, onMount } from "svelte"; +import HeadTitle from "$lib/Home/HeadTitle.svelte"; +import { + databaseTimeToDate, + dateToInputTime, + inputTimeToDatabaseTime, +} from "$lib/Utility/time"; +import proxy from "$lib/Utility/proxy"; +import locale from "$stores/locale"; +import Skeleton from "$lib/Loading/Skeleton.svelte"; +import Message from "$lib/Loading/Message.svelte"; +import Dropdown from "$lib/Layout/Dropdown.svelte"; +import { activityText } from "$lib/Data/AniList/activity"; +import SettingHint from "$lib/Settings/SettingHint.svelte"; +import Popup from "$lib/Layout/Popup.svelte"; +import { page } from "$app/stores"; +import { browser } from "$app/environment"; +import BadgePreview from "$lib/User/BadgeWall/BadgePreview.svelte"; +import authorisedJson from "$lib/Data/Static/authorised.json"; +import identity from "$stores/identity"; +import "$lib/User/BadgeWall/badges.css"; +import Badges from "$lib/User/BadgeWall/Badges.svelte"; +import type { IndexedBadge } from "$lib/User/BadgeWall/badge"; +import { graphql } from "$houdini"; +import type { Preferences } from "../../../../graphql/$types"; +import localforage from "localforage"; +import type { PageData } from "./$types"; + +export let data: PageData; + +$: ({ BadgeWallUser } = data); +$: preferences = $BadgeWallUser.fetching + ? undefined + : ($BadgeWallUser.data?.User?.preferences as Preferences | undefined); + +$: if (browser && preferences && preferences.badge_wall_css) { + const sanitise = (css: string) => + css + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/<\/?[^>]+(>|$)/g, "") + .replace( + /(expression|javascript|vbscript|onerror|onload|onclick|onmouseover|onmouseout|onmouseup|onmousedown|onkeydown|onkeyup|onkeypress|onblur|onfocus|onsubmit|onreset|onselect|onchange|ondblclick):/gi, + "", + ) + .replace(/(behaviour|behavior|moz-binding|content):/gi, "") + .replace(/\s+/g, " ") + .trim(); + const style = document.createElement("style"); + + style.dataset.badgeWall = "true"; + style.innerHTML = sanitise(preferences.badge_wall_css); + + document.head.appendChild(style); +} + +const updateBadgeQuery = graphql(` mutation UpdateBadge( $id: Int $post: String @@ -95,7 +100,7 @@ } `); - const pruneBadgesQuery = graphql(` +const pruneBadgesQuery = graphql(` mutation PruneUserBadges { pruneUserBadges { id @@ -117,7 +122,7 @@ } `); - const hideCategoryQuery = graphql(` +const hideCategoryQuery = graphql(` mutation HideCategory($category: String) { hideBadge(category: $category) { id @@ -139,7 +144,7 @@ } `); - const deleteBadgeQuery = graphql(` +const deleteBadgeQuery = graphql(` mutation DeleteBadge($id: Int!) { deleteBadge(id: $id) { id @@ -161,7 +166,7 @@ } `); - const shadowHideBadgeQuery = graphql(` +const shadowHideBadgeQuery = graphql(` mutation ShadowHideBadge($id: Int!, $state: Boolean) { shadowHideBadge(id: $id, state: $state) { id @@ -173,359 +178,387 @@ } `); - interface ImportImage { - link?: string; - image: string; - } - - let editMode = false; - let importMode = false; - let error: null | string; - let awcPromise: Promise<Response>; - let confirmDelete = 0; - let confirmPrune = 0; - let selectedBadge: IndexedBadge | undefined = undefined; - let loadError: string | null = null; - const isId = /^\d+$/.test(data.username); - let importImages: ImportImage[] | undefined = undefined; - let importLinks = false; - let importCategory = ''; - let importReplies = false; - let badger: Partial<User> | null; - let migrateMode = false; - let hideMode = false; - const authorised = authorisedJson.includes($identity.id); - let noticeDismissed = false; - - $: categoryFilter = new URLSearchParams($page.url.searchParams).get('category'); - $: loadQueryParameter = new URLSearchParams($page.url.searchParams).get('load'); - - type GroupedBadges = { [key: string]: IndexedBadge[] }; - - const setShadowHide = () => { - if (!badger) { - loadError = 'Something went wrong. Try refreshing.'; - - return; - } - - shadowHideBadgeQuery.mutate({ - id: badger.id as number - }); - }; - - onMount(async () => { - if (browser && (await localforage.getItem('badgeWallNoticeDismissed'))) noticeDismissed = true; - - badger = isId - ? { - id: parseInt(data.username), - name: 'User' - } - : await user(data.username); - - if (!badger) { - loadError = "Couldn't find this user."; - - return; - } - - awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`)); - }); - - onDestroy(() => { - if (browser) - Array.from(document.head.querySelectorAll('style')).forEach((style) => { - if (style.dataset.badgeWall) style.remove(); - }); - }); - - const submitBadge = () => { - const imageURL = document.querySelector('input[name="image_url"]') as HTMLInputElement; - const activityURL = document.querySelector('input[name="activity_url"]') as HTMLInputElement; - const description = document.querySelector('input[name="description"]') as HTMLInputElement; - const time = document.querySelector('input[type="datetime-local"]') as HTMLInputElement; - const category = document.querySelector('input[name="category"]') as HTMLInputElement; - const hidden = document.querySelector('input[name="hidden"]') as HTMLInputElement; - const source = document.querySelector('input[name="source"]') as HTMLInputElement; - const designer = document.querySelector('input[name="designer"]') as HTMLInputElement; - - if (!imageURL.value) { - error = 'Image URL cannot be empty.'; - - return; - } - - if ( - !imageURL.value.startsWith('http') || - (activityURL.value.length > 0 && !activityURL.value.startsWith('http')) - ) { - error = 'URLs must start with http or https.'; - - return; - } - - updateBadgeQuery - .mutate({ - id: selectedBadge?.id, - image: imageURL.value, - post: activityURL.value || '#', - description: description.value, - category: category.value, - time: time.value ? inputTimeToDatabaseTime(new Date(time.value)) : undefined, - hidden: hidden.value === 'Hidden', - source: source.value, - designer: designer.value - }) - .then(() => { - error = null; - imageURL.value = ''; - activityURL.value = ''; - description.value = ''; - category.value = ''; - hidden.value = 'Shown'; - selectedBadge = undefined; - source.value = ''; - designer.value = ''; - }); - }; - - const removeAllBadges = () => { - if (confirmPrune === 2) { - confirmPrune = 0; - } else if (confirmPrune === 0) { - confirmPrune = 1; - - return; - } else { - confirmPrune = 2; - - return; - } - - selectedBadge = undefined; - - pruneBadgesQuery.mutate(null).then(); - }; - - const removeBadge = (badge: Badge) => { - if (!badge.id) return; - - if (confirmDelete === badge.id * 2) { - confirmDelete = 0; - } else if (confirmDelete / 4 === badge.id) { - confirmDelete = badge.id * 2; - - return; - } else { - confirmDelete = badge.id * 2; - - return; - } - - selectedBadge = undefined; - - deleteBadgeQuery - .mutate({ - id: badge.id - }) - .then(); - }; - - const groupBadges = (badges: IndexedBadge[]) => { - const groupedBadges: GroupedBadges = {}; - - badges.forEach((badge) => { - if (!badge.category) badge.category = 'Uncategorised'; - - if (!groupedBadges[badge.category]) groupedBadges[badge.category] = []; - - groupedBadges[badge.category].push(badge); - }); - - Object.entries(groupedBadges).forEach(([_categoryKey, badges]) => { - badges.forEach((badge, index) => { - badge.index = index; - }); - }); - - return Object.entries(groupedBadges) - .sort((a, b) => a[1].length - b[1].length) - .sort((a, b) => { - const pinnedCategories = - preferences && preferences.pinned_badge_wall_categories - ? preferences.pinned_badge_wall_categories - : ([] as string[]); - const aIndex = pinnedCategories.indexOf(a[0]); - const bIndex = pinnedCategories.indexOf(b[0]); - - if (aIndex === -1 && bIndex === -1) return 0; - if (aIndex === -1) return 1; - if (bIndex === -1) return -1; - - return aIndex - bIndex; - }) - .reduce((set: GroupedBadges, [key, value]) => { - set[key] = value; - - return set; - }, {}); - }; - - const parsePost = async () => { - if (importImages && importImages.length > 0) importImages = undefined; - - const link = (document.querySelector('#import_activity_url') as HTMLInputElement).value; - const type = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$1'); - const id = link.replace(/.*\/(activity|thread)\/(\d+).*/, '$2'); - - if (type !== 'activity') return null; - - let text = await activityText(parseInt(id), importReplies); - - const images: ImportImage[] = []; - - if (importLinks) { - Array.from(new DOMParser().parseFromString(text, 'text/html').querySelectorAll('a')).forEach( - (a) => { - const anchor = a as HTMLAnchorElement; - - if (anchor.querySelector('img')) { - images.push({ - link: anchor.href, - image: (anchor.querySelector('img') as HTMLImageElement).src - }); - } - } - ); - - text = text.replace(/<a.*?>.*?<img.*?>.*?<\/a>/g, ''); - - Array.from( - new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img') - ).forEach((img) => { - const image = img as HTMLImageElement; - - images.push({ - image: image.src - }); - }); - } else { - Array.from( - new DOMParser().parseFromString(text, 'text/html').querySelectorAll('img') - ).forEach((img) => { - const image = img as HTMLImageElement; - - images.push({ - image: image.src - }); - }); - } - - importImages = images; - }; - - const importBadges = () => - fetch( - `/api/badges?import=true - ${importCategory.length > 0 ? `&category=${encodeURIComponent(importCategory)}` : ''} +interface ImportImage { + link?: string; + image: string; +} + +let editMode = false; +let importMode = false; +let error: null | string; +let awcPromise: Promise<Response>; +let confirmDelete = 0; +let confirmPrune = 0; +let selectedBadge: IndexedBadge | undefined = undefined; +let loadError: string | null = null; +const isId = /^\d+$/.test(data.username); +let importImages: ImportImage[] | undefined = undefined; +let importLinks = false; +let importCategory = ""; +let importReplies = false; +let badger: Partial<User> | null; +let migrateMode = false; +let hideMode = false; +const authorised = authorisedJson.includes($identity.id); +let noticeDismissed = false; + +$: categoryFilter = new URLSearchParams($page.url.searchParams).get("category"); +$: loadQueryParameter = new URLSearchParams($page.url.searchParams).get("load"); + +type GroupedBadges = { [key: string]: IndexedBadge[] }; + +const setShadowHide = () => { + if (!badger) { + loadError = "Something went wrong. Try refreshing."; + + return; + } + + shadowHideBadgeQuery.mutate({ + id: badger.id as number, + }); +}; + +onMount(async () => { + if (browser && (await localforage.getItem("badgeWallNoticeDismissed"))) + noticeDismissed = true; + + badger = isId + ? { + id: parseInt(data.username), + name: "User", + } + : await user(data.username); + + if (!badger) { + loadError = "Couldn't find this user."; + + return; + } + + awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`)); +}); + +onDestroy(() => { + if (browser) + Array.from(document.head.querySelectorAll("style")).forEach((style) => { + if (style.dataset.badgeWall) style.remove(); + }); +}); + +const submitBadge = () => { + const imageURL = document.querySelector( + 'input[name="image_url"]', + ) as HTMLInputElement; + const activityURL = document.querySelector( + 'input[name="activity_url"]', + ) as HTMLInputElement; + const description = document.querySelector( + 'input[name="description"]', + ) as HTMLInputElement; + const time = document.querySelector( + 'input[type="datetime-local"]', + ) as HTMLInputElement; + const category = document.querySelector( + 'input[name="category"]', + ) as HTMLInputElement; + const hidden = document.querySelector( + 'input[name="hidden"]', + ) as HTMLInputElement; + const source = document.querySelector( + 'input[name="source"]', + ) as HTMLInputElement; + const designer = document.querySelector( + 'input[name="designer"]', + ) as HTMLInputElement; + + if (!imageURL.value) { + error = "Image URL cannot be empty."; + + return; + } + + if ( + !imageURL.value.startsWith("http") || + (activityURL.value.length > 0 && !activityURL.value.startsWith("http")) + ) { + error = "URLs must start with http or https."; + + return; + } + + updateBadgeQuery + .mutate({ + id: selectedBadge?.id, + image: imageURL.value, + post: activityURL.value || "#", + description: description.value, + category: category.value, + time: time.value + ? inputTimeToDatabaseTime(new Date(time.value)) + : undefined, + hidden: hidden.value === "Hidden", + source: source.value, + designer: designer.value, + }) + .then(() => { + error = null; + imageURL.value = ""; + activityURL.value = ""; + description.value = ""; + category.value = ""; + hidden.value = "Shown"; + selectedBadge = undefined; + source.value = ""; + designer.value = ""; + }); +}; + +const removeAllBadges = () => { + if (confirmPrune === 2) { + confirmPrune = 0; + } else if (confirmPrune === 0) { + confirmPrune = 1; + + return; + } else { + confirmPrune = 2; + + return; + } + + selectedBadge = undefined; + + pruneBadgesQuery.mutate(null).then(); +}; + +const removeBadge = (badge: Badge) => { + if (!badge.id) return; + + if (confirmDelete === badge.id * 2) { + confirmDelete = 0; + } else if (confirmDelete / 4 === badge.id) { + confirmDelete = badge.id * 2; + + return; + } else { + confirmDelete = badge.id * 2; + + return; + } + + selectedBadge = undefined; + + deleteBadgeQuery + .mutate({ + id: badge.id, + }) + .then(); +}; + +const groupBadges = (badges: IndexedBadge[]) => { + const groupedBadges: GroupedBadges = {}; + + badges.forEach((badge) => { + if (!badge.category) badge.category = "Uncategorised"; + + if (!groupedBadges[badge.category]) groupedBadges[badge.category] = []; + + groupedBadges[badge.category].push(badge); + }); + + Object.entries(groupedBadges).forEach(([_categoryKey, badges]) => { + badges.forEach((badge, index) => { + badge.index = index; + }); + }); + + return Object.entries(groupedBadges) + .sort((a, b) => a[1].length - b[1].length) + .sort((a, b) => { + const pinnedCategories = + preferences && preferences.pinned_badge_wall_categories + ? preferences.pinned_badge_wall_categories + : ([] as string[]); + const aIndex = pinnedCategories.indexOf(a[0]); + const bIndex = pinnedCategories.indexOf(b[0]); + + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + + return aIndex - bIndex; + }) + .reduce((set: GroupedBadges, [key, value]) => { + set[key] = value; + + return set; + }, {}); +}; + +const parsePost = async () => { + if (importImages && importImages.length > 0) importImages = undefined; + + const link = ( + document.querySelector("#import_activity_url") as HTMLInputElement + ).value; + const type = link.replace(/.*\/(activity|thread)\/(\d+).*/, "$1"); + const id = link.replace(/.*\/(activity|thread)\/(\d+).*/, "$2"); + + if (type !== "activity") return null; + + let text = await activityText(parseInt(id), importReplies); + + const images: ImportImage[] = []; + + if (importLinks) { + Array.from( + new DOMParser().parseFromString(text, "text/html").querySelectorAll("a"), + ).forEach((a) => { + const anchor = a as HTMLAnchorElement; + + if (anchor.querySelector("img")) { + images.push({ + link: anchor.href, + image: (anchor.querySelector("img") as HTMLImageElement).src, + }); + } + }); + + text = text.replace(/<a.*?>.*?<img.*?>.*?<\/a>/g, ""); + + Array.from( + new DOMParser() + .parseFromString(text, "text/html") + .querySelectorAll("img"), + ).forEach((img) => { + const image = img as HTMLImageElement; + + images.push({ + image: image.src, + }); + }); + } else { + Array.from( + new DOMParser() + .parseFromString(text, "text/html") + .querySelectorAll("img"), + ).forEach((img) => { + const image = img as HTMLImageElement; + + images.push({ + image: image.src, + }); + }); + } + + importImages = images; +}; + +const importBadges = () => + fetch( + `/api/badges?import=true + ${importCategory.length > 0 ? `&category=${encodeURIComponent(importCategory)}` : ""} `, - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify( - importImages?.map((image) => ({ - image: image.image, - post: image.link || '#', - category: importCategory - })) - ) - } - ).then(() => { - importMode = false; - importImages = undefined; - }); - - const migrateCategory = () => { - fetch( - `/api/badges?migrate=true&original=${encodeURIComponent( - (document.querySelector('#migrate_original') as HTMLInputElement).value - )}&new=${encodeURIComponent( - (document.querySelector('#migrate_new') as HTMLInputElement).value - )}`, - { - method: 'PUT' - } - ).then(() => (migrateMode = false)); - }; - - const hideCategory = () => { - hideCategoryQuery - .mutate({ - category: (document.querySelector('#category_hide') as HTMLInputElement).value - }) - .then(() => (hideMode = false)); - }; - - const removeHiddenBadges = (isOwner: boolean, badges: IndexedBadge[]) => - isOwner || authorised ? badges : badges.filter((b) => !b.hidden && !b.shadow_hidden); - - const setAdjacentCursor = (badges: IndexedBadge[], direction: number) => { - const currentCategory = selectedBadge?.category || 'Uncategorised'; - const currentBadge = selectedBadge?.index; - const categoryBadges = groupBadges(badges)[currentCategory]; - - if (!currentCategory || currentBadge === undefined) return; - - let previousBadge = categoryBadges[currentBadge + direction]; - - while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden)) - previousBadge = categoryBadges[previousBadge.index + direction]; - - if (previousBadge) selectedBadge = previousBadge; - }; - - const adjacentBadgeExists = ( - selectedBadge: IndexedBadge | undefined, - badges: IndexedBadge[], - direction: number - ) => { - const currentCategory = selectedBadge?.category || 'Uncategorised'; - const currentBadge = selectedBadge?.index; - const categoryBadges = groupBadges(badges)[currentCategory]; - - if (!currentCategory || currentBadge === undefined || !categoryBadges) return; - - let previousBadge = categoryBadges[currentBadge + direction]; - - while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden)) - previousBadge = categoryBadges[previousBadge.index + direction]; - - return previousBadge; - }; - - const castAsStringArray = (array: unknown[]) => array as string[]; - - const castBadgesToIndexedBadges = (array: unknown[]) => array as IndexedBadge[]; - - const shadowHideBadge = () => { - if (!selectedBadge && !authorised) return; - - if (!badger) { - loadError = 'Something went wrong. Try refreshing.'; - - return; - } - - shadowHideBadgeQuery - .mutate({ - id: badger.id as number, - state: selectedBadge?.shadow_hidden as boolean - }) - .then(); - }; + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + importImages?.map((image) => ({ + image: image.image, + post: image.link || "#", + category: importCategory, + })), + ), + }, + ).then(() => { + importMode = false; + importImages = undefined; + }); + +const migrateCategory = () => { + fetch( + `/api/badges?migrate=true&original=${encodeURIComponent( + (document.querySelector("#migrate_original") as HTMLInputElement).value, + )}&new=${encodeURIComponent( + (document.querySelector("#migrate_new") as HTMLInputElement).value, + )}`, + { + method: "PUT", + }, + ).then(() => (migrateMode = false)); +}; + +const hideCategory = () => { + hideCategoryQuery + .mutate({ + category: (document.querySelector("#category_hide") as HTMLInputElement) + .value, + }) + .then(() => (hideMode = false)); +}; + +const removeHiddenBadges = (isOwner: boolean, badges: IndexedBadge[]) => + isOwner || authorised + ? badges + : badges.filter((b) => !b.hidden && !b.shadow_hidden); + +const setAdjacentCursor = (badges: IndexedBadge[], direction: number) => { + const currentCategory = selectedBadge?.category || "Uncategorised"; + const currentBadge = selectedBadge?.index; + const categoryBadges = groupBadges(badges)[currentCategory]; + + if (!currentCategory || currentBadge === undefined) return; + + let previousBadge = categoryBadges[currentBadge + direction]; + + while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden)) + previousBadge = categoryBadges[previousBadge.index + direction]; + + if (previousBadge) selectedBadge = previousBadge; +}; + +const adjacentBadgeExists = ( + selectedBadge: IndexedBadge | undefined, + badges: IndexedBadge[], + direction: number, +) => { + const currentCategory = selectedBadge?.category || "Uncategorised"; + const currentBadge = selectedBadge?.index; + const categoryBadges = groupBadges(badges)[currentCategory]; + + if (!currentCategory || currentBadge === undefined || !categoryBadges) return; + + let previousBadge = categoryBadges[currentBadge + direction]; + + while (previousBadge && (previousBadge.hidden || previousBadge.shadow_hidden)) + previousBadge = categoryBadges[previousBadge.index + direction]; + + return previousBadge; +}; + +const castAsStringArray = (array: unknown[]) => array as string[]; + +const castBadgesToIndexedBadges = (array: unknown[]) => array as IndexedBadge[]; + +const shadowHideBadge = () => { + if (!selectedBadge && !authorised) return; + + if (!badger) { + loadError = "Something went wrong. Try refreshing."; + + return; + } + + shadowHideBadgeQuery + .mutate({ + id: badger.id as number, + state: selectedBadge?.shadow_hidden as boolean, + }) + .then(); +}; </script> <HeadTitle route={`${data.username}'s Badge Wall`} path={`/user/${data.username}`} /> @@ -580,7 +613,7 @@ be required to use the hide feature to hide these badges from the public, while allowing them to stay visible to you as the account holder. </div> - {:else if !noticeDismissed} + {:else if false && !noticeDismissed} <div class="card"> <b>Notice:</b> AniList has begun purging outbound links which contain AI-generated material, this includes Badge Wall. If you have collected badges with AI-generated diff --git a/src/routes/user/[user]/badges/+page.ts b/src/routes/user/[user]/badges/+page.ts index db70d16c..a14c0a6a 100644 --- a/src/routes/user/[user]/badges/+page.ts +++ b/src/routes/user/[user]/badges/+page.ts @@ -1,22 +1,22 @@ -import { load_BadgeWallUser } from '$houdini'; -import { user } from '$lib/Data/AniList/user'; -import type { LoadEvent } from '@sveltejs/kit'; +import { load_BadgeWallUser } from "$houdini"; +import { user } from "$lib/Data/AniList/user"; +import type { LoadEvent } from "@sveltejs/kit"; export const load = async (event: LoadEvent) => { - const username = event.params.user as string; - const userData = await user(username, /^\d+$/.test(username)); + const username = event.params.user as string; + const userData = await user(username, /^\d+$/.test(username)); - if (!userData) throw new Error(`User not found: ${username}`); + if (!userData) throw new Error(`User not found: ${username}`); - return { - ...(await load_BadgeWallUser({ - event, - variables: { - id: userData.id - } - })), - username, - userData, - event - }; + return { + ...(await load_BadgeWallUser({ + event, + variables: { + id: userData.id, + }, + })), + username, + userData, + event, + }; }; diff --git a/src/routes/welcome/+page.svelte b/src/routes/welcome/+page.svelte index 3e15ab26..b4fd1fe0 100644 --- a/src/routes/welcome/+page.svelte +++ b/src/routes/welcome/+page.svelte @@ -1,7 +1,7 @@ <script> - import Landing from '$lib/Landing.svelte'; - import LandingHero from '$lib/LandingHero.svelte'; - import Spacer from '$lib/Layout/Spacer.svelte'; +import Landing from "$lib/Landing.svelte"; +import LandingHero from "$lib/LandingHero.svelte"; +import Spacer from "$lib/Layout/Spacer.svelte"; </script> <LandingHero /> |