diff options
| author | Fuwn <[email protected]> | 2024-10-09 00:41:20 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-10-09 00:41:43 -0700 |
| commit | 998b63a35256ac985a5a2714dd1ca451af4dfd8a (patch) | |
| tree | 50796121a9d5ab0330fdc5d7e098bda2860d9726 /src/routes | |
| parent | feat(graphql): add badgeCount field (diff) | |
| download | due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.tar.xz due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.zip | |
chore(prettier): use spaces instead of tabs
Diffstat (limited to 'src/routes')
48 files changed, 3283 insertions, 3283 deletions
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte index 9e1bca13..f822d521 100644 --- a/src/routes/+error.svelte +++ b/src/routes/+error.svelte @@ -1,31 +1,31 @@ <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> - <p style="text-align: center;"> - <a href={$page.url.pathname}>{$page.url.pathname}</a> not found - </p> + <p style="text-align: center;"> + <a href={$page.url.pathname}>{$page.url.pathname}</a> not found + </p> - <blockquote style="margin: 0 0 0 1.5rem;"> - Did you mean "<a - href={suggestion} - style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''} - >{suggestion.charAt(0).toUpperCase() + suggestion.slice(1)}</a - >"? - </blockquote> + <blockquote style="margin: 0 0 0 1.5rem;"> + Did you mean "<a + href={suggestion} + style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''} + >{suggestion.charAt(0).toUpperCase() + suggestion.slice(1)}</a + >"? + </blockquote> </Popup> diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index d2fdce1b..1b7bf69a 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,14 +1,14 @@ 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 d9b257e9..6d7fe757 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,329 +1,329 @@ <script lang="ts"> - 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 Notifications from 'svelte-notifications'; - import * as EventNotification from '$lib/Notification/Notification.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'; - - injectSpeedInsights(); - - export let data; - - let isHeaderVisible = true; - let previousScrollPosition = 0; - let notificationInterval: NodeJS.Timeout | 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 (localStorage.getItem('redirect')) { - window.location.href = localStorage.getItem('redirect') ?? '/'; - - localStorage.removeItem('redirect'); - } - - window.addEventListener('scroll', handleScroll); - - if (localStorage.getItem('commit') !== data.commit) { - localStorage.removeItem('identity'); - localStorage.removeItem('anime'); - localStorage.removeItem('manga'); - localStorage.removeItem('lastPruneTimes'); - localStorage.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') - }); - } - }); - }); - - if (!(await userDatabase.users.get($userIdentity.id))) - 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 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 Notifications from 'svelte-notifications'; + import * as EventNotification from '$lib/Notification/Notification.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'; + + injectSpeedInsights(); + + export let data; + + let isHeaderVisible = true; + let previousScrollPosition = 0; + let notificationInterval: NodeJS.Timeout | 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 (localStorage.getItem('redirect')) { + window.location.href = localStorage.getItem('redirect') ?? '/'; + + localStorage.removeItem('redirect'); + } + + window.addEventListener('scroll', handleScroll); + + if (localStorage.getItem('commit') !== data.commit) { + localStorage.removeItem('identity'); + localStorage.removeItem('anime'); + localStorage.removeItem('manga'); + localStorage.removeItem('lastPruneTimes'); + localStorage.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') + }); + } + }); + }); + + if (!(await userDatabase.users.get($userIdentity.id))) + 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); + }); + } </script> <HeadTitle /> {#if !$locales.includes('en') || !$locale().navigation} - <Message /> - - {#if data.url === '/settings'} - <Skeleton grid={true} count={1} height="10vh" /> - <Skeleton grid={true} count={1} height="30vh" /> - <Skeleton grid={true} count={1} height="30vh" /> - {:else} - <Skeleton grid={true} count={1} height="10vh" /> - <Skeleton grid={true} count={1} height="80vh" /> - {/if} + <Message /> + + {#if data.url === '/settings'} + <Skeleton grid={true} count={1} height="10vh" /> + <Skeleton grid={true} count={1} height="30vh" /> + <Skeleton grid={true} count={1} height="30vh" /> + {:else} + <Skeleton grid={true} count={1} height="10vh" /> + <Skeleton grid={true} count={1} height="80vh" /> + {/if} {:else} - <Announcement /> - - <div class="container"> - <div class="card card-centered header" class:header-hidden={!isHeaderVisible}> - <div> - <a href={root('/')} class="header-item">{$locale().navigation.home}</a><a - href={root('/completed')} - class="header-item" - > - {$locale().navigation.completed} - </a> - <Dropdown - items={[ - { name: $locale().navigation.subtitleSchedule, url: root('/schedule') }, - { name: $locale().navigation.hololive, url: root('/hololive') }, - { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') }, - { name: $locale().navigation.newReleases, url: root('/updates') } - ]} - header={false} - > - <span slot="title" class="header-item">{$locale().navigation.schedule}</span> - </Dropdown> - <a href={root('/tools')} class="header-item">{$locale().navigation.tools}</a> - <a href={root('/settings')} class="header-item">{$locale().navigation.settings}</a> - - <span class="header-item opaque separator">•</span> - - {#if data.user} - <Dropdown - items={[ - { name: $locale().navigation.myProfile, url: root(`/user/${$userIdentity.name}`) }, - { - name: $locale().navigation.myBadgeWall, - url: root(`/user/${$userIdentity.name}/badges`) - }, - { - name: $locale().navigation.logOut, - url: '#', - preventDefault: true, - onClick: () => { - localStorage.removeItem('identity'); - localStorage.removeItem('commit'); - - document.cookie = 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - - window.location.href = root('/api/authentication/log-out'); - } - } - ]} - header={false} - > - <span slot="title" class="header-item"> - {$locale().navigation.profile} - </span> - </Dropdown> - {/if} - - {#if data.user === undefined} - <a - class="header-item" - href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`} - on:click={() => { - localStorage.setItem( - 'redirect', - window.location.origin + window.location.pathname + window.location.search - ); - }} - > - {$locale().navigation.logIn} - </a> - {:else if data.user} - <a href={root(`/user/${$userIdentity.name}`)} class="header-item"> - <img class="avatar" src={$userIdentity.avatar} alt="Avatar" /> - </a> - {/if} - </div> - </div> - - <p /> - - <Notifications item={EventNotification} zIndex={5000}> - <Root {data} {way}> - {#if $userIdentity.id !== -1} - <slot /> - {:else if data.url === '/settings'} - <Skeleton grid={true} count={1} height="10vh" /> - <Skeleton grid={true} count={1} height="30vh" /> - <Skeleton grid={true} count={1} height="30vh" /> - {:else} - <Skeleton grid={true} count={1} height="10vh" /> - <Skeleton grid={true} count={1} height="80vh" /> - {/if} - </Root> - </Notifications> - </div> + <Announcement /> + + <div class="container"> + <div class="card card-centered header" class:header-hidden={!isHeaderVisible}> + <div> + <a href={root('/')} class="header-item">{$locale().navigation.home}</a><a + href={root('/completed')} + class="header-item" + > + {$locale().navigation.completed} + </a> + <Dropdown + items={[ + { name: $locale().navigation.subtitleSchedule, url: root('/schedule') }, + { name: $locale().navigation.hololive, url: root('/hololive') }, + { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') }, + { name: $locale().navigation.newReleases, url: root('/updates') } + ]} + header={false} + > + <span slot="title" class="header-item">{$locale().navigation.schedule}</span> + </Dropdown> + <a href={root('/tools')} class="header-item">{$locale().navigation.tools}</a> + <a href={root('/settings')} class="header-item">{$locale().navigation.settings}</a> + + <span class="header-item opaque separator">•</span> + + {#if data.user} + <Dropdown + items={[ + { name: $locale().navigation.myProfile, url: root(`/user/${$userIdentity.name}`) }, + { + name: $locale().navigation.myBadgeWall, + url: root(`/user/${$userIdentity.name}/badges`) + }, + { + name: $locale().navigation.logOut, + url: '#', + preventDefault: true, + onClick: () => { + localStorage.removeItem('identity'); + localStorage.removeItem('commit'); + + document.cookie = 'user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + + window.location.href = root('/api/authentication/log-out'); + } + } + ]} + header={false} + > + <span slot="title" class="header-item"> + {$locale().navigation.profile} + </span> + </Dropdown> + {/if} + + {#if data.user === undefined} + <a + class="header-item" + href={`https://anilist.co/api/v2/oauth/authorize?client_id=${env.PUBLIC_ANILIST_CLIENT_ID}&redirect_uri=${env.PUBLIC_ANILIST_REDIRECT_URI}&response_type=code`} + on:click={() => { + localStorage.setItem( + 'redirect', + window.location.origin + window.location.pathname + window.location.search + ); + }} + > + {$locale().navigation.logIn} + </a> + {:else if data.user} + <a href={root(`/user/${$userIdentity.name}`)} class="header-item"> + <img class="avatar" src={$userIdentity.avatar} alt="Avatar" /> + </a> + {/if} + </div> + </div> + + <p /> + + <Notifications item={EventNotification} zIndex={5000}> + <Root {data} {way}> + {#if $userIdentity.id !== -1} + <slot /> + {:else if data.url === '/settings'} + <Skeleton grid={true} count={1} height="10vh" /> + <Skeleton grid={true} count={1} height="30vh" /> + <Skeleton grid={true} count={1} height="30vh" /> + {:else} + <Skeleton grid={true} count={1} height="10vh" /> + <Skeleton grid={true} count={1} height="80vh" /> + {/if} + </Root> + </Notifications> + </div> {/if} <style lang="scss"> - .header { - font-family: 'DM Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-size: 1.05em; - font-weight: 600; - padding: 0.8rem 0.4rem; - z-index: 4; - position: sticky; - top: 1.25rem; - transition: transform 0.3s ease; - } - - .header-hidden { - transform: translateY(-150%); - } - - :global(html.light-theme) { - filter: invert(0); - } - - :global(html.light-theme img) { - filter: invert(0); - } - - .container { - height: 100%; - display: flex; - flex-direction: column; - } - - :global(a) { - text-decoration: none; - transition: all 0.15s ease-in-out; - } - - :global(a:hover) { - text-decoration: underline; - } - - .header-item { - margin: 0 0.5rem; - color: var(--base06); - white-space: nowrap; - } - - .header-item:hover { - text-decoration: none; - } - - .header-item:active { - outline: none; - } - - .avatar { - width: 2em; - display: inline-block; - vertical-align: middle; - border-radius: 8px; - box-shadow: 0 1.5px 9px var(--base01), 0 0 0 4px var(--base0E), 0 4px 30px var(--base01); - } - - .separator { - color: var(--base04); - } + .header { + font-family: 'DM Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 1.05em; + font-weight: 600; + padding: 0.8rem 0.4rem; + z-index: 4; + position: sticky; + top: 1.25rem; + transition: transform 0.3s ease; + } + + .header-hidden { + transform: translateY(-150%); + } + + :global(html.light-theme) { + filter: invert(0); + } + + :global(html.light-theme img) { + filter: invert(0); + } + + .container { + height: 100%; + display: flex; + flex-direction: column; + } + + :global(a) { + text-decoration: none; + transition: all 0.15s ease-in-out; + } + + :global(a:hover) { + text-decoration: underline; + } + + .header-item { + margin: 0 0.5rem; + color: var(--base06); + white-space: nowrap; + } + + .header-item:hover { + text-decoration: none; + } + + .header-item:active { + outline: none; + } + + .avatar { + width: 2em; + display: inline-block; + vertical-align: middle; + border-radius: 8px; + box-shadow: 0 1.5px 9px var(--base01), 0 0 0 4px var(--base0E), 0 4px 30px var(--base01); + } + + .separator { + color: var(--base04); + } </style> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3f130f96..653c3836 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,25 +1,25 @@ <script lang="ts"> - 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 IndexColumn from '$lib/List/Anime/DueIndexColumn.svelte'; - - export let data; - - let heightObserver: NodeJS.Timeout; - - onMount(() => (heightObserver = setInterval(() => createHeightObserver(), 0))); - - onDestroy(() => clearInterval(heightObserver)); + 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 IndexColumn from '$lib/List/Anime/DueIndexColumn.svelte'; + + export let data; + + let heightObserver: NodeJS.Timeout; + + onMount(() => (heightObserver = setInterval(() => createHeightObserver(), 0))); + + onDestroy(() => clearInterval(heightObserver)); </script> <HeadTitle /> @@ -27,97 +27,97 @@ <LastActivity user={data.user} /> {#if data.user === undefined} - <div class="card">Please log in to view due media.</div> + <div class="card">Please log in to view due media.</div> - <p /> + <p /> - <Landing /> + <Landing /> {:else} - <div - class="grid-container" - style={` + <div + class="grid-container" + style={` grid-template-columns: ${ - [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] - .map(Number) - .reduce((a, b) => a + b) > 1 - ? '1fr 1fr' - : '1fr' - } + [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] + .map(Number) + .reduce((a, b) => a + b) > 1 + ? '1fr 1fr' + : '1fr' + } `} - > - <div class="left-column"> - {#if !$settings.disableUpcomingAnime} - <details open={!$settings.displayUpcomingAnimeCollapsed} class="list list-upcoming"> - {#if $userIdentity.id !== -2} - <UpcomingAnimeList user={data.user} /> - {:else} - <ListTitle title={$locale().lists.upcoming.episodes} /> - - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> - {/if} - - {#if !$settings.disableAnime && !$settings.disableManga} - <IndexColumn user={data.user} userIdentity={$userIdentity} /> - {/if} - </div> - - <div class="right-column"> - {#if !$settings.disableAnime && $settings.disableManga} - <IndexColumn user={data.user} userIdentity={$userIdentity} /> - {/if} - - {#if !$settings.disableManga} - <details open={!$settings.displayMangaCollapsed} 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} - </details> - {/if} - </div> - - {#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga} - <video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop> - <track kind="captions" /> - </video> - {/if} - </div> + > + <div class="left-column"> + {#if !$settings.disableUpcomingAnime} + <details open={!$settings.displayUpcomingAnimeCollapsed} class="list list-upcoming"> + {#if $userIdentity.id !== -2} + <UpcomingAnimeList user={data.user} /> + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + {/if} + + {#if !$settings.disableAnime && !$settings.disableManga} + <IndexColumn user={data.user} userIdentity={$userIdentity} /> + {/if} + </div> + + <div class="right-column"> + {#if !$settings.disableAnime && $settings.disableManga} + <IndexColumn user={data.user} userIdentity={$userIdentity} /> + {/if} + + {#if !$settings.disableManga} + <details open={!$settings.displayMangaCollapsed} 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} + </details> + {/if} + </div> + + {#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga} + <video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop> + <track kind="captions" /> + </video> + {/if} + </div> {/if} <style> - .grid-container { - display: grid; - gap: 1rem; - } - - .left-column { - display: grid; - gap: 1rem; - align-content: start; - } - - .right-column { - align-self: start; - } - - .list { - overflow-y: auto; - break-inside: avoid; - page-break-inside: avoid; - } - - @media (max-width: 800px) { - .grid-container { - grid-template-columns: 1fr !important; - } - } + .grid-container { + display: grid; + gap: 1rem; + } + + .left-column { + display: grid; + gap: 1rem; + align-content: start; + } + + .right-column { + align-self: start; + } + + .list { + overflow-y: auto; + break-inside: avoid; + page-break-inside: avoid; + } + + @media (max-width: 800px) { + .grid-container { + grid-template-columns: 1fr !important; + } + } </style> diff --git a/src/routes/api/animeschedule/oauth/callback/+server.ts b/src/routes/api/animeschedule/oauth/callback/+server.ts index 69a48600..2b96ab81 100644 --- a/src/routes/api/animeschedule/oauth/callback/+server.ts +++ b/src/routes/api/animeschedule/oauth/callback/+server.ts @@ -3,15 +3,15 @@ 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 e3ce347a..305c846f 100644 --- a/src/routes/api/authentication/log-out/+server.ts +++ b/src/routes/api/authentication/log-out/+server.ts @@ -2,14 +2,14 @@ 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 + }); - throw redirect(303, root('/')); + throw redirect(303, root('/')); }; diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts index e7a26320..7c62285d 100644 --- a/src/routes/api/badges/+server.ts +++ b/src/routes/api/badges/+server.ts @@ -100,9 +100,9 @@ export const PUT = async ({ cookies, url, request }) => { allBadges .filter((badge) => badge.category === (url.searchParams.get('category') || '')) .filter((badge) => badge.hidden).length > - allBadges.filter( - (badge) => badge.category === (url.searchParams.get('category') || '') - ).length / + allBadges.filter( + (badge) => badge.category === (url.searchParams.get('category') || '') + ).length / 2 ? false : true diff --git a/src/routes/api/birthdays/primary/+server.ts b/src/routes/api/birthdays/primary/+server.ts index 0b92a11e..1e480283 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'; 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')}`); + 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([]); + if (!section) return Response.json([]); - const ul = section.querySelector('ul.covers.simple'); + const ul = section.querySelector('ul.covers.simple'); - if (!ul) return Response.json([]); + 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'); + 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: '' }; + if (!anchor || !title) return { image: '', title: '' }; - return { - image: anchor.getAttribute('data-bg') - ? `https://cdn.anisearch.com/images/${anchor.getAttribute('data-bg')}` - : null, - name: title.textContent?.trim() - }; - }), - { - headers: { - 'Cache-Control': 'max-age=10800, s-maxage=10800', - 'Access-Control-Allow-Origin': 'https://due.moe' - } - } - ); + return { + image: anchor.getAttribute('data-bg') + ? `https://cdn.anisearch.com/images/${anchor.getAttribute('data-bg')}` + : null, + name: title.textContent?.trim() + }; + }), + { + headers: { + 'Cache-Control': 'max-age=10800, s-maxage=10800', + 'Access-Control-Allow-Origin': 'https://due.moe' + } + } + ); }; diff --git a/src/routes/api/birthdays/secondary/+server.ts b/src/routes/api/birthdays/secondary/+server.ts index ae9178a0..f28e2e71 100644 --- a/src/routes/api/birthdays/secondary/+server.ts +++ b/src/routes/api/birthdays/secondary/+server.ts @@ -1,25 +1,25 @@ 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' - } - } - ) - ).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' + } + } + ) + ).json(), + { + headers: { + 'Cache-Control': 'max-age=10800, s-maxage=10800', + 'Access-Control-Allow-Origin': 'https://due.moe' + } + } + ); }; diff --git a/src/routes/api/configuration/+server.ts b/src/routes/api/configuration/+server.ts index 65fd8d8b..41a70f7b 100644 --- a/src/routes/api/configuration/+server.ts +++ b/src/routes/api/configuration/+server.ts @@ -1,70 +1,70 @@ import { userIdentity } from '$lib/Data/AniList/identity'; import { - deleteUserConfiguration, - getUserConfiguration, - setUserConfiguration + deleteUserConfiguration, + getUserConfiguration, + setUserConfiguration } from '$lib/Database/SB/User/configuration'; 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' - } - }); + Response.json(await getUserConfiguration(Number(url.searchParams.get('id') || 0)), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); export const PUT = async ({ cookies, request }) => { - const userCookie = cookies.get('user'); + const userCookie = cookies.get('user'); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised; - const user = JSON.parse(userCookie); + const user = JSON.parse(userCookie); - 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 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' + } + } + ); }; export const DELETE = async ({ 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 user = JSON.parse(userCookie); - 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( + ( + 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' + } + } + ); }; diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts index 2f7d72d4..22280394 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'; 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 430578a3..9bd4c33a 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'; 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/myanimelist/oauth/callback/+server.ts b/src/routes/api/myanimelist/oauth/callback/+server.ts index d5dce3c1..0bb78a64 100644 --- a/src/routes/api/myanimelist/oauth/callback/+server.ts +++ b/src/routes/api/myanimelist/oauth/callback/+server.ts @@ -3,16 +3,16 @@ 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 3ffa0331..d410dc9d 100644 --- a/src/routes/api/notifications/subscribe/+server.ts +++ b/src/routes/api/notifications/subscribe/+server.ts @@ -4,24 +4,24 @@ import { setUserSubscription } from '$lib/Database/SB/User/notifications'; 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 = 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 (!userId) return unauthorised; + if (!userId) return unauthorised; - await setUserSubscription(userId, await request.json(), fingerprint); + await setUserSubscription(userId, await request.json(), fingerprint); - return new Response(null, { status: 200 }); + 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 92494ce7..ded228f3 100644 --- a/src/routes/api/notifications/unsubscribe/+server.ts +++ b/src/routes/api/notifications/unsubscribe/+server.ts @@ -4,24 +4,24 @@ import { deleteUserSubscription } from '$lib/Database/SB/User/notifications'; 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 = 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 (!userId) return unauthorised; + if (!userId) return unauthorised; - await deleteUserSubscription(userId, fingerprint); + await deleteUserSubscription(userId, fingerprint); - return new Response(null, { status: 200 }); + 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 4510c4a2..986520f9 100644 --- a/src/routes/api/oauth/callback/+server.ts +++ b/src/routes/api/oauth/callback/+server.ts @@ -3,14 +3,14 @@ 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 b2b36db2..66b4209c 100644 --- a/src/routes/api/oauth/refresh/+server.ts +++ b/src/routes/api/oauth/refresh/+server.ts @@ -3,28 +3,28 @@ 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')) throw redirect(303, '/'); - else return Response.json(newUser); + if (url.searchParams.get('redirect')) throw redirect(303, '/'); + else return Response.json(newUser); }; diff --git a/src/routes/api/preferences/+server.ts b/src/routes/api/preferences/+server.ts index 0b3be7e5..0fb91f45 100644 --- a/src/routes/api/preferences/+server.ts +++ b/src/routes/api/preferences/+server.ts @@ -1,89 +1,89 @@ import { userIdentity } from '$lib/Data/AniList/identity'; import { - getUserPreferences, - toggleHideMissingBadges, - setCSS, - setBiography, - toggleHideAWCBadges, - togglePinnedBadgeWallCategory, - setPinnedBadgeWallCategories + getUserPreferences, + toggleHideMissingBadges, + setCSS, + setBiography, + toggleHideAWCBadges, + togglePinnedBadgeWallCategory, + setPinnedBadgeWallCategories } from '$lib/Database/SB/User/preferences'; const unauthorised = new Response('Unauthorised', { status: 401 }); export const GET = async ({ url }) => { - const preferences = await getUserPreferences(Number(url.searchParams.get('id') || 0)); + const preferences = await getUserPreferences(Number(url.searchParams.get('id') || 0)); - return Response.json(preferences ? preferences : {}, { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe' - } - }); + return Response.json(preferences ? preferences : {}, { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); }; export const PUT = async ({ url, cookies, request }) => { - const userCookie = cookies.get('user'); + const userCookie = cookies.get('user'); - if (!userCookie) return unauthorised; + 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; + 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('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('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('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('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('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' - } - }); + 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' - } - }); + return Response.json(await getUserPreferences(Number(url.searchParams.get('id') || 0)), { + headers: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + }); }; diff --git a/src/routes/api/preferences/pin/+server.ts b/src/routes/api/preferences/pin/+server.ts index 73c4d5de..28398cf0 100644 --- a/src/routes/api/preferences/pin/+server.ts +++ b/src/routes/api/preferences/pin/+server.ts @@ -4,29 +4,29 @@ import { toggleHololiveStreamPinning } from '$lib/Database/SB/User/preferences'; const unauthorised = new Response('Unauthorised', { status: 401 }); export const PUT = async ({ cookies, url }) => { - const userCookie = cookies.get('user'); + const userCookie = cookies.get('user'); - if (!userCookie) return unauthorised; + if (!userCookie) return unauthorised; - const user = JSON.parse(userCookie); + 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' - } - } - ); + 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' + } + } + ); }; diff --git a/src/routes/api/subsplease/+server.ts b/src/routes/api/subsplease/+server.ts index 82631047..93e734c2 100644 --- a/src/routes/api/subsplease/+server.ts +++ b/src/routes/api/subsplease/+server.ts @@ -1,16 +1,16 @@ 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' - } - } - ); + 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' + } + } + ); diff --git a/src/routes/api/updates/all-novels/+server.ts b/src/routes/api/updates/all-novels/+server.ts index b5dea142..5dd2aaad 100644 --- a/src/routes/api/updates/all-novels/+server.ts +++ b/src/routes/api/updates/all-novels/+server.ts @@ -1,20 +1,20 @@ 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', + 'Access-Control-Allow-Origin': 'https://due.moe' + }, + body: JSON.stringify({ + mode: 'get-releases' + }) + }) + ).json() + ); }; diff --git a/src/routes/api/updates/manga/+server.ts b/src/routes/api/updates/manga/+server.ts index dcc0df5c..af078c05 100644 --- a/src/routes/api/updates/manga/+server.ts +++ b/src/routes/api/updates/manga/+server.ts @@ -1,18 +1,18 @@ import Parser from 'rss-parser'; 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: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + } + ); }; diff --git a/src/routes/api/updates/novels/+server.ts b/src/routes/api/updates/novels/+server.ts index 676d1bd2..7ca6ca43 100644 --- a/src/routes/api/updates/novels/+server.ts +++ b/src/routes/api/updates/novels/+server.ts @@ -1,18 +1,18 @@ import Parser from 'rss-parser'; 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: { + 'Access-Control-Allow-Origin': 'https://due.moe' + } + } + ); }; diff --git a/src/routes/completed/+page.svelte b/src/routes/completed/+page.svelte index 8a5724fd..d483d7fe 100644 --- a/src/routes/completed/+page.svelte +++ b/src/routes/completed/+page.svelte @@ -1,24 +1,24 @@ <script lang="ts"> - 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 { 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'; - export let data; + export let data; - let heightObserver: NodeJS.Timeout; + let heightObserver: NodeJS.Timeout; - onMount(() => (heightObserver = setInterval(() => createHeightObserver(), 0))); + onMount(() => (heightObserver = setInterval(() => createHeightObserver(), 0))); - onDestroy(() => clearInterval(heightObserver)); + onDestroy(() => clearInterval(heightObserver)); </script> <HeadTitle route="Completed" path="/completed" /> @@ -26,54 +26,54 @@ <LastActivity user={data.user} /> {#if data.user === undefined} - <div class="card">Please log in to view completed media.</div> + <div class="card">Please log in to view completed media.</div> - <p /> + <p /> - <Landing /> + <Landing /> {:else} - <div class="list-container"> - {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableAnime} - <details open={!$settings.displayAnimeCollapsed} class="list"> - {#if $userIdentity.id !== -2} - <WatchingAnimeList user={data.user} /> - {:else} - <ListTitle title={$locale().lists.completed.anime} /> + <div class="list-container"> + {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableAnime} + <details open={!$settings.displayAnimeCollapsed} class="list"> + {#if $userIdentity.id !== -2} + <WatchingAnimeList user={data.user} /> + {:else} + <ListTitle title={$locale().lists.completed.anime} /> - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> - {/if} + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + {/if} - {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableManga} - <details open={!$settings.displayMangaCollapsed} class="list"> - {#if $userIdentity.id !== -2} - <MangaListTemplate - user={data.user} - displayUnresolved={$settings.displayUnresolved} - due={false} - /> - {:else} - <ListTitle title={$locale().lists.completed.mangaAndLightNovels} /> + {#if !$settings.displayFiltersIncludeCompleted || !$settings.disableManga} + <details open={!$settings.displayMangaCollapsed} class="list"> + {#if $userIdentity.id !== -2} + <MangaListTemplate + 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} - </details> - {/if} - </div> + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + {/if} + </div> {/if} <style> - .list-container { - display: flex; - flex-wrap: wrap; - align-items: start; - gap: 1em; - } + .list-container { + display: flex; + flex-wrap: wrap; + align-items: start; + gap: 1em; + } - .list { - overflow-y: auto; - min-width: 300px; - flex: 1 1 300px; - } + .list { + overflow-y: auto; + min-width: 300px; + flex: 1 1 300px; + } </style> diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index f7018ec9..d3270e30 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -1,26 +1,26 @@ <script> - import Event from '$lib/Events/Event.svelte'; - import Message from '$lib/Loading/Message.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`))} - <Message message="Loading events ..." /> + <Message message="Loading events ..." /> {:then eventsResponse} - {#await eventsResponse.json()} - <Message message="Parsing events ..." /> - {:then events} - {#if events} - {#each events as rawEvent, i} - <Event event={rawEvent} avatar /> + {#await eventsResponse.json()} + <Message message="Parsing events ..." /> + {:then events} + {#if events} + {#each events as rawEvent, i} + <Event event={rawEvent} avatar /> - {#if i < events.length - 1} - <p /> - {/if} - {/each} - {/if} - {:catch} - <Message message="" loader="ripple" slot withReload>Error parsing events.</Message> - {/await} + {#if i < events.length - 1} + <p /> + {/if} + {/each} + {/if} + {:catch} + <Message message="" loader="ripple" slot withReload>Error parsing events.</Message> + {/await} {/await} diff --git a/src/routes/events/group/[group]/+page.server.ts b/src/routes/events/group/[group]/+page.server.ts index b4094b4f..3df284fb 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 01203563..37c23c40 100644 --- a/src/routes/events/group/[group]/+page.svelte +++ b/src/routes/events/group/[group]/+page.svelte @@ -1,74 +1,74 @@ <script lang="ts"> - 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 { 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'; - export let data; + export let data; - 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: any) => group as GroupType; + const asGroup = (group: any) => group as GroupType; - const asEvent = (event: any) => event as EventType; + const asEvent = (event: any) => event as EventType; </script> {#await groupsResponse} - <Message message="Loading group ..." /> + <Message message="Loading group ..." /> {:then group} - {#if group} - {#await group.json()} - <Message message="Parsing group ..." /> - {:then json} - {#if json === null} - <Message message="" loader="ripple" slot> - This group may not exist. Please - <a href={'#'} on:click={() => location.reload()}>try again</a> later. - </Message> - {:else} - {@const group = asGroup(json)} + {#if group} + {#await group.json()} + <Message message="Parsing group ..." /> + {:then json} + {#if json === null} + <Message message="" loader="ripple" slot> + This group may not exist. Please + <a href={'#'} on:click={() => location.reload()}>try again</a> later. + </Message> + {:else} + {@const group = asGroup(json)} - <Group {group} /> + <Group {group} /> - <p /> + <p /> - <details open> - <summary>Events</summary> + <details open> + <summary>Events</summary> - {#await fetch(root(`/api/events?group=${data.group}`))} - <Message message="Loading events ..." /> - {:then eventsResponse} - {#await eventsResponse.json()} - <Message message="Parsing events ..." /> - {:then events} - {#if events} - {#each events as rawEvent, i} - <Event event={asEvent(rawEvent)} /> + {#await fetch(root(`/api/events?group=${data.group}`))} + <Message message="Loading events ..." /> + {:then eventsResponse} + {#await eventsResponse.json()} + <Message message="Parsing events ..." /> + {:then events} + {#if events} + {#each events as rawEvent, i} + <Event event={asEvent(rawEvent)} /> - {#if i < events.length - 1} - <p /> - {/if} - {/each} - {/if} - {:catch} - <Message message="" loader="ripple" slot withReload>Error parsing events.</Message> - {/await} - {/await} - </details> - {/if} - {:catch} - <Message message="" loader="ripple" slot withReload>Error parsing group.</Message> - {/await} - {:else} - <Message message="Parsing groups ..." /> - {/if} + {#if i < events.length - 1} + <p /> + {/if} + {/each} + {/if} + {:catch} + <Message message="" loader="ripple" slot withReload>Error parsing events.</Message> + {/await} + {/await} + </details> + {/if} + {:catch} + <Message message="" loader="ripple" slot withReload>Error parsing group.</Message> + {/await} + {:else} + <Message message="Parsing groups ..." /> + {/if} {:catch} - <Message message="" loader="ripple" slot withReload>Error loading group.</Message> + <Message message="" loader="ripple" slot withReload>Error loading group.</Message> {/await} diff --git a/src/routes/events/groups/+page.svelte b/src/routes/events/groups/+page.svelte index a28e11a6..d90cce34 100644 --- a/src/routes/events/groups/+page.svelte +++ b/src/routes/events/groups/+page.svelte @@ -1,43 +1,43 @@ <script lang="ts"> - 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 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: any) => group as GroupType; + const asGroup = (group: any) => group as GroupType; </script> {#await groupsResponse} - <Message message="Loading groups ..." /> + <Message message="Loading groups ..." /> {:then groups} - {#if groups} - {#await groups.json()} - <Message message="Parsing groups ..." /> - {:then json} - {#each json as rawGroup, i} - {@const group = asGroup(rawGroup)} + {#if groups} + {#await groups.json()} + <Message message="Parsing groups ..." /> + {:then json} + {#each json as rawGroup, i} + {@const group = asGroup(rawGroup)} - <a href={root(`/events/group/${group.anilist_username}`)}> - <Group {group} /> - </a> + <a href={root(`/events/group/${group.anilist_username}`)}> + <Group {group} /> + </a> - {#if i < json.length - 1} - <p /> - {/if} - {/each} - {:catch} - <Message message="" loader="ripple" slot withReload>Error parsing groups.</Message> - {/await} - {:else} - <Message message="Parsing groups ..." /> - {/if} + {#if i < json.length - 1} + <p /> + {/if} + {/each} + {:catch} + <Message message="" loader="ripple" slot withReload>Error parsing groups.</Message> + {/await} + {:else} + <Message message="Parsing groups ..." /> + {/if} {:catch} - <Message message="" loader="ripple" slot withReload>Error loading groups.</Message> + <Message message="" loader="ripple" slot withReload>Error loading groups.</Message> {/await} diff --git a/src/routes/feeds/activity-notifications/+server.ts b/src/routes/feeds/activity-notifications/+server.ts index 354f80a0..b8675de9 100644 --- a/src/routes/feeds/activity-notifications/+server.ts +++ b/src/routes/feeds/activity-notifications/+server.ts @@ -2,7 +2,7 @@ import { notifications, type Notification } from '$lib/Data/AniList/notification 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" ?> @@ -22,33 +22,33 @@ const render = (posts: Notification[] = []) => `<?xml version="1.0" encoding="UT <language>en-US</language> <snf:logo><url>https://due.moe/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 +57,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 6d3cbc62..71982c32 100644 --- a/src/routes/girls/+page.svelte +++ b/src/routes/girls/+page.svelte @@ -1,132 +1,132 @@ <script lang="ts"> - 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 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" /> <div class="card"> - <div class="split"> - <div> - {#await Senpy.getRandomImage()} - <Message message="Loading image ..." /> - - <Skeleton grid={true} count={1} width="49%" height="16.25em" /> - {:then randomImage} - <div class="preview"> - <a href={randomImage.image} target="_blank"> - <img src={randomImage.image} alt="A random anime girl holding a programming book" /> - </a> - </div> - {/await} - </div> - <div> - The Senpy Club <span class="opaque">|</span> Anime Girls Holding Programming Books - - <p /> - - <ul> - <li> - <a href="https://github.com/senpy-club/frontend-next" target="_blank" - >Front-end Version 1.</a - > — Similar functionality to this page, but with a different design - </li> - <li> - <a href="https://github.com/senpy-club/api-worker" target="_blank">REST API</a> — The Official - Senpy Club REST API - </li> - <li> - <a href="https://github.com/senpy-club/cli" target="_blank">Command-line Interface</a> — Access - The Senpy Club API from your terminal (and export to files too) - </li> - <li> - <a href="https://docs.rs/senpy" target="_blank"><code>senpy-rs</code></a> — Rust bindings to - The Senpy Club API - </li> - <li> - <a href="https://docs.rs/senpy-ffi" target="_blank"><code>senpy-ffi</code></a> — Access The - Senpy Club API from any programming language (with FFI support) - </li> - <li> - <a href="https://github.com/senpy-club/graphql-api" target="_blank">GraphQL API</a> — - Official GraphQL access to The Senpy Club API (<a - href="https://graphql.senpy.club/playground" - target="_blank" - > - Playground</a - >) - </li> - </ul> - </div> - </div> + <div class="split"> + <div> + {#await Senpy.getRandomImage()} + <Message message="Loading image ..." /> + + <Skeleton grid={true} count={1} width="49%" height="16.25em" /> + {:then randomImage} + <div class="preview"> + <a href={randomImage.image} target="_blank"> + <img src={randomImage.image} alt="A random anime girl holding a programming book" /> + </a> + </div> + {/await} + </div> + <div> + The Senpy Club <span class="opaque">|</span> Anime Girls Holding Programming Books + + <p /> + + <ul> + <li> + <a href="https://github.com/senpy-club/frontend-next" target="_blank" + >Front-end Version 1.</a + > — Similar functionality to this page, but with a different design + </li> + <li> + <a href="https://github.com/senpy-club/api-worker" target="_blank">REST API</a> — The Official + Senpy Club REST API + </li> + <li> + <a href="https://github.com/senpy-club/cli" target="_blank">Command-line Interface</a> — Access + The Senpy Club API from your terminal (and export to files too) + </li> + <li> + <a href="https://docs.rs/senpy" target="_blank"><code>senpy-rs</code></a> — Rust bindings to + The Senpy Club API + </li> + <li> + <a href="https://docs.rs/senpy-ffi" target="_blank"><code>senpy-ffi</code></a> — Access The + Senpy Club API from any programming language (with FFI support) + </li> + <li> + <a href="https://github.com/senpy-club/graphql-api" target="_blank">GraphQL API</a> — + Official GraphQL access to The Senpy Club API (<a + href="https://graphql.senpy.club/playground" + target="_blank" + > + Playground</a + >) + </li> + </ul> + </div> + </div> </div> <p /> <details class="languages" open> - <summary>Languages</summary> - - {#await Senpy.getLanguages()} - <Message message="Loading languages ..." /> - - <Skeleton - card={false} - count={10} - pad={false} - height={'0.9rem'} - width={'100%'} - list={true} - grid={false} - /> - {:then languages} - <ul> - {#each languages as language} - <li><a href={root(`/girls/${encodeURIComponent(language)}`)}>{language}</a></li> - {/each} - </ul> - {/await} + <summary>Languages</summary> + + {#await Senpy.getLanguages()} + <Message message="Loading languages ..." /> + + <Skeleton + card={false} + count={10} + pad={false} + height={'0.9rem'} + width={'100%'} + list={true} + grid={false} + /> + {:then languages} + <ul> + {#each languages as language} + <li><a href={root(`/girls/${encodeURIComponent(language)}`)}>{language}</a></li> + {/each} + </ul> + {/await} </details> <style> - img { - border-radius: 8px; - width: 25vh; - } - - .languages ul { - columns: 6; - } - - @media (max-width: 1280px) { - .languages ul { - columns: 5; - } - } - - @media (max-width: 1024px) { - .languages ul { - columns: 4; - } - } - - @media (max-width: 768px) { - .languages ul { - columns: 3; - } - } - - @media (max-width: 512px) { - .languages ul { - columns: 2; - } - } - - .split { - display: flex; - gap: 1em; - } + img { + border-radius: 8px; + width: 25vh; + } + + .languages ul { + columns: 6; + } + + @media (max-width: 1280px) { + .languages ul { + columns: 5; + } + } + + @media (max-width: 1024px) { + .languages ul { + columns: 4; + } + } + + @media (max-width: 768px) { + .languages ul { + columns: 3; + } + } + + @media (max-width: 512px) { + .languages ul { + columns: 2; + } + } + + .split { + display: flex; + gap: 1em; + } </style> diff --git a/src/routes/girls/[language]/+page.server.ts b/src/routes/girls/[language]/+page.server.ts index 7084986f..62fbb311 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 8b8764d0..91b18628 100644 --- a/src/routes/girls/[language]/+page.svelte +++ b/src/routes/girls/[language]/+page.svelte @@ -1,39 +1,39 @@ <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'; - export let data; + export let data; </script> <div class="card"> - {#await Senpy.getImages(data.language)} - <Message message="Loading images ..." /> + {#await Senpy.getImages(data.language)} + <Message message="Loading images ..." /> - <Skeleton grid={true} count={1} width="49%" height="16.25em" /> - {:then images} - <div class="images"> - {#each images as image} - <a href={image} target="_blank"> - <div class="preview"> - <img src={image} alt="An anime girl holding a programming book" /> - </div> - </a> - {/each} - </div> - {/await} + <Skeleton grid={true} count={1} width="49%" height="16.25em" /> + {:then images} + <div class="images"> + {#each images as image} + <a href={image} target="_blank"> + <div class="preview"> + <img src={image} alt="An anime girl holding a programming book" /> + </div> + </a> + {/each} + </div> + {/await} </div> <style lang="scss"> - .images { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(16.25em, 1fr)); - gap: 1em; + .images { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16.25em, 1fr)); + gap: 1em; - img { - border-radius: 8px; - width: 100%; - } - } + img { + border-radius: 8px; + width: 100%; + } + } </style> diff --git a/src/routes/hololive/[[stream]]/+page.server.ts b/src/routes/hololive/[[stream]]/+page.server.ts index 87839c14..6eb5dad1 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 777f1e70..d5419711 100644 --- a/src/routes/hololive/[[stream]]/+page.svelte +++ b/src/routes/hololive/[[stream]]/+page.svelte @@ -1,82 +1,82 @@ <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'; - export let data; + export let data; - 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" /> {#await schedulePromise} - <Message message="Loading schedule ..." /> + <Message message="Loading schedule ..." /> - <Skeleton grid={true} count={100} width="49%" height="16.25em" /> + <Skeleton grid={true} count={100} width="49%" height="16.25em" /> {:then scheduleResponse} - {#if scheduleResponse} - {#await scheduleResponse.text()} - <Message message="Parsing schedule ..." /> + {#if scheduleResponse} + {#await scheduleResponse.text()} + <Message message="Parsing schedule ..." /> - <Skeleton grid={true} count={100} width="49%" height="16.25em" /> - {:then untypedSchedule} - {@const schedule = typeSchedule(parseScheduleHtml(untypedSchedule))} + <Skeleton grid={true} count={100} width="49%" height="16.25em" /> + {:then untypedSchedule} + {@const schedule = typeSchedule(parseScheduleHtml(untypedSchedule))} - <Lives {schedule} {pinnedStreams} {getPinnedStreams} filter={data.stream} /> - {:catch} - <Message loader="ripple" slot> - {$locale().hololive.parseError} - <a href={'#'} on:click={() => location.reload()}>Try again?</a> - </Message> - {/await} - {:else} - <Message message="Loading schedule ..." /> + <Lives {schedule} {pinnedStreams} {getPinnedStreams} filter={data.stream} /> + {:catch} + <Message loader="ripple" slot> + {$locale().hololive.parseError} + <a href={'#'} on:click={() => location.reload()}>Try again?</a> + </Message> + {/await} + {:else} + <Message message="Loading schedule ..." /> - <Skeleton grid={true} count={100} width="49%" height="16.25em" /> - {/if} + <Skeleton grid={true} count={100} width="49%" height="16.25em" /> + {/if} {:catch} - <Message loader="ripple" slot> - {$locale().hololive.loadError} Please - <a href={'#'} on:click={() => location.reload()}>try again</a> later. - </Message> + <Message loader="ripple" slot> + {$locale().hololive.loadError} Please + <a href={'#'} on:click={() => location.reload()}>try again</a> later. + </Message> {/await} diff --git a/src/routes/reader/+page.svelte b/src/routes/reader/+page.svelte index 98c3ae1c..775d3659 100644 --- a/src/routes/reader/+page.svelte +++ b/src/routes/reader/+page.svelte @@ -1,38 +1,38 @@ <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> - {#if resourceIdentity} - {#await fetchResource(submission)} - <Message message="Loading chapters ..." /> - {:then response} - {#if response.ok} - {#await decodeResource(response, submission) then data} - {#if resourceIdentity === Resource.MangaDex} - <MangaDexChapters {data} /> - {:else if resourceIdentity === Resource.Rawkuma} - <RawkumaChapters {data} /> - {/if} - {:catch error} - <Notice>{error}</Notice> - {/await} - {:else} - <Notice>Failed to fetch data</Notice> - {/if} - {:catch} - <Notice>An unknown error has occurred.</Notice> - {/await} - {:else} - <Notice>Invalid URL</Notice> - {/if} + {#if resourceIdentity} + {#await fetchResource(submission)} + <Message message="Loading chapters ..." /> + {:then response} + {#if response.ok} + {#await decodeResource(response, submission) then data} + {#if resourceIdentity === Resource.MangaDex} + <MangaDexChapters {data} /> + {:else if resourceIdentity === Resource.Rawkuma} + <RawkumaChapters {data} /> + {/if} + {:catch error} + <Notice>{error}</Notice> + {/await} + {:else} + <Notice>Failed to fetch data</Notice> + {/if} + {:catch} + <Notice>An unknown error has occurred.</Notice> + {/await} + {:else} + <Notice>Invalid URL</Notice> + {/if} </InputTemplate> diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte index c1447e5f..78744347 100644 --- a/src/routes/schedule/+page.svelte +++ b/src/routes/schedule/+page.svelte @@ -1,27 +1,27 @@ <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 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'; - 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" /> @@ -53,39 +53,39 @@ <p /> --> {#if !$subsPlease} - <Message message="Loading subtitle schedule ..." /> + <Message message="Loading subtitle schedule ..." /> - <Skeleton grid={true} count={7} height="15em" width="49.5%" /> + <Skeleton grid={true} count={7} height="15em" width="49.5%" /> {:else} - {#await scheduledMediaPromise} - <Message message="Loading schedule ..." /> + {#await scheduledMediaPromise} + <Message message="Loading schedule ..." /> - <Skeleton grid={true} count={7} height="15em" width="49.5%" /> - {:then scheduledMedia} - {#if scheduledMedia} - <div class="schedule-container" id="schedule"> - <Days subsPlease={$subsPlease} {scheduledMedia} {forceListMode} /> - </div> - {:else} - <Message message="Loading schedule ..." /> + <Skeleton grid={true} count={7} height="15em" width="49.5%" /> + {:then scheduledMedia} + {#if scheduledMedia} + <div class="schedule-container" id="schedule"> + <Days subsPlease={$subsPlease} {scheduledMedia} {forceListMode} /> + </div> + {:else} + <Message message="Loading schedule ..." /> - <Skeleton grid={true} count={7} height="15em" width="49.5%" /> - {/if} - {:catch} - <Error type="Media" loginSessionError={false} card list={false} /> - {/await} + <Skeleton grid={true} count={7} height="15em" width="49.5%" /> + {/if} + {:catch} + <Error type="Media" loginSessionError={false} card list={false} /> + {/await} {/if} <style> - .schedule-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(35%, 1fr)); - gap: 0.5em; - } + .schedule-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(35%, 1fr)); + gap: 0.5em; + } - @media (max-width: 800px) { - .schedule-container { - grid-template-columns: 1fr !important; - } - } + @media (max-width: 800px) { + .schedule-container { + grid-template-columns: 1fr !important; + } + } </style> diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index b507eeca..42ee4edd 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,56 +1,56 @@ <script lang="ts"> - /* eslint svelte/no-at-html-tags: "off" */ + /* eslint svelte/no-at-html-tags: "off" */ - 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'; - export let data; + export let data; - // 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" /> <blockquote> - {#if $settings.displayLanguage == 'en'} - Have feedback or suggestions? Send a private message to - <a - href="https://anilist.co/user/fuwn" - target="_blank" - title={$locale().settings.tooltips.author} - use:tooltip>@fuwn</a - > - on AniList! - {:else if $settings.displayLanguage == 'ja'} - フィードバックや提案はありますか?AniListで - <a - href="https://anilist.co/user/fuwn" - target="_blank" - title={$locale().settings.tooltips.author} - use:tooltip>@fuwn</a - > - にDMを送ってください! - {/if} + {#if $settings.displayLanguage == 'en'} + Have feedback or suggestions? Send a private message to + <a + href="https://anilist.co/user/fuwn" + target="_blank" + title={$locale().settings.tooltips.author} + use:tooltip>@fuwn</a + > + on AniList! + {:else if $settings.displayLanguage == 'ja'} + フィードバックや提案はありますか?AniListで + <a + href="https://anilist.co/user/fuwn" + target="_blank" + title={$locale().settings.tooltips.author} + use:tooltip>@fuwn</a + > + にDMを送ってください! + {/if} - <!-- <p /> + <!-- <p /> <p> <b>{$locale().settings.fields.notice}</b> @@ -60,50 +60,50 @@ </blockquote> {#if data.user === undefined} - <LogInRestricted /> + <LogInRestricted /> {:else} - <div class="small-categories"> - <Category title={$locale().settings.settingsSync.title} id="sync" newLine={false}> - <SettingSync /> - </Category> - <Category title={$locale().settings.rssFeeds.title} id="feeds" newLine={false}> - <RssFeeds user={data.user} /> - </Category> - </div> + <div class="small-categories"> + <Category title={$locale().settings.settingsSync.title} id="sync" newLine={false}> + <SettingSync /> + </Category> + <Category title={$locale().settings.rssFeeds.title} id="feeds" newLine={false}> + <RssFeeds user={data.user} /> + </Category> + </div> - <p /> + <p /> - <Category title={$locale().settings.display.title}><Display /></Category> - <Category title={$locale().settings.calculation.title}><Calculation /></Category> - <Category title={$locale().settings.cache.title}><Cache /></Category> - <Category id="debug"> - <summary> - {$locale().settings.debug.title} - <button - class="smaller-button button-badge badge-info unclickable-button" - title={$locale().settings.debug.tooltips.version} - use:tooltip - on:click|preventDefault - >{data.commit.slice(0, 7)} - </button></summary - > + <Category title={$locale().settings.display.title}><Display /></Category> + <Category title={$locale().settings.calculation.title}><Calculation /></Category> + <Category title={$locale().settings.cache.title}><Cache /></Category> + <Category id="debug"> + <summary> + {$locale().settings.debug.title} + <button + class="smaller-button button-badge badge-info unclickable-button" + title={$locale().settings.debug.tooltips.version} + use:tooltip + on:click|preventDefault + >{data.commit.slice(0, 7)} + </button></summary + > - <Debug /> - </Category> - <Category title={$locale().settings.attributions.title} open={false}><Attributions /></Category> + <Debug /> + </Category> + <Category title={$locale().settings.attributions.title} open={false}><Attributions /></Category> {/if} <style> - .small-categories { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - align-items: start; - } + .small-categories { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + align-items: start; + } - @media (max-width: 600px) { - .small-categories { - grid-template-columns: 1fr; - } - } + @media (max-width: 600px) { + .small-categories { + grid-template-columns: 1fr; + } + } </style> diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index 01a73877..97e27e0e 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -1,10 +1,10 @@ <script lang="ts"> - 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 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} /> @@ -12,63 +12,63 @@ <HeadTitle route={tools[tool].name()} path={`/tools?tool=${tool}`} /> <div class="card"> - <div class="tool-grid"> - {#each Object.keys(tools).filter((t) => t !== 'default') as t} - <a href={root(`/tools/${tools[t].id}`)} on:click={() => (tool = t)}> - <div class="tool-grid-tool card"> - <span class="title"> - {tools[t].name()} - </span> + <div class="tool-grid"> + {#each Object.keys(tools).filter((t) => t !== 'default') as t} + <a href={root(`/tools/${tools[t].id}`)} on:click={() => (tool = t)}> + <div class="tool-grid-tool card"> + <span class="title"> + {tools[t].name()} + </span> - <p /> + <p /> - {#if tools[t].description} - <span class="description"> - {tools[t].description?.()} - </span> - {/if} - </div> - </a> - {/each} - </div> + {#if tools[t].description} + <span class="description"> + {tools[t].description?.()} + </span> + {/if} + </div> + </a> + {/each} + </div> - <p /> + <p /> - <blockquote style="margin: 0 0 0 1.5rem;"> - Have any requests for cool tools that you think others might find useful? Send a private message - to - <a href="https://anilist.co/user/fuwn" target="_blank" rel="noopener">@fuwn</a> on AniList! - </blockquote> + <blockquote style="margin: 0 0 0 1.5rem;"> + Have any requests for cool tools that you think others might find useful? Send a private message + to + <a href="https://anilist.co/user/fuwn" target="_blank" rel="noopener">@fuwn</a> on AniList! + </blockquote> </div> <style lang="scss"> - .tool-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - grid-gap: 1rem; - } + .tool-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-gap: 1rem; + } - .tool-grid-tool { - display: flex; - flex-direction: column; - height: 100%; - $delay: 0.25s; - transition: transform $delay ease; + .tool-grid-tool { + display: flex; + flex-direction: column; + height: 100%; + $delay: 0.25s; + transition: transform $delay ease; - .description { - // font-size: 0.9rem; - color: var(--fg); - } + .description { + // font-size: 0.9rem; + color: var(--fg); + } - &:hover { - transform: scale(1.05); - position: relative; - z-index: 2; - transition: transform $delay ease; - } - } + &:hover { + transform: scale(1.05); + position: relative; + z-index: 2; + transition: transform $delay ease; + } + } - .title { - font-weight: 500; - } + .title { + font-weight: 500; + } </style> diff --git a/src/routes/tools/[tool]/+page.server.ts b/src/routes/tools/[tool]/+page.server.ts index c7e72d18..11ba6d15 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 3749c096..9140ae1c 100644 --- a/src/routes/tools/[tool]/+page.svelte +++ b/src/routes/tools/[tool]/+page.svelte @@ -1,84 +1,84 @@ <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 HololiveBirthdays from '$lib/Tools/HololiveBirthdays.svelte'; - import SequelCatcher from '$lib/Tools/SequelCatcher/Tool.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 HololiveBirthdays from '$lib/Tools/HololiveBirthdays.svelte'; + import SequelCatcher from '$lib/Tools/SequelCatcher/Tool.svelte'; - export let data; + export let data; - 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 /> {#if !Object.keys(tools).includes(tool)} - <HeadTitle route="Tools" path="/tools" /> + <HeadTitle route="Tools" path="/tools" /> - <Popup> - <p style="text-align: center;"> - Tool "<a href={root(`/tools/${tool}`)}>{tool}</a>" not found - </p> + <Popup> + <p style="text-align: center;"> + Tool "<a href={root(`/tools/${tool}`)}>{tool}</a>" not found + </p> - <blockquote style="margin: 0 0 0 1.5rem;"> - Did you mean "<a - href={root(`/tools/${tools[suggestion].id}`)} - on:click={() => (tool = suggestion)} - style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''} - > - {suggestion === '...' ? '...' : tools[suggestion].name()}</a - >"? - </blockquote> - </Popup> + <blockquote style="margin: 0 0 0 1.5rem;"> + Did you mean "<a + href={root(`/tools/${tools[suggestion].id}`)} + on:click={() => (tool = suggestion)} + style={suggestion === '...' ? 'pointer-events: none; color: inherit;' : ''} + > + {suggestion === '...' ? '...' : tools[suggestion].name()}</a + >"? + </blockquote> + </Popup> {:else} - <HeadTitle route={tools[tool].name()} path={`/tools?tool=${tool}`} /> + <HeadTitle route={tools[tool].name()} path={`/tools?tool=${tool}`} /> - {#if tool === 'activity_history'} - <ActivityHistory user={data.user} /> - {:else if tool === 'wrapped'} - <Wrapped user={data.user} /> - {:else if tool === 'discussions'} - <EpisodeDiscussionCollector /> - {:else if tool === 'birthdays'} - <CharacterBirthdays /> - {:else if tool === 'sequel_spy'} - <SequelSpy user={data.user} /> - {:else if tool === 'random_follower'} - <RandomFollower /> - {:else if tool === 'dump_profile'} - <DumpProfile /> - {:else if tool === 'likes'} - <Likes /> - {:else if tool === 'uma_musume_birthdays'} - <UmaMusumeBirthdays /> - {:else if tool === 'hayai'} - <Hayai /> - {:else if tool === 'hololive_birthdays'} - <HololiveBirthdays /> - {:else if tool === 'sequel_catcher'} - <SequelCatcher user={data.user} /> - {/if} + {#if tool === 'activity_history'} + <ActivityHistory user={data.user} /> + {:else if tool === 'wrapped'} + <Wrapped user={data.user} /> + {:else if tool === 'discussions'} + <EpisodeDiscussionCollector /> + {:else if tool === 'birthdays'} + <CharacterBirthdays /> + {:else if tool === 'sequel_spy'} + <SequelSpy user={data.user} /> + {:else if tool === 'random_follower'} + <RandomFollower /> + {:else if tool === 'dump_profile'} + <DumpProfile /> + {:else if tool === 'likes'} + <Likes /> + {:else if tool === 'uma_musume_birthdays'} + <UmaMusumeBirthdays /> + {:else if tool === 'hayai'} + <Hayai /> + {:else if tool === 'hololive_birthdays'} + <HololiveBirthdays /> + {:else if tool === 'sequel_catcher'} + <SequelCatcher user={data.user} /> + {/if} {/if} diff --git a/src/routes/updates/+page.svelte b/src/routes/updates/+page.svelte index 61ed9678..9af001b4 100644 --- a/src/routes/updates/+page.svelte +++ b/src/routes/updates/+page.svelte @@ -1,144 +1,144 @@ <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: NodeJS.Timeout; - - 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'); + /* 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: NodeJS.Timeout; + + 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'); </script> <HeadTitle route="Updates" path="/updates" /> <div class="list-container"> - <details open class="list"> - <summary> - Manga - <small class="opaque">{mangaEndTime ? mangaEndTime / 1000 : '...'}s</small> - </summary> - - {#if feed === null} - Failed to load feed - {:else if feed !== undefined} - <ul> - {#each feed.items as item} - <li> - {#if directLink} - <i>{reformatChapter(item.title)}</i> - - {@html item.content} - {:else} - <a - href={`https://anilist.co/search/manga?search=${clipTitle( - reformatChapter(item.title) - )}&sort=SEARCH_MATCH`} - > - <i>{@html clipTitle(reformatChapter(item.title))}</i> - </a> - {@html chapterTitle(reformatChapter(item.title))} - {/if} - </li> - {/each} - </ul> - {:else} - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> - - <details open class="list"> - <summary> - Novels - <small class="opaque">{novelEndTime ? novelEndTime / 1000 : '...'}s</small> - </summary> - - {#if novelFeed === null} - Failed to load feed - {:else if novelFeed !== undefined} - <ul> - {#each novelFeed.data.items as item} - <li> - {#if directLink} - <a href={item.srcurl}> - <i>{@html item.series.name}</i> - {@html item.postfix || `Ch. ${item.chapter}`} - </a> - {:else} - <a - href={`https://anilist.co/search/manga?search=${item.series.name}&sort=SEARCH_MATCH`} - > - <i>{@html item.series.name}</i> - </a> - {@html item.postfix || `Ch. ${item.chapter}`} - {/if} - </li> - {/each} - </ul> - {:else} - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> + <details open class="list"> + <summary> + Manga + <small class="opaque">{mangaEndTime ? mangaEndTime / 1000 : '...'}s</small> + </summary> + + {#if feed === null} + Failed to load feed + {:else if feed !== undefined} + <ul> + {#each feed.items as item} + <li> + {#if directLink} + <i>{reformatChapter(item.title)}</i> + + {@html item.content} + {:else} + <a + href={`https://anilist.co/search/manga?search=${clipTitle( + reformatChapter(item.title) + )}&sort=SEARCH_MATCH`} + > + <i>{@html clipTitle(reformatChapter(item.title))}</i> + </a> + {@html chapterTitle(reformatChapter(item.title))} + {/if} + </li> + {/each} + </ul> + {:else} + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + + <details open class="list"> + <summary> + Novels + <small class="opaque">{novelEndTime ? novelEndTime / 1000 : '...'}s</small> + </summary> + + {#if novelFeed === null} + Failed to load feed + {:else if novelFeed !== undefined} + <ul> + {#each novelFeed.data.items as item} + <li> + {#if directLink} + <a href={item.srcurl}> + <i>{@html item.series.name}</i> + {@html item.postfix || `Ch. ${item.chapter}`} + </a> + {:else} + <a + href={`https://anilist.co/search/manga?search=${item.series.name}&sort=SEARCH_MATCH`} + > + <i>{@html item.series.name}</i> + </a> + {@html item.postfix || `Ch. ${item.chapter}`} + {/if} + </li> + {/each} + </ul> + {:else} + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> </div> <style> - .list-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - align-items: start; - gap: 1em; - flex-wrap: wrap; - } - - .list { - overflow-y: auto; - min-width: 300px; - flex: 1 1 300px; - } + .list-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + align-items: start; + gap: 1em; + flex-wrap: wrap; + } + + .list { + overflow-y: auto; + min-width: 300px; + flex: 1 1 300px; + } </style> diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte index e66c8503..8db13d7d 100644 --- a/src/routes/user/+page.svelte +++ b/src/routes/user/+page.svelte @@ -1,26 +1,26 @@ <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 { 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'; - const user = - browser && localStorage.getItem('identity') - ? (JSON.parse(localStorage.getItem('identity') || '') as UserIdentity).name - : null; + const user = + browser && localStorage.getItem('identity') + ? (JSON.parse(localStorage.getItem('identity') || '') as UserIdentity).name + : null; - onMount(() => { - if (user) { - 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` - ); - } - }); + onMount(() => { + if (user) { + 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 fd31248b..491290aa 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 84f121ea..d60ea8e5 100644 --- a/src/routes/user/[user]/+page.svelte +++ b/src/routes/user/[user]/+page.svelte @@ -1,550 +1,550 @@ <script lang="ts"> - 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 '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); - - const setCategoriesQuery = graphql(` - mutation SetCategories($categories: [String!]!) { - setPinnedBadgeWallCategories(categories: $categories) { - id - - preferences { - pinned_badge_wall_categories - } - } - } - `); - - const toggleCategoryQuery = graphql(` - mutation ToggleCategory($category: String!) { - togglePinnedBadgeWallCategory(category: $category) { - id - - preferences { - pinned_badge_wall_categories - } - } - } - `); - - const toggleHideMissingBadgesQuery = graphql(` - mutation ToggleHideMissingBadges { - toggleHideMissingBadges { - id - - preferences { - hide_missing_badges - } - } - } - `); - - const toggleHideAWCBadgesQuery = graphql(` - mutation ToggleHideAWCBadges { - toggleHideAWCBadges { - id - - preferences { - hide_awc_badges - } - } - } - `); - - const setBiographyQuery = graphql(` - mutation SetBiography($biography: String!) { - setBiography(biography: $biography) { - id - - preferences { - biography - } - } - } - `); - - const setBadgeWallCSSQuery = graphql(` - mutation SetBadgeWallCSS($css: String!) { - setBadgeWallCSS(css: $css) { - id - - preferences { - badge_wall_css - } - } - } - `); - - $: 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: any) => { - event.preventDefault(); - - 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 = ''; - }; + 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 '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); + + const setCategoriesQuery = graphql(` + mutation SetCategories($categories: [String!]!) { + setPinnedBadgeWallCategories(categories: $categories) { + id + + preferences { + pinned_badge_wall_categories + } + } + } + `); + + const toggleCategoryQuery = graphql(` + mutation ToggleCategory($category: String!) { + togglePinnedBadgeWallCategory(category: $category) { + id + + preferences { + pinned_badge_wall_categories + } + } + } + `); + + const toggleHideMissingBadgesQuery = graphql(` + mutation ToggleHideMissingBadges { + toggleHideMissingBadges { + id + + preferences { + hide_missing_badges + } + } + } + `); + + const toggleHideAWCBadgesQuery = graphql(` + mutation ToggleHideAWCBadges { + toggleHideAWCBadges { + id + + preferences { + hide_awc_badges + } + } + } + `); + + const setBiographyQuery = graphql(` + mutation SetBiography($biography: String!) { + setBiography(biography: $biography) { + id + + preferences { + biography + } + } + } + `); + + const setBadgeWallCSSQuery = graphql(` + mutation SetBadgeWallCSS($css: String!) { + setBadgeWallCSS(css: $css) { + id + + preferences { + badge_wall_css + } + } + } + `); + + $: 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: any) => { + event.preventDefault(); + + 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 + // 8.5827814569536423841e0 </script> <HeadTitle route={`${data.username}'s Profile`} path={`/user/${data.username}`} /> {#if error} - <AnimeRateLimited> - <a href={`https://anilist.co/user/${data.username}`} target="_blank">@{data.username}</a>'s - profile could not be loaded. - </AnimeRateLimited> + <AnimeRateLimited> + <a href={`https://anilist.co/user/${data.username}`} target="_blank">@{data.username}</a>'s + profile could not be loaded. + </AnimeRateLimited> {:else} - {#if userData === null} - <Message slot withReload> - <p> - Could not load user profile for <a - href={`https://anilist.co/user/${data.username}`} - target="_blank">@{data.username}</a - >. - </p> - </Message> - {:else if userData === undefined} - <Skeleton card={false} bigCard count={1} height="224px" /> - - <Message message="Loading user profile ..." /> - {:else} - <div class="card card-small"> - <div - class="card user-grid" - style={`background-image: ${ - userData ? `url(${userData.bannerImage})` : 'none' - }; padding: 0;`} - > - {#if userData} - <img src={userData.bannerImage} alt="" class="cover-image" /> - {/if} - - <div class="card user-grid-content"> - <div class="user-grid-avatar"> - <LinkedTooltip content={`${userData.name}`} id="avatar" pin="avatar" relative> - <a href={`https://anilist.co/user/${userData.name}`} target="_blank"> - <ParallaxImage - source={$settings.displayDataSaver - ? userData.avatar.medium - : userData.avatar.large} - alternativeText="" - style="border-radius: 8px; width: 6.5em;" - /> - </a> - </LinkedTooltip> - </div> - - <div class="user-grid-rest"> - <p> - <a - href={`https://anilist.co/user/${userData.name}`} - target="_blank" - title={String(userData.id)} - use:tooltip - > - @{userData.name} - </a> - {#if userData && authorisedUsers.includes(userData.id)} - ‌ - <button class="unclickable-button button-badge badge-rainbow">Owner</button> - {/if} - <span class="click-item separator opaque">•</span> - <a href={root(`/user/${userData.name}/badges`)}>Badge Wall</a> - </p> - - {#if preferences && preferences.biography && preferences.biography.length > 0} - <SvelteMarkdown - source={preferences.biography} - renderers={{ - link: MarkdownLink - }} - /> - {/if} - - {$locale({ - values: { - username: userData.name, - anime: (userData.statistics.anime.minutesWatched / 60 / 24).toFixed(1), - manga: estimatedDayReading(userData.statistics.manga.chaptersRead).toFixed(1) - } - }).user.profile.statistics} - - {#if schedule && preferences && preferences.biography && preferences.biography.length > 0} - <br /> - {:else} - <p /> - {/if} - - {#if $Profile.fetching} - {displayBadges(userData.name, '...')} - {:else if $Profile.data && $Profile.data.User} - {@const badges = $Profile.data.User.badgesCount} - - {#if badges} - {displayBadges(userData.name, badges)} - {:else} - {displayBadges(userData.name, '?')} - {/if} - {:else} - {displayBadges(userData.name, '?')} - {/if} - </div> - </div> - </div> - </div> - {/if} - - {#if schedule && preferences && preferences.pinned_hololive_streams.length > 0} - <p /> - - <div class="card"> - <div class="hololive-badges"> - {#each preferences.pinned_hololive_streams as stream, index} - {@const avatar = schedule.dict[stream]} - - {#if avatar} - <LinkedTooltip - content={stream} - id={`hololive-badge-${index}`} - pin={`hololive-badge-${index}`} - relative - > - <a href={root(`/hololive/${encodeURIComponent(stream)}`)}> - <div class="user-grid-hololive-badges"> - <ParallaxImage source={avatar} alternativeText="Avatar" /> - </div> - </a> - </LinkedTooltip> - {/if} - {/each} - </div> - </div> - {/if} - - {#if preferences && userData && userData.id === $identity.id} - <p /> - - <details open> - <summary>{$locale().user.preferences.title}</summary> - - <input - type="checkbox" - on:change={() => { - if (userData) toggleHideMissingBadgesQuery.mutate(null).then(); - }} - checked={preferences.hide_missing_badges} - /> - {$locale().user.preferences.hideMissingBadges.title} - <SettingHint lineBreak>{$locale().user.preferences.hideMissingBadges.hint}</SettingHint> - - <p /> - - <input - type="checkbox" - on:change={() => { - if (userData) toggleHideAWCBadgesQuery.mutate(null).then(); - }} - checked={preferences.hide_awc_badges} - /> - {$locale().user.preferences.hideAWCBadges.title} - - <p /> - - Pinned Categories - - <div class="pinned-categories"> - {#each preferences.pinned_badge_wall_categories as category} - <div - class="card card-small pinned-category" - draggable="true" - on:dragstart={(event) => handleDragStart(event, category)} - on:dragover={handleDragOver} - on:dragenter={(event) => handleDragEnter(event, category)} - on:dragleave={(event) => handleDragLeave(event, category)} - on:drop={handleDrop} - role="button" - tabindex="0" - > - <span class="pinned-category-name"> - {category} - </span> - - <button - on:click={() => { - if (userData) toggleCategoryQuery.mutate({ category }).then(); - }}>Remove</button - > - </div> - {/each} - - <span class="card card-small pinned-category"> - <span class="pinned-category-name"> - <input type="text" id="category" placeholder="Category" style="width: 10em;" /> - </span> - - <button class="button-lined" on:click={toggleCategory}>Add</button> - </span> - </div> - - <p /> - - Biography - - <button - on:click={() => { - if (userData) - setBiographyQuery - .mutate({ - biography: getBiography() - }) - .then(); - }}>Save</button - > - <textarea - value={preferences.biography} - rows="5" - cols="100" - id="biography" - placeholder="Markdown supported!" - /> - - <p /> - - Badge Wall Custom CSS - - <button - on:click={() => { - if (userData) - setBadgeWallCSSQuery - .mutate({ - css: getBadgeWallCSS() - }) - .then(); - }}>Save</button - > - <textarea - value={preferences.badge_wall_css} - rows="10" - cols="100" - id="badgeWallCSS" - placeholder="/* Use classes and IDs such as .badges, #badges, .badge, or standard elements like body and details, or anything, as long as it's valid CSS! */" - /> - </details> - {/if} + {#if userData === null} + <Message slot withReload> + <p> + Could not load user profile for <a + href={`https://anilist.co/user/${data.username}`} + target="_blank">@{data.username}</a + >. + </p> + </Message> + {:else if userData === undefined} + <Skeleton card={false} bigCard count={1} height="224px" /> + + <Message message="Loading user profile ..." /> + {:else} + <div class="card card-small"> + <div + class="card user-grid" + style={`background-image: ${ + userData ? `url(${userData.bannerImage})` : 'none' + }; padding: 0;`} + > + {#if userData} + <img src={userData.bannerImage} alt="" class="cover-image" /> + {/if} + + <div class="card user-grid-content"> + <div class="user-grid-avatar"> + <LinkedTooltip content={`${userData.name}`} id="avatar" pin="avatar" relative> + <a href={`https://anilist.co/user/${userData.name}`} target="_blank"> + <ParallaxImage + source={$settings.displayDataSaver + ? userData.avatar.medium + : userData.avatar.large} + alternativeText="" + style="border-radius: 8px; width: 6.5em;" + /> + </a> + </LinkedTooltip> + </div> + + <div class="user-grid-rest"> + <p> + <a + href={`https://anilist.co/user/${userData.name}`} + target="_blank" + title={String(userData.id)} + use:tooltip + > + @{userData.name} + </a> + {#if userData && authorisedUsers.includes(userData.id)} + ‌ + <button class="unclickable-button button-badge badge-rainbow">Owner</button> + {/if} + <span class="click-item separator opaque">•</span> + <a href={root(`/user/${userData.name}/badges`)}>Badge Wall</a> + </p> + + {#if preferences && preferences.biography && preferences.biography.length > 0} + <SvelteMarkdown + source={preferences.biography} + renderers={{ + link: MarkdownLink + }} + /> + {/if} + + {$locale({ + values: { + username: userData.name, + anime: (userData.statistics.anime.minutesWatched / 60 / 24).toFixed(1), + manga: estimatedDayReading(userData.statistics.manga.chaptersRead).toFixed(1) + } + }).user.profile.statistics} + + {#if schedule && preferences && preferences.biography && preferences.biography.length > 0} + <br /> + {:else} + <p /> + {/if} + + {#if $Profile.fetching} + {displayBadges(userData.name, '...')} + {:else if $Profile.data && $Profile.data.User} + {@const badges = $Profile.data.User.badgesCount} + + {#if badges} + {displayBadges(userData.name, badges)} + {:else} + {displayBadges(userData.name, '?')} + {/if} + {:else} + {displayBadges(userData.name, '?')} + {/if} + </div> + </div> + </div> + </div> + {/if} + + {#if schedule && preferences && preferences.pinned_hololive_streams.length > 0} + <p /> + + <div class="card"> + <div class="hololive-badges"> + {#each preferences.pinned_hololive_streams as stream, index} + {@const avatar = schedule.dict[stream]} + + {#if avatar} + <LinkedTooltip + content={stream} + id={`hololive-badge-${index}`} + pin={`hololive-badge-${index}`} + relative + > + <a href={root(`/hololive/${encodeURIComponent(stream)}`)}> + <div class="user-grid-hololive-badges"> + <ParallaxImage source={avatar} alternativeText="Avatar" /> + </div> + </a> + </LinkedTooltip> + {/if} + {/each} + </div> + </div> + {/if} + + {#if preferences && userData && userData.id === $identity.id} + <p /> + + <details open> + <summary>{$locale().user.preferences.title}</summary> + + <input + type="checkbox" + on:change={() => { + if (userData) toggleHideMissingBadgesQuery.mutate(null).then(); + }} + checked={preferences.hide_missing_badges} + /> + {$locale().user.preferences.hideMissingBadges.title} + <SettingHint lineBreak>{$locale().user.preferences.hideMissingBadges.hint}</SettingHint> + + <p /> + + <input + type="checkbox" + on:change={() => { + if (userData) toggleHideAWCBadgesQuery.mutate(null).then(); + }} + checked={preferences.hide_awc_badges} + /> + {$locale().user.preferences.hideAWCBadges.title} + + <p /> + + Pinned Categories + + <div class="pinned-categories"> + {#each preferences.pinned_badge_wall_categories as category} + <div + class="card card-small pinned-category" + draggable="true" + on:dragstart={(event) => handleDragStart(event, category)} + on:dragover={handleDragOver} + on:dragenter={(event) => handleDragEnter(event, category)} + on:dragleave={(event) => handleDragLeave(event, category)} + on:drop={handleDrop} + role="button" + tabindex="0" + > + <span class="pinned-category-name"> + {category} + </span> + + <button + on:click={() => { + if (userData) toggleCategoryQuery.mutate({ category }).then(); + }}>Remove</button + > + </div> + {/each} + + <span class="card card-small pinned-category"> + <span class="pinned-category-name"> + <input type="text" id="category" placeholder="Category" style="width: 10em;" /> + </span> + + <button class="button-lined" on:click={toggleCategory}>Add</button> + </span> + </div> + + <p /> + + Biography + + <button + on:click={() => { + if (userData) + setBiographyQuery + .mutate({ + biography: getBiography() + }) + .then(); + }}>Save</button + > + <textarea + value={preferences.biography} + rows="5" + cols="100" + id="biography" + placeholder="Markdown supported!" + /> + + <p /> + + Badge Wall Custom CSS + + <button + on:click={() => { + if (userData) + setBadgeWallCSSQuery + .mutate({ + css: getBadgeWallCSS() + }) + .then(); + }}>Save</button + > + <textarea + value={preferences.badge_wall_css} + rows="10" + cols="100" + id="badgeWallCSS" + placeholder="/* Use classes and IDs such as .badges, #badges, .badge, or standard elements like body and details, or anything, as long as it's valid CSS! */" + /> + </details> + {/if} {/if} <style lang="scss"> - .user-grid-content { - display: flex; - flex-wrap: wrap; - column-gap: 1.5em; - background-color: rgba(0, 0, 0, 0.468); - color: #d8d8d8; - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - .user-grid-avatar { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - .click-item { - margin: 0 0.625rem; - } - - .user-grid { - background-size: cover; - background-position: center; - background-repeat: no-repeat; - } - - .cover-image { - visibility: hidden; - height: 4.5em; - } - - .user-grid-hololive-badges { - $transitionDuration: 0.45s; - $transition: transform $transitionDuration ease, box-shadow $transitionDuration ease; - $size: 5.25em; - - border-radius: 8px; - width: $size; - height: $size; - object-fit: cover; - transition: $transition; - box-shadow: rgba(0, 0, 11, 0.1) 0px 7px 29px 0px; - overflow: hidden; - - &:hover { - z-index: 2; - transition: $transition; - transform: scale(1.15); - box-shadow: 0 1.5px 9px var(--base01), 0 0 0 4px var(--base02), 0 4px 30px var(--base01); - } - } - - .hololive-badges { - display: flex; - gap: 1rem; - border-radius: 8px; - justify-content: space-around; - flex-wrap: wrap; - } - - .separator { - color: var(--base04); - } - - .user-grid-rest { - flex: 1; - } - - .pinned-categories { - display: flex; - flex-wrap: wrap; - gap: 1rem; - } - - .pinned-category { - display: flex; - align-items: center; - } - - .pinned-category-name { - margin-right: 0.5em; - } + .user-grid-content { + display: flex; + flex-wrap: wrap; + column-gap: 1.5em; + background-color: rgba(0, 0, 0, 0.468); + color: #d8d8d8; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .user-grid-avatar { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .click-item { + margin: 0 0.625rem; + } + + .user-grid { + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + + .cover-image { + visibility: hidden; + height: 4.5em; + } + + .user-grid-hololive-badges { + $transitionDuration: 0.45s; + $transition: transform $transitionDuration ease, box-shadow $transitionDuration ease; + $size: 5.25em; + + border-radius: 8px; + width: $size; + height: $size; + object-fit: cover; + transition: $transition; + box-shadow: rgba(0, 0, 11, 0.1) 0px 7px 29px 0px; + overflow: hidden; + + &:hover { + z-index: 2; + transition: $transition; + transform: scale(1.15); + box-shadow: 0 1.5px 9px var(--base01), 0 0 0 4px var(--base02), 0 4px 30px var(--base01); + } + } + + .hololive-badges { + display: flex; + gap: 1rem; + border-radius: 8px; + justify-content: space-around; + flex-wrap: wrap; + } + + .separator { + color: var(--base04); + } + + .user-grid-rest { + flex: 1; + } + + .pinned-categories { + display: flex; + flex-wrap: wrap; + gap: 1rem; + } + + .pinned-category { + display: flex; + align-items: center; + } + + .pinned-category-name { + margin-right: 0.5em; + } </style> diff --git a/src/routes/user/[user]/+page.ts b/src/routes/user/[user]/+page.ts index c84caafb..e44f06aa 100644 --- a/src/routes/user/[user]/+page.ts +++ b/src/routes/user/[user]/+page.ts @@ -3,17 +3,17 @@ 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)); - 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 afdd797d..060b38e7 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 3857d18b..44dd852a 100644 --- a/src/routes/user/[user]/badges/+page.svelte +++ b/src/routes/user/[user]/badges/+page.svelte @@ -1,1042 +1,1042 @@ <script lang="ts"> - 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'; - - export let data; - - $: ({ BadgeWallUser } = data); - $: preferences = $BadgeWallUser.fetching - ? undefined - : ($BadgeWallUser.data?.User.preferences as Preferences); - - $: 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 - $image: String - $description: String - $time: String - $category: String - $hidden: Boolean - $source: String - $designer: String - ) { - updateBadge( - id: $id - post: $post - image: $image - description: $description - time: $time - category: $category - hidden: $hidden - source: $source - designer: $designer - ) { - id - - badges { - post - image - description - id - time - category - hidden - source - designer - shadow_hidden - click_count - } - } - } - `); - - const pruneBadgesQuery = graphql(` - mutation PruneUserBadges { - pruneUserBadges { - id - - badges { - post - image - description - id - time - category - hidden - source - designer - shadow_hidden - click_count - } - } - } - `); - - const hideCategoryQuery = graphql(` - mutation HideCategory($category: String) { - hideBadge(category: $category) { - id - - badges { - post - image - description - id - time - category - hidden - source - designer - shadow_hidden - click_count - } - } - } - `); - - const deleteBadgeQuery = graphql(` - mutation DeleteBadge($id: Int!) { - deleteBadge(id: $id) { - id - - badges { - post - image - description - id - time - category - hidden - source - designer - shadow_hidden - click_count - } - } - } - `); - - const shadowHideBadgeQuery = graphql(` - mutation ShadowHideBadge($id: Int!, $state: Boolean) { - shadowHideBadge(id: $id, state: $state) { - id - - badges { - id - } - } - } - `); - - 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>; - let migrateMode = false; - let hideMode = false; - const authorised = authorisedJson.includes($identity.id); - let noticeDismissed = false; - - $: categoryFilter = new URLSearchParams($page.url.searchParams).get('category'); - - type GroupedBadges = { [key: string]: IndexedBadge[] }; - - const setShadowHide = () => - shadowHideBadgeQuery.mutate({ - id: badger.id as number - }); - - onMount(async () => { - if (browser && localStorage.getItem('badgeWallNoticeDismissed')) noticeDismissed = true; - - badger = isId - ? { - id: parseInt(data.username), - name: 'User' - } - : await user(data.username); - - if (!isId && !badger) { - loadError = 'User not found.'; - - 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.valueAsDate ? inputTimeToDatabaseTime(time.valueAsDate) : 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(([_category, 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 + 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'; + + export let data; + + $: ({ BadgeWallUser } = data); + $: preferences = $BadgeWallUser.fetching + ? undefined + : ($BadgeWallUser.data?.User.preferences as Preferences); + + $: 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 + $image: String + $description: String + $time: String + $category: String + $hidden: Boolean + $source: String + $designer: String + ) { + updateBadge( + id: $id + post: $post + image: $image + description: $description + time: $time + category: $category + hidden: $hidden + source: $source + designer: $designer + ) { + id + + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + } + `); + + const pruneBadgesQuery = graphql(` + mutation PruneUserBadges { + pruneUserBadges { + id + + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + } + `); + + const hideCategoryQuery = graphql(` + mutation HideCategory($category: String) { + hideBadge(category: $category) { + id + + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + } + `); + + const deleteBadgeQuery = graphql(` + mutation DeleteBadge($id: Int!) { + deleteBadge(id: $id) { + id + + badges { + post + image + description + id + time + category + hidden + source + designer + shadow_hidden + click_count + } + } + } + `); + + const shadowHideBadgeQuery = graphql(` + mutation ShadowHideBadge($id: Int!, $state: Boolean) { + shadowHideBadge(id: $id, state: $state) { + id + + badges { + id + } + } + } + `); + + 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>; + let migrateMode = false; + let hideMode = false; + const authorised = authorisedJson.includes($identity.id); + let noticeDismissed = false; + + $: categoryFilter = new URLSearchParams($page.url.searchParams).get('category'); + + type GroupedBadges = { [key: string]: IndexedBadge[] }; + + const setShadowHide = () => + shadowHideBadgeQuery.mutate({ + id: badger.id as number + }); + + onMount(async () => { + if (browser && localStorage.getItem('badgeWallNoticeDismissed')) noticeDismissed = true; + + badger = isId + ? { + id: parseInt(data.username), + name: 'User' + } + : await user(data.username); + + if (!isId && !badger) { + loadError = 'User not found.'; + + 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.valueAsDate ? inputTimeToDatabaseTime(time.valueAsDate) : 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(([_category, 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: any[]) => array as string[]; - - const castBadgesToIndexedBadges = (array: any[]) => array as IndexedBadge[]; - - const shadowHideBadge = () => { - if (!selectedBadge && !authorised) 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: any[]) => array as string[]; + + const castBadgesToIndexedBadges = (array: any[]) => array as IndexedBadge[]; + + const shadowHideBadge = () => { + if (!selectedBadge && !authorised) 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}`} /> {#if loadError} - <Popup fullscreen locked> - {loadError} - </Popup> + <Popup fullscreen locked> + {loadError} + </Popup> {:else} - {@const isOwner = $identity && (isId ? $identity.id : $identity.name) === data.username} - - {#if $BadgeWallUser.fetching || !$BadgeWallUser.data} - <Message message="Loading badges ..." /> - - <Skeleton grid={true} count={100} width="150px" height="170px" /> - {:else} - {@const ungroupedBadges = castBadgesToIndexedBadges($BadgeWallUser.data.User.badges)} - {@const isBadgeSelected = - selectedBadge && - selectedBadge !== undefined && - selectedBadge.image && - selectedBadge.image !== undefined && - !editMode} - - <div id="badges"> - {#if preferences && !preferences.hide_awc_badges} - <AWC {awcPromise} {categoryFilter} {isOwner} {preferences} /> - {/if} - - {#if ungroupedBadges === null} - <Message message="Loading badges ..." /> - - <Skeleton grid={true} count={10} width="150px" height="170px" /> - {:else} - {@const groupedBadges = Object.entries( - groupBadges(removeHiddenBadges(isOwner, ungroupedBadges)) - )} - - {#if isOwner || authorised} - {@const shadowHiddenCount = ungroupedBadges.filter((badge) => badge.shadow_hidden).length} - {@const shadowHidden = shadowHiddenCount > 0} - - {#if shadowHidden} - <div class="card"> - <b>Notice:</b> The Badge Wall overseer system has detected badges containing - AI-generated material on your wall. {shadowHiddenCount} of your badges have been shadow - hidden. - <p /> - You may use the "Un-shadow Hide Badges" button to unhide these badges, from where you will - 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} - <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 - elements, kindly use the hide feature to hide these badges from the public, while - allowing them to stay visible to you as the account holder. - <p /> - Failure to comply with this request at your earliest convenience will result in the hiding - of all badges from your Badge Wall. - <p /> - <button - on:click={() => { - noticeDismissed = true; - - localStorage.setItem('badgeWallNoticeDismissed', 'true'); - }} - > - Dismiss - </button> - </div> - {/if} - - <p /> - - <div class="card"> - {#if authorised} - <button on:click={setShadowHide}>Shadow Hide Badges</button> - {/if} - - {#if isOwner && authorised} - <span style="margin: 0 0.625rem;">•</span> - {/if} - - {#if isOwner} - <button - on:click={() => { - selectedBadge = undefined; - editMode = !editMode; - }} - > - {editMode - ? $locale().user.badges.editMode.disable - : $locale().user.badges.editMode.enable} - </button> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - selectedBadge = undefined; - importMode = !importMode; - }} - > - {importMode - ? $locale().user.badges.importMode.disable - : $locale().user.badges.importMode.enable} - </button> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - selectedBadge = undefined; - migrateMode = !migrateMode; - }} - > - Migrate Category - </button> - <span style="margin: 0 0.625rem;">•</span> - <button - on:click={() => { - selectedBadge = undefined; - hideMode = !hideMode; - }} - > - Hide Category - </button> - <!-- <!-- <span style="margin: 0 0.625rem;">•</span> --> - <!-- <button on:click={() => exportBadges(groupedBadges)}>Export Badges</button> --> - - {#if shadowHidden} - <span style="margin: 0 0.625rem;">•</span> - <button on:click={setShadowHide}>Un-shadow Hide Badges</button> - {/if} - - {#if editMode && isOwner} - {@const groups = groupedBadges - .map((group) => group[0]) - .filter((group) => group !== 'Uncategorised')} - {@const designers = castAsStringArray([ - ...new Set( - ungroupedBadges - .map((badge) => badge.designer) - .filter((designer) => designer !== undefined && designer !== null) - .filter( - (designer, index, array) => - array.indexOf(designer) === index && !array.includes(`@${designer}`) - ) - ) - ])} - - <p /> - - {#if error} - <p style="color: red;">{error}</p> - {/if} - - <input - type="text" - placeholder={$locale().user.badges.editMode.imageURL} - name="image_url" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge ? selectedBadge.image : ''} - /> - <input - type="text" - placeholder={$locale().user.badges.editMode.activityURL} - name="activity_url" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge - ? selectedBadge.post === '#' - ? '' - : selectedBadge.post - : ''} - /> - <input - type="text" - placeholder={$locale().user.badges.editMode.description} - name="description" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge ? selectedBadge.description : ''} - /> - <Dropdown - items={groups.map((group) => ({ - name: group, - url: '#', - onClick: () => { - const category = document.querySelector('input[name="category"]'); - - if (category instanceof HTMLInputElement) category.value = group; - } - }))} - header={false} - center={false} - > - <span slot="title"> - <input - type="text" - placeholder={$locale().user.badges.editMode.category} - name="category" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge - ? selectedBadge.category === 'Uncategorised' - ? '' - : selectedBadge.category - : ''} - list="categories" - /> - </span> - </Dropdown> - <span style="float: right;"> - <input - type="datetime-local" - value={selectedBadge && selectedBadge.time - ? dateToInputTime(databaseTimeToDate(selectedBadge.time)) - : ''} - /> - <small>Must be full date and time, defaults to now if any fields empty</small> - </span> - - <p /> - - <div class="edit-row-2"> - <input - type="text" - placeholder={$locale().user.badges.editMode.source} - name="source" - minlength="1" - maxlength="1000" - size="16" - value={selectedBadge ? selectedBadge.source : ''} - /> - <Dropdown - items={designers.map((designer) => ({ - name: designer, - url: '#', - onClick: () => { - const designerField = document.querySelector('input[name="designer"]'); - - if (designerField instanceof HTMLInputElement) - designerField.value = designer; - } - }))} - header={false} - center={false} - > - <span slot="title"> - <input - type="text" - placeholder={$locale().user.badges.editMode.designer} - name="designer" - minlength="1" - maxlength="1000" - size="17" - value={selectedBadge ? selectedBadge.designer : ''} - /> - </span> - </Dropdown> - <Dropdown - items={[false, true].map((hidden) => ({ - name: hidden ? 'Hidden' : 'Shown', - url: '#', - onClick: () => { - const hiddenInput = document.querySelector('input[name="hidden"]'); - - if (hiddenInput instanceof HTMLInputElement) - hiddenInput.value = hidden ? 'Hidden' : 'Shown'; - } - }))} - header={false} - center={false} - > - <span slot="title"> - <input - type="text" - placeholder="Shown" - name="hidden" - minlength="1" - maxlength="1000" - size="15" - value={selectedBadge - ? selectedBadge.hidden - ? 'Hidden' - : 'Shown' - : 'Shown'} - /> - </span> - </Dropdown> - <button class="button-lined" on:click={submitBadge} - >{selectedBadge - ? $locale().user.badges.editMode.update - : $locale().user.badges.editMode.add}</button - > - {#if selectedBadge} - {$locale().user.badges.editMode.or} - <button - class="button-lined" - on:click={() => { - if (selectedBadge) removeBadge(selectedBadge); - }}>{$locale().user.badges.editMode.delete}</button - > - {/if} - </div> - {/if} - {/if} - </div> - {/if} - - <p /> - - <Badges - {ungroupedBadges} - {groupedBadges} - {categoryFilter} - {editMode} - {preferences} - bind:selectedBadge - /> - {/if} - </div> - - {#if isBadgeSelected} - <!-- {@const anyAdjacentBadgeExists = + {@const isOwner = $identity && (isId ? $identity.id : $identity.name) === data.username} + + {#if $BadgeWallUser.fetching || !$BadgeWallUser.data} + <Message message="Loading badges ..." /> + + <Skeleton grid={true} count={100} width="150px" height="170px" /> + {:else} + {@const ungroupedBadges = castBadgesToIndexedBadges($BadgeWallUser.data.User.badges)} + {@const isBadgeSelected = + selectedBadge && + selectedBadge !== undefined && + selectedBadge.image && + selectedBadge.image !== undefined && + !editMode} + + <div id="badges"> + {#if preferences && !preferences.hide_awc_badges} + <AWC {awcPromise} {categoryFilter} {isOwner} {preferences} /> + {/if} + + {#if ungroupedBadges === null} + <Message message="Loading badges ..." /> + + <Skeleton grid={true} count={10} width="150px" height="170px" /> + {:else} + {@const groupedBadges = Object.entries( + groupBadges(removeHiddenBadges(isOwner, ungroupedBadges)) + )} + + {#if isOwner || authorised} + {@const shadowHiddenCount = ungroupedBadges.filter((badge) => badge.shadow_hidden).length} + {@const shadowHidden = shadowHiddenCount > 0} + + {#if shadowHidden} + <div class="card"> + <b>Notice:</b> The Badge Wall overseer system has detected badges containing + AI-generated material on your wall. {shadowHiddenCount} of your badges have been shadow + hidden. + <p /> + You may use the "Un-shadow Hide Badges" button to unhide these badges, from where you will + 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} + <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 + elements, kindly use the hide feature to hide these badges from the public, while + allowing them to stay visible to you as the account holder. + <p /> + Failure to comply with this request at your earliest convenience will result in the hiding + of all badges from your Badge Wall. + <p /> + <button + on:click={() => { + noticeDismissed = true; + + localStorage.setItem('badgeWallNoticeDismissed', 'true'); + }} + > + Dismiss + </button> + </div> + {/if} + + <p /> + + <div class="card"> + {#if authorised} + <button on:click={setShadowHide}>Shadow Hide Badges</button> + {/if} + + {#if isOwner && authorised} + <span style="margin: 0 0.625rem;">•</span> + {/if} + + {#if isOwner} + <button + on:click={() => { + selectedBadge = undefined; + editMode = !editMode; + }} + > + {editMode + ? $locale().user.badges.editMode.disable + : $locale().user.badges.editMode.enable} + </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + selectedBadge = undefined; + importMode = !importMode; + }} + > + {importMode + ? $locale().user.badges.importMode.disable + : $locale().user.badges.importMode.enable} + </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + selectedBadge = undefined; + migrateMode = !migrateMode; + }} + > + Migrate Category + </button> + <span style="margin: 0 0.625rem;">•</span> + <button + on:click={() => { + selectedBadge = undefined; + hideMode = !hideMode; + }} + > + Hide Category + </button> + <!-- <!-- <span style="margin: 0 0.625rem;">•</span> --> + <!-- <button on:click={() => exportBadges(groupedBadges)}>Export Badges</button> --> + + {#if shadowHidden} + <span style="margin: 0 0.625rem;">•</span> + <button on:click={setShadowHide}>Un-shadow Hide Badges</button> + {/if} + + {#if editMode && isOwner} + {@const groups = groupedBadges + .map((group) => group[0]) + .filter((group) => group !== 'Uncategorised')} + {@const designers = castAsStringArray([ + ...new Set( + ungroupedBadges + .map((badge) => badge.designer) + .filter((designer) => designer !== undefined && designer !== null) + .filter( + (designer, index, array) => + array.indexOf(designer) === index && !array.includes(`@${designer}`) + ) + ) + ])} + + <p /> + + {#if error} + <p style="color: red;">{error}</p> + {/if} + + <input + type="text" + placeholder={$locale().user.badges.editMode.imageURL} + name="image_url" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge ? selectedBadge.image : ''} + /> + <input + type="text" + placeholder={$locale().user.badges.editMode.activityURL} + name="activity_url" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge + ? selectedBadge.post === '#' + ? '' + : selectedBadge.post + : ''} + /> + <input + type="text" + placeholder={$locale().user.badges.editMode.description} + name="description" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge ? selectedBadge.description : ''} + /> + <Dropdown + items={groups.map((group) => ({ + name: group, + url: '#', + onClick: () => { + const category = document.querySelector('input[name="category"]'); + + if (category instanceof HTMLInputElement) category.value = group; + } + }))} + header={false} + center={false} + > + <span slot="title"> + <input + type="text" + placeholder={$locale().user.badges.editMode.category} + name="category" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge + ? selectedBadge.category === 'Uncategorised' + ? '' + : selectedBadge.category + : ''} + list="categories" + /> + </span> + </Dropdown> + <span style="float: right;"> + <input + type="datetime-local" + value={selectedBadge && selectedBadge.time + ? dateToInputTime(databaseTimeToDate(selectedBadge.time)) + : ''} + /> + <small>Must be full date and time, defaults to now if any fields empty</small> + </span> + + <p /> + + <div class="edit-row-2"> + <input + type="text" + placeholder={$locale().user.badges.editMode.source} + name="source" + minlength="1" + maxlength="1000" + size="16" + value={selectedBadge ? selectedBadge.source : ''} + /> + <Dropdown + items={designers.map((designer) => ({ + name: designer, + url: '#', + onClick: () => { + const designerField = document.querySelector('input[name="designer"]'); + + if (designerField instanceof HTMLInputElement) + designerField.value = designer; + } + }))} + header={false} + center={false} + > + <span slot="title"> + <input + type="text" + placeholder={$locale().user.badges.editMode.designer} + name="designer" + minlength="1" + maxlength="1000" + size="17" + value={selectedBadge ? selectedBadge.designer : ''} + /> + </span> + </Dropdown> + <Dropdown + items={[false, true].map((hidden) => ({ + name: hidden ? 'Hidden' : 'Shown', + url: '#', + onClick: () => { + const hiddenInput = document.querySelector('input[name="hidden"]'); + + if (hiddenInput instanceof HTMLInputElement) + hiddenInput.value = hidden ? 'Hidden' : 'Shown'; + } + }))} + header={false} + center={false} + > + <span slot="title"> + <input + type="text" + placeholder="Shown" + name="hidden" + minlength="1" + maxlength="1000" + size="15" + value={selectedBadge + ? selectedBadge.hidden + ? 'Hidden' + : 'Shown' + : 'Shown'} + /> + </span> + </Dropdown> + <button class="button-lined" on:click={submitBadge} + >{selectedBadge + ? $locale().user.badges.editMode.update + : $locale().user.badges.editMode.add}</button + > + {#if selectedBadge} + {$locale().user.badges.editMode.or} + <button + class="button-lined" + on:click={() => { + if (selectedBadge) removeBadge(selectedBadge); + }}>{$locale().user.badges.editMode.delete}</button + > + {/if} + </div> + {/if} + {/if} + </div> + {/if} + + <p /> + + <Badges + {ungroupedBadges} + {groupedBadges} + {categoryFilter} + {editMode} + {preferences} + bind:selectedBadge + /> + {/if} + </div> + + {#if isBadgeSelected} + <!-- {@const anyAdjacentBadgeExists = adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) || adjacentBadgeExists(selectedBadge, ungroupedBadges, 1)} --> - <Popup - fullscreen - show={isBadgeSelected} - onLeave={() => { - selectedBadge = undefined; - }} - > - <BadgePreview - bind:selectedBadge - onNext={() => setAdjacentCursor(ungroupedBadges, 1)} - onPrevious={() => setAdjacentCursor(ungroupedBadges, -1)} - hasNext={adjacentBadgeExists(selectedBadge, ungroupedBadges, 1) !== undefined} - hasPrevious={adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) !== undefined} - /> - - {#if authorised} - <button on:click={shadowHideBadge}> - {#if selectedBadge && selectedBadge.shadow_hidden} - Un-shadow - {:else} - Shadow - {/if} Hide Badge ({selectedBadge ? selectedBadge.id : 0}) - </button> - {/if} - </Popup> - {/if} - {/if} + <Popup + fullscreen + show={isBadgeSelected} + onLeave={() => { + selectedBadge = undefined; + }} + > + <BadgePreview + bind:selectedBadge + onNext={() => setAdjacentCursor(ungroupedBadges, 1)} + onPrevious={() => setAdjacentCursor(ungroupedBadges, -1)} + hasNext={adjacentBadgeExists(selectedBadge, ungroupedBadges, 1) !== undefined} + hasPrevious={adjacentBadgeExists(selectedBadge, ungroupedBadges, -1) !== undefined} + /> + + {#if authorised} + <button on:click={shadowHideBadge}> + {#if selectedBadge && selectedBadge.shadow_hidden} + Un-shadow + {:else} + Shadow + {/if} Hide Badge ({selectedBadge ? selectedBadge.id : 0}) + </button> + {/if} + </Popup> + {/if} + {/if} {/if} {#if true} - <Popup fullscreen onLeave={() => (importMode = false)} show={importMode}> - {$locale().user.badges.importMode.title} - - <p /> - - <input - type="text" - placeholder={$locale().user.badges.editMode.activityURL} - id="import_activity_url" - minlength="1" - maxlength="1000" - size="20" - /> - <input - type="text" - placeholder={$locale().user.badges.editMode.category} - id="import_category" - minlength="1" - maxlength="1000" - size="20" - /> - - <p /> - - <input type="checkbox" id="import_links" name="import_links" bind:checked={importLinks} /> - {$locale().user.badges.importMode.importLinks.title} - <SettingHint lineBreak> - {$locale().user.badges.importMode.importLinks.hint} - </SettingHint> - - <p /> - - <input type="checkbox" id="import_links" name="import_links" bind:checked={importReplies} /> - {$locale().user.badges.importMode.importReplies} - - <p /> - - <button - on:click={() => { - importMode = false; - importImages = undefined; - }} - class="button-lined" - > - {$locale().user.badges.importMode.cancel} - </button> - <button on:click={() => parsePost()} class="button-lined" style="float: right;"> - {$locale().user.badges.importMode.fetch} - </button> - - <p /> - - <details> - <summary>{$locale().user.badges.importMode.dangerous}</summary> - - <button - class="button-lined no-shadow" - data-umami-event="Remove All Badges" - on:click={removeAllBadges} - > - {$locale({ - values: { - times: 3 - confirmPrune - } - }).user.badges.importMode.deleteAll.title} - </button> - <SettingHint lineBreak> - {$locale().user.badges.importMode.deleteAll.hint} - </SettingHint> - </details> - - {#if importImages && importImages.length > 0} - <p /> - - {$locale({ - values: { - count: importImages.length - } - }).user.badges.importMode.importConfirm} - <button - on:click={() => importBadges()} - class="button-lined no-shadow" - data-umami-event="Import Badges" - > - {$locale().user.badges.importMode.import} - </button> - - <SettingHint lineBreak> - {$locale().user.badges.importMode.importWait} - </SettingHint> - {/if} - </Popup> + <Popup fullscreen onLeave={() => (importMode = false)} show={importMode}> + {$locale().user.badges.importMode.title} + + <p /> + + <input + type="text" + placeholder={$locale().user.badges.editMode.activityURL} + id="import_activity_url" + minlength="1" + maxlength="1000" + size="20" + /> + <input + type="text" + placeholder={$locale().user.badges.editMode.category} + id="import_category" + minlength="1" + maxlength="1000" + size="20" + /> + + <p /> + + <input type="checkbox" id="import_links" name="import_links" bind:checked={importLinks} /> + {$locale().user.badges.importMode.importLinks.title} + <SettingHint lineBreak> + {$locale().user.badges.importMode.importLinks.hint} + </SettingHint> + + <p /> + + <input type="checkbox" id="import_links" name="import_links" bind:checked={importReplies} /> + {$locale().user.badges.importMode.importReplies} + + <p /> + + <button + on:click={() => { + importMode = false; + importImages = undefined; + }} + class="button-lined" + > + {$locale().user.badges.importMode.cancel} + </button> + <button on:click={() => parsePost()} class="button-lined" style="float: right;"> + {$locale().user.badges.importMode.fetch} + </button> + + <p /> + + <details> + <summary>{$locale().user.badges.importMode.dangerous}</summary> + + <button + class="button-lined no-shadow" + data-umami-event="Remove All Badges" + on:click={removeAllBadges} + > + {$locale({ + values: { + times: 3 - confirmPrune + } + }).user.badges.importMode.deleteAll.title} + </button> + <SettingHint lineBreak> + {$locale().user.badges.importMode.deleteAll.hint} + </SettingHint> + </details> + + {#if importImages && importImages.length > 0} + <p /> + + {$locale({ + values: { + count: importImages.length + } + }).user.badges.importMode.importConfirm} + <button + on:click={() => importBadges()} + class="button-lined no-shadow" + data-umami-event="Import Badges" + > + {$locale().user.badges.importMode.import} + </button> + + <SettingHint lineBreak> + {$locale().user.badges.importMode.importWait} + </SettingHint> + {/if} + </Popup> {/if} <Popup fullscreen onLeave={() => (migrateMode = false)} show={migrateMode}> - Migrate Category - - <p /> - - <input - type="text" - placeholder="Original Category" - id="migrate_original" - minlength="1" - maxlength="1000" - size="20" - /> - <input - type="text" - placeholder="New Category" - id="migrate_new" - minlength="1" - maxlength="1000" - size="20" - /> - <SettingHint lineBreak>Leave category empty to migrate all to or from uncategorised.</SettingHint> - - <p /> - - <button - on:click={() => { - importMode = false; - importImages = undefined; - }} - class="button-lined" - > - {$locale().user.badges.importMode.cancel} - </button> - <button on:click={() => migrateCategory()} class="button-lined" style="float: right;"> - Migrate - </button> + Migrate Category + + <p /> + + <input + type="text" + placeholder="Original Category" + id="migrate_original" + minlength="1" + maxlength="1000" + size="20" + /> + <input + type="text" + placeholder="New Category" + id="migrate_new" + minlength="1" + maxlength="1000" + size="20" + /> + <SettingHint lineBreak>Leave category empty to migrate all to or from uncategorised.</SettingHint> + + <p /> + + <button + on:click={() => { + importMode = false; + importImages = undefined; + }} + class="button-lined" + > + {$locale().user.badges.importMode.cancel} + </button> + <button on:click={() => migrateCategory()} class="button-lined" style="float: right;"> + Migrate + </button> </Popup> <Popup fullscreen onLeave={() => (hideMode = false)} show={hideMode}> - Hide Category - - <SettingHint lineBreak> - If the majority of the badges in a category are shown, the category will be hidden, and vice - versa. - </SettingHint> - - <p /> - - <input - type="text" - placeholder="Category" - id="category_hide" - minlength="1" - maxlength="1000" - size="20" - /> - <SettingHint lineBreak>Leave category field empty to hide all.</SettingHint> - - <p /> - - <button - on:click={() => { - hideMode = false; - importImages = undefined; - }} - class="button-lined" - > - {$locale().user.badges.importMode.cancel} - </button> - <button on:click={() => hideCategory()} class="button-lined" style="float: right;" - >Toggle Visibility</button - > + Hide Category + + <SettingHint lineBreak> + If the majority of the badges in a category are shown, the category will be hidden, and vice + versa. + </SettingHint> + + <p /> + + <input + type="text" + placeholder="Category" + id="category_hide" + minlength="1" + maxlength="1000" + size="20" + /> + <SettingHint lineBreak>Leave category field empty to hide all.</SettingHint> + + <p /> + + <button + on:click={() => { + hideMode = false; + importImages = undefined; + }} + class="button-lined" + > + {$locale().user.badges.importMode.cancel} + </button> + <button on:click={() => hideCategory()} class="button-lined" style="float: right;" + >Toggle Visibility</button + > </Popup> diff --git a/src/routes/welcome/+page.svelte b/src/routes/welcome/+page.svelte index 296ce30e..603e1305 100644 --- a/src/routes/welcome/+page.svelte +++ b/src/routes/welcome/+page.svelte @@ -1,5 +1,5 @@ <script> - import Landing from '$lib/Landing.svelte'; + import Landing from '$lib/Landing.svelte'; </script> <Landing /> |