aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/cdn/index.ts14
-rw-r--r--apps/cdn/wrangler.toml2
-rw-r--r--apps/proxy/src/index.js13
-rw-r--r--apps/proxy/wrangler.toml2
-rw-r--r--package.json3
-rw-r--r--pnpm-lock.yaml11
-rw-r--r--src/lib/CommandPalette/actions.ts18
-rw-r--r--src/lib/Data/AniList/media.ts35
-rw-r--r--src/lib/Data/AniList/schedule.ts55
-rw-r--r--src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte3
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte22
-rw-r--r--src/lib/List/Anime/DueAnimeList.svelte22
-rw-r--r--src/lib/List/Anime/UpcomingAnimeList.svelte36
-rw-r--r--src/lib/List/Manga/CleanMangaList.svelte20
-rw-r--r--src/lib/List/Manga/MangaListTemplate.svelte22
-rw-r--r--src/lib/Locale/english.ts4
-rw-r--r--src/lib/Locale/japanese.ts4
-rw-r--r--src/lib/Locale/layout.ts1
-rw-r--r--src/lib/Media/invalidate.ts18
-rw-r--r--src/lib/Notification/Notification.svelte6
-rw-r--r--src/lib/Settings/Categories/Debug.svelte16
-rw-r--r--src/lib/Settings/Categories/Display.svelte1
-rw-r--r--src/lib/Tools/FollowFix.svelte8
-rw-r--r--src/lib/Tools/Wrapped/Tool.svelte4
-rw-r--r--src/lib/Tooltip/LinkedTooltip.svelte15
-rw-r--r--src/lib/Tooltip/tooltip.ts10
-rw-r--r--src/lib/User/BadgeWall/BadgePreview.svelte2
-rw-r--r--src/lib/Utility/appOrigin.ts1
-rw-r--r--src/routes/+page.svelte282
-rw-r--r--src/routes/api/badges/+server.ts25
-rw-r--r--src/routes/api/subsplease/+server.ts11
-rw-r--r--src/stores/revalidateAnime.ts2
-rw-r--r--src/stores/revalidateManga.ts5
-rw-r--r--src/stores/settings.ts65
-rw-r--r--src/trigger/notifications.ts6
35 files changed, 531 insertions, 233 deletions
diff --git a/apps/cdn/index.ts b/apps/cdn/index.ts
index e0d63db5..63b6e798 100644
--- a/apps/cdn/index.ts
+++ b/apps/cdn/index.ts
@@ -99,14 +99,16 @@ const handleRequest = async (originalRequest) => {
// );
// }
+ const responseHeaders = new Headers(response.headers);
+ responseHeaders.set(
+ "Cache-Control",
+ "public, immutable, s-maxage=31536000, max-age=31536000, stale-while-revalidate=60",
+ );
+ responseHeaders.set("Access-Control-Allow-Origin", "https://due.moe");
+
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
- headers: {
- "Cache-Control":
- "public, immutable, s-maxage=31536000, max-age=31536000, stale-while-revalidate=60",
- "Access-Control-Allow-Origin": "https://due.moe",
- ...response.headers,
- },
+ headers: responseHeaders,
});
};
diff --git a/apps/cdn/wrangler.toml b/apps/cdn/wrangler.toml
index adb3e071..fcb5cdff 100644
--- a/apps/cdn/wrangler.toml
+++ b/apps/cdn/wrangler.toml
@@ -1,4 +1,4 @@
name = "cdn"
main = "./index.ts"
-compatibility_date = "2023-10-30"
+compatibility_date = "2025-01-01"
diff --git a/apps/proxy/src/index.js b/apps/proxy/src/index.js
index 592899c0..e6ed8690 100644
--- a/apps/proxy/src/index.js
+++ b/apps/proxy/src/index.js
@@ -18,6 +18,7 @@ const isPrivateHostname = (hostname) =>
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname.endsWith(".local") ||
+ hostname.endsWith(".localhost") ||
/^10\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
@@ -241,7 +242,7 @@ const recommendedVolumeText = (volumeChapterBoundaries, progress) => {
return recommended;
};
-const handleMangaChapterCounts = async (request, env, ctx) => {
+const handleMangaChapterCounts = async (request, env, executionContext) => {
if (!hasSupabaseConfig(env))
return jsonResponse(
request,
@@ -299,7 +300,7 @@ const handleMangaChapterCounts = async (request, env, ctx) => {
);
if (queueableRows.length)
- ctx.waitUntil(
+ executionContext.waitUntil(
Promise.all(queueBootstrap(env, queueableRows)).catch((error) => {
if (!isMangadexIdConstraintConflict(error)) throw error;
}),
@@ -374,14 +375,14 @@ const handleMangaSync = async (request, env) => {
};
export default {
- async fetch(request, env, ctx) {
+ async fetch(request, env, executionContext) {
try {
const url = new URL(request.url);
if (request.method === "OPTIONS") return handleOptions(request);
if (url.pathname === "/manga/chapter-counts" && request.method === "POST")
- return handleMangaChapterCounts(request, env, ctx);
+ return handleMangaChapterCounts(request, env, executionContext);
if (
url.pathname === "/manga/native-chapter-counts" &&
@@ -408,9 +409,9 @@ export default {
}
},
- async scheduled(_controller, env, ctx) {
+ async scheduled(_controller, env, executionContext) {
if (!hasSupabaseConfig(env)) return;
- ctx.waitUntil(syncMangadexIndex(env));
+ executionContext.waitUntil(syncMangadexIndex(env));
},
};
diff --git a/apps/proxy/wrangler.toml b/apps/proxy/wrangler.toml
index 1115beeb..b86f92d6 100644
--- a/apps/proxy/wrangler.toml
+++ b/apps/proxy/wrangler.toml
@@ -1,6 +1,6 @@
name = "due-proxy"
main = "src/index.js"
-compatibility_date = "2023-12-18"
+compatibility_date = "2025-01-01"
[triggers]
crons = ["* * * * *"]
diff --git a/package.json b/package.json
index 058298c1..cf138b81 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "dev": "vite dev",
+ "dev": "PUBLIC_APP_ORIGIN=https://due.localhost portless due vite dev",
"graphql:generate": "pnpm exec sveltekit-graphql generate",
"build": "pnpm run graphql:generate && vite build",
"preview": "vite preview",
@@ -37,6 +37,7 @@
"graphql": "^16.13.0",
"houdini": "^2.0.0-next.11",
"houdini-svelte": "^2.1.20",
+ "portless": "^0.10.3",
"sass": "^1.69.7",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6dbd0cf3..e8e3e20a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -114,6 +114,9 @@ importers:
houdini-svelte:
specifier: ^2.1.20
+ portless:
+ specifier: ^0.10.3
+ version: 0.10.3
sass:
specifier: ^1.69.7
version: 1.97.3
@@ -3698,6 +3701,12 @@ packages:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
+ resolution: {integrity: sha512-lXbVUsNiwWMKJGMi49HhTMW0lS7u0EzawonDZQ+yglSnHioiQK18Y4Fe0Ilk+TuXej71E/nTOHIFIBjUGvTm9w==}
+ engines: {node: '>=20'}
+ os: [darwin, linux, win32]
+ hasBin: true
+
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -8193,6 +8202,8 @@ snapshots:
+
diff --git a/src/lib/CommandPalette/actions.ts b/src/lib/CommandPalette/actions.ts
index 01259130..9915fe66 100644
--- a/src/lib/CommandPalette/actions.ts
+++ b/src/lib/CommandPalette/actions.ts
@@ -1,3 +1,5 @@
+import { invalidateListCaches } from "$lib/Media/invalidate";
+
export interface CommandPaletteAction {
name: string;
url: string;
@@ -165,4 +167,20 @@ export const defaultActions: CommandPaletteAction[] = [
url: "/user?badges=1",
tags: ["user", "me", "settings"],
},
+ {
+ name: "Refresh Anime & Manga List Caches",
+ url: "",
+ preventDefault: true,
+ tags: [
+ "cache",
+ "clear",
+ "refresh",
+ "invalidate",
+ "debug",
+ "anime",
+ "manga",
+ "list",
+ ],
+ onClick: invalidateListCaches,
+ },
];
diff --git a/src/lib/Data/AniList/media.ts b/src/lib/Data/AniList/media.ts
index 099d4182..b9e0ef3f 100644
--- a/src/lib/Data/AniList/media.ts
+++ b/src/lib/Data/AniList/media.ts
@@ -429,23 +429,24 @@ export const mediaListCollection = async (
export const publicMediaListCollection = async (
userId: number,
type: Type,
-): Promise<Media[]> =>
- flattenLists(
- (
- await (
- await fetch("https://graphql.anilist.co", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json",
- },
- body: JSON.stringify({
- query: collectionQueryTemplate(type, userId, {}),
- }),
- })
- ).json()
- )["data"]["MediaListCollection"]["lists"],
- );
+): Promise<Media[]> => {
+ const response = await (
+ await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ query: collectionQueryTemplate(type, userId, {}),
+ }),
+ })
+ ).json();
+
+ if (!response?.data?.MediaListCollection?.lists) return [];
+
+ return flattenLists(response.data.MediaListCollection.lists);
+};
const countMedian = (guesses: number[]) => {
guesses.sort((a: number, b: number) => a - b);
diff --git a/src/lib/Data/AniList/schedule.ts b/src/lib/Data/AniList/schedule.ts
index eb70a03b..474f0e92 100644
--- a/src/lib/Data/AniList/schedule.ts
+++ b/src/lib/Data/AniList/schedule.ts
@@ -57,28 +57,32 @@ const schedulePage = async (
type Season = "WINTER" | "SPRING" | "SUMMER" | "FALL";
-export const scheduleMediaListCollection = async (
+const collectAllSchedulePages = async (
year: number,
season: Season,
- includeLastSeason = false,
+ into: SchedulePage["data"]["Page"]["media"],
) => {
- const scheduledMedia = [];
let page = 1;
- let currentPage = await schedulePage(page, year, season);
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
+ while (true) {
+ const currentPage = await schedulePage(page, year, season);
- while (currentPage["data"]["Page"]["pageInfo"]["hasNextPage"]) {
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
+ for (const candidate of currentPage.data.Page.media) into.push(candidate);
+
+ if (!currentPage.data.Page.pageInfo.hasNextPage) break;
page += 1;
- currentPage = await schedulePage(page, year, season);
}
+};
+
+export const scheduleMediaListCollection = async (
+ year: number,
+ season: Season,
+ includeLastSeason = false,
+) => {
+ const scheduledMedia: SchedulePage["data"]["Page"]["media"] = [];
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
+ await collectAllSchedulePages(year, season, scheduledMedia);
if (includeLastSeason) {
const lastSeason = {
@@ -86,34 +90,11 @@ export const scheduleMediaListCollection = async (
SPRING: "WINTER",
SUMMER: "SPRING",
FALL: "SUMMER",
- }[season];
+ }[season] as Season;
const lastSeasonYear = season === "WINTER" ? year - 1 : year;
- let page = 1;
- let currentPage = await schedulePage(
- page,
- lastSeasonYear,
- lastSeason as Season,
- );
-
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
-
- while (currentPage["data"]["Page"]["pageInfo"]["hasNextPage"]) {
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
-
- page += 1;
- currentPage = await schedulePage(
- page,
- lastSeasonYear,
- lastSeason as Season,
- );
- }
-
- for (const candidate of currentPage.data.Page.media)
- scheduledMedia.push(candidate);
+ await collectAllSchedulePages(lastSeasonYear, lastSeason, scheduledMedia);
}
return scheduledMedia as Partial<Media[]>;
diff --git a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte
index c48003d7..d717f5d0 100644
--- a/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte
+++ b/src/lib/Events/AniListBadges/EasterEvent2025/EasterEgg.svelte
@@ -59,9 +59,6 @@ onMount(() => {
return () => {
if (intervalId) clearInterval(intervalId);
-
- window.removeEventListener("resize", updatePosition);
- window.removeEventListener("scroll", updatePosition);
};
});
diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte
index 808a8e5b..76701a93 100644
--- a/src/lib/List/Anime/CleanAnimeList.svelte
+++ b/src/lib/List/Anime/CleanAnimeList.svelte
@@ -90,6 +90,20 @@ $: lists = Array.from(
),
);
+$: hasDistinguishingList = lists.some((list) =>
+ media.some((m) => !m.mediaListEntry?.customLists?.[list]),
+);
+
+$: if (
+ lists.length > 0 &&
+ selectedList !== "All" &&
+ !lists.includes(selectedList)
+) {
+ selectedList = "All";
+
+ if (!disableFilter) $stateBin[filterKey] = "All";
+}
+
$: filteredMedia =
selectedList === "All" || !$settings.displayMediaListFilter
? media
@@ -193,7 +207,7 @@ onDestroy(() => clearAiringRefreshTimeout());
const increment = (anime: Media, progress: number) => {
if (!dummy && pendingUpdate !== anime.id) {
- $revalidateAnime = true;
+ $revalidateAnime = $revalidateAnime + 1;
lastUpdatedMedia = anime.id;
pendingUpdate = anime.id;
@@ -224,9 +238,9 @@ const increment = (anime: Media, progress: number) => {
: filteredMedia.length}
{title}
hideTime={dummy}
- hideCount={dummy}
+ hideCount={dummy || media.length === 0}
>
- {#if $settings.displayMediaRoulette && !upcoming && !notYetReleased && filteredMedia.length > 0}
+ {#if $settings.displayMediaRoulette && !upcoming && !notYetReleased && filteredMedia.length > 1}
<button
class="small-button"
onclick={() => (showRoulette = true)}
@@ -241,7 +255,7 @@ const increment = (anime: Media, progress: number) => {
No anime to display. <button onclick={() => (animeLists = cleanCache(user, $identity))}>
Force refresh
</button>
-{:else if $settings.displayMediaListFilter && !disableFilter}
+{:else if $settings.displayMediaListFilter && !disableFilter && hasDistinguishingList}
<select value={selectedList} onchange={updateSelectedList}>
<option value="All">All</option>
diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte
index 2c707ffb..d2c47ebe 100644
--- a/src/lib/List/Anime/DueAnimeList.svelte
+++ b/src/lib/List/Anime/DueAnimeList.svelte
@@ -16,6 +16,7 @@ import {
import { addNotification } from "$lib/Notification/store";
import locale from "$stores/locale";
import identity from "$stores/identity";
+import revalidateAnime from "$stores/revalidateAnime";
export let user: AniListAuthorisation;
let animeLists: Promise<Media[]>;
@@ -68,6 +69,27 @@ onMount(async () => {
$: if (keyCacher && keyCacheMinutes !== $settings.cacheMinutes)
restartKeyCacher($settings.cacheMinutes);
+let lastAnimeRevalidation = 0;
+
+$: if ($revalidateAnime > lastAnimeRevalidation) {
+ lastAnimeRevalidation = $revalidateAnime;
+
+ startTime = performance.now();
+ endTime = -1;
+
+ animeLists = mediaListCollection(
+ user,
+ $identity,
+ Type.Anime,
+ $anime,
+ $lastPruneTimes.anime,
+ {
+ forcePrune: true,
+ addNotification,
+ },
+ );
+}
+
onDestroy(() => {
if (keyCacher) clearInterval(keyCacher);
});
diff --git a/src/lib/List/Anime/UpcomingAnimeList.svelte b/src/lib/List/Anime/UpcomingAnimeList.svelte
index d9b91122..ed47f22f 100644
--- a/src/lib/List/Anime/UpcomingAnimeList.svelte
+++ b/src/lib/List/Anime/UpcomingAnimeList.svelte
@@ -96,22 +96,26 @@ const cleanMedia = (
return upcomingAnime;
};
-$: {
- if ($revalidateAnime) {
- $revalidateAnime = false;
- $lastPruneTimes.anime = -1;
- animeLists = mediaListCollection(
- user,
- $identity,
- Type.Anime,
- $anime,
- $lastPruneTimes.anime,
- {
- addNotification,
- notificationType: "Upcoming Episodes",
- },
- );
- }
+let lastAnimeRevalidation = 0;
+
+$: if ($revalidateAnime > lastAnimeRevalidation) {
+ lastAnimeRevalidation = $revalidateAnime;
+
+ startTime = performance.now();
+ endTime = -1;
+
+ animeLists = mediaListCollection(
+ user,
+ $identity,
+ Type.Anime,
+ $anime,
+ $lastPruneTimes.anime,
+ {
+ forcePrune: true,
+ addNotification,
+ notificationType: "Upcoming Episodes",
+ },
+ );
}
</script>
diff --git a/src/lib/List/Manga/CleanMangaList.svelte b/src/lib/List/Manga/CleanMangaList.svelte
index 497e2ae6..a52a9d7e 100644
--- a/src/lib/List/Manga/CleanMangaList.svelte
+++ b/src/lib/List/Manga/CleanMangaList.svelte
@@ -83,6 +83,20 @@ $: lists = Array.from(
),
);
+$: hasDistinguishingList = lists.some((list) =>
+ media.some((m) => !m.mediaListEntry?.customLists?.[list]),
+);
+
+$: if (
+ lists.length > 0 &&
+ selectedList !== "All" &&
+ !lists.includes(selectedList)
+) {
+ selectedList = "All";
+
+ if (!disableFilter) $stateBin[filterKey] = "All";
+}
+
$: filteredMedia =
selectedList === "All" || !$settings.displayMediaListFilter
? media
@@ -126,7 +140,7 @@ const increment = (manga: Media) => {
? $locale().lists.due.mangaAndLightNovels
: $locale().lists.completed.mangaAndLightNovels}
hideTime={dummy}
- hideCount={dummy}
+ hideCount={dummy || media.length === 0}
>
{#if !dummy}
<button
@@ -135,7 +149,7 @@ const increment = (manga: Media) => {
onclick={cleanCache}
data-umami-event="Force Refresh Manga">Refresh</button
>
- {#if $settings.displayMediaRoulette && filteredMedia.length > 0}
+ {#if $settings.displayMediaRoulette && filteredMedia.length > 1}
<button
class="small-button"
onclick={() => (showRoulette = true)}
@@ -189,7 +203,7 @@ const increment = (manga: Media) => {
>
You can re-enable it later in the <a href={root('/settings')}>Settings</a>.
</span>
-{:else if $settings.displayMediaListFilter && !disableFilter}
+{:else if $settings.displayMediaListFilter && !disableFilter && hasDistinguishingList}
<select value={selectedList} onchange={updateSelectedList}>
<option value="All">All</option>
diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte
index 52649098..df894910 100644
--- a/src/lib/List/Manga/MangaListTemplate.svelte
+++ b/src/lib/List/Manga/MangaListTemplate.svelte
@@ -19,6 +19,7 @@ import identity from "$stores/identity";
import lastPruneTimes from "$stores/lastPruneTimes";
import locale from "$stores/locale";
import manga from "$stores/manga";
+import revalidateManga from "$stores/revalidateManga";
import settings from "$stores/settings";
import ListTitle from "../ListTitle.svelte";
import CleanMangaList from "./CleanMangaList.svelte";
@@ -141,6 +142,27 @@ $: if (
)
restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5));
+let lastMangaRevalidation = 0;
+
+$: if (!dummy && $revalidateManga > lastMangaRevalidation) {
+ lastMangaRevalidation = $revalidateManga;
+
+ startTime = performance.now();
+ endTime = -1;
+
+ mangaLists = mediaListCollection(
+ user,
+ $identity,
+ Type.Manga,
+ $manga,
+ $lastPruneTimes.manga,
+ {
+ forcePrune: true,
+ addNotification,
+ },
+ );
+}
+
onDestroy(() => {
if (keyCacher) clearInterval(keyCacher);
});
diff --git a/src/lib/Locale/english.ts b/src/lib/Locale/english.ts
index 97d00a4b..9918d98f 100644
--- a/src/lib/Locale/english.ts
+++ b/src/lib/Locale/english.ts
@@ -108,6 +108,8 @@ const English: Locale = {
},
tooltips: {
beta: "Beta",
+ dataSaver:
+ "Use smaller images and lighter data paths where possible to reduce bandwidth usage.",
},
},
debug: {
@@ -269,7 +271,7 @@ const English: Locale = {
},
},
debug: {
- clearCaches: "Clear anime and manga list caches",
+ clearCaches: "Invalidate anime and manga list caches",
showListTimings: "Show media list timings",
resetAllSettings: {
title: "Reset ALL settings",
diff --git a/src/lib/Locale/japanese.ts b/src/lib/Locale/japanese.ts
index b220a986..79e15e96 100644
--- a/src/lib/Locale/japanese.ts
+++ b/src/lib/Locale/japanese.ts
@@ -107,6 +107,8 @@ const Japanese: Locale = {
},
tooltips: {
beta: "ベータ",
+ dataSaver:
+ "可能な場合は小さい画像や軽量なデータ経路を使い、通信量を抑えます。",
},
},
debug: {
@@ -269,7 +271,7 @@ const Japanese: Locale = {
},
},
debug: {
- clearCaches: "ブラウザのAniListアニメと漫画リストのキャッシュを消去する",
+ clearCaches: "ブラウザのAniListアニメと漫画リストのキャッシュを無効化する",
showListTimings: "メディアリストの処理時間を表示する",
resetAllSettings: {
title: "すべての設定をリセット",
diff --git a/src/lib/Locale/layout.ts b/src/lib/Locale/layout.ts
index 08ddc22a..f5149b71 100644
--- a/src/lib/Locale/layout.ts
+++ b/src/lib/Locale/layout.ts
@@ -110,6 +110,7 @@ export interface Locale {
};
tooltips: {
beta: LocaleValue;
+ dataSaver: LocaleValue;
};
};
debug: {
diff --git a/src/lib/Media/invalidate.ts b/src/lib/Media/invalidate.ts
new file mode 100644
index 00000000..3a9ff067
--- /dev/null
+++ b/src/lib/Media/invalidate.ts
@@ -0,0 +1,18 @@
+import { browser } from "$app/environment";
+import { addNotification } from "$lib/Notification/store";
+import { options } from "$lib/Notification/options";
+import revalidateAnime from "$stores/revalidateAnime";
+import revalidateManga from "$stores/revalidateManga";
+
+export const invalidateListCaches = () => {
+ if (!browser) return;
+
+ revalidateAnime.update((n) => n + 1);
+ revalidateManga.update((n) => n + 1);
+
+ addNotification(
+ options({
+ heading: "Anime and manga list caches successfully invalidated",
+ }),
+ );
+};
diff --git a/src/lib/Notification/Notification.svelte b/src/lib/Notification/Notification.svelte
index 76775ff3..a3271d58 100644
--- a/src/lib/Notification/Notification.svelte
+++ b/src/lib/Notification/Notification.svelte
@@ -9,7 +9,11 @@ export let onRemove: () => void = () => {
};
export let removed = false;
-onMount(() => setTimeout(remove, notification.duration));
+onMount(() => {
+ const removeTimer = setTimeout(remove, notification.duration);
+
+ return () => clearTimeout(removeTimer);
+});
const remove = () => {
removed = true;
diff --git a/src/lib/Settings/Categories/Debug.svelte b/src/lib/Settings/Categories/Debug.svelte
index c6b16086..ad388fee 100644
--- a/src/lib/Settings/Categories/Debug.svelte
+++ b/src/lib/Settings/Categories/Debug.svelte
@@ -7,19 +7,7 @@ import { options } from "$lib/Notification/options";
import locale from "$stores/locale";
import SettingCheckboxToggle from "../SettingCheckboxToggle.svelte";
import localforage from "localforage";
-import { browser } from "$app/environment";
-
-const clearCaches = async () => {
- if (!browser) return;
-
- await localforage.removeItem("anime");
- await localforage.removeItem("manga");
- addNotification(
- options({
- heading: "Anime and manga list caches successfully cleared",
- }),
- );
-};
+import { invalidateListCaches } from "$lib/Media/invalidate";
</script>
<SettingCheckboxToggle setting="debugDummyLists" text={$locale().debug.dummyLists} />
@@ -29,7 +17,7 @@ const clearCaches = async () => {
/>
<br />
-<button onclick={clearCaches}>{$locale().debug.clearCaches}</button>
+<button onclick={invalidateListCaches}>{$locale().debug.clearCaches}</button>
<Spacer />
diff --git a/src/lib/Settings/Categories/Display.svelte b/src/lib/Settings/Categories/Display.svelte
index 313e6cf5..8b1f0a43 100644
--- a/src/lib/Settings/Categories/Display.svelte
+++ b/src/lib/Settings/Categories/Display.svelte
@@ -181,6 +181,7 @@ const onHelperChange = () => {
<SettingCheckboxToggle
setting="displayDataSaver"
text={$locale().settings.display.categories.dataSaver}
+ tooltipText={$locale().settings.display.tooltips.dataSaver}
/>
<select bind:value={$settings.displayLanguage} class="no-shadow">
<option value="en">
diff --git a/src/lib/Tools/FollowFix.svelte b/src/lib/Tools/FollowFix.svelte
index c072cf5e..6c599569 100644
--- a/src/lib/Tools/FollowFix.svelte
+++ b/src/lib/Tools/FollowFix.svelte
@@ -26,9 +26,11 @@ let submit = "";
}
}}
/>
- <a href={'#'} onclick={() => (submit = input)}>
- Toggle follow for {input.length === 0 ? '...' : input}
- </a>
+ {#if input.length > 0}
+ <a href={'#'} onclick={() => (submit = input)}>
+ Toggle follow for {input}
+ </a>
+ {/if}
</p>
{#if submit.length > 0}
diff --git a/src/lib/Tools/Wrapped/Tool.svelte b/src/lib/Tools/Wrapped/Tool.svelte
index 4932c2a9..b04bc5f6 100644
--- a/src/lib/Tools/Wrapped/Tool.svelte
+++ b/src/lib/Tools/Wrapped/Tool.svelte
@@ -42,8 +42,8 @@ import LogInRestricted from "$lib/Error/LogInRestricted.svelte";
export let user: AniListAuthorisation;
-const currentYear = new Date(Date.now()).getFullYear();
-let selectedYear = new Date(Date.now()).getFullYear();
+const currentYear = new Date().getFullYear();
+let selectedYear = currentYear;
let episodes = 0;
let chapters = 0;
let minutesWatched = 0;
diff --git a/src/lib/Tooltip/LinkedTooltip.svelte b/src/lib/Tooltip/LinkedTooltip.svelte
index 82147536..bbf0ffe5 100644
--- a/src/lib/Tooltip/LinkedTooltip.svelte
+++ b/src/lib/Tooltip/LinkedTooltip.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import tooltipPosition from "$stores/tooltipPosition";
import { fade } from "svelte/transition";
+import { onDestroy } from "svelte";
export let id: string | undefined = undefined;
export let pin: string | undefined = undefined;
@@ -20,10 +21,24 @@ let hideTimeout: number | null = null;
let debounceTimer: number | null = null;
let opacity = 0;
+onDestroy(() => {
+ if (hideTimeout !== null) clearTimeout(hideTimeout);
+
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
+
+ if (tooltipDiv && tooltipDiv.parentNode === document.body) {
+ document.body.removeChild(tooltipDiv);
+ tooltipDiv = null;
+ }
+});
+
const createTooltip = () => {
if (!tooltipDiv) {
tooltipDiv = document.createElement("div");
tooltipDiv.style.position = "absolute";
+ tooltipDiv.style.top = "-9999px";
+ tooltipDiv.style.left = "-9999px";
+ tooltipDiv.style.visibility = "hidden";
tooltipDiv.style.zIndex = "1000";
opacity = 0;
tooltipDiv.style.pointerEvents = "none";
diff --git a/src/lib/Tooltip/tooltip.ts b/src/lib/Tooltip/tooltip.ts
index 5772c33f..3235cb25 100644
--- a/src/lib/Tooltip/tooltip.ts
+++ b/src/lib/Tooltip/tooltip.ts
@@ -14,12 +14,13 @@ const tooltip = (element: HTMLElement) => {
tooltipDiv = document.createElement("div");
tooltipDiv.style.position = "absolute";
+ tooltipDiv.style.top = "-9999px";
+ tooltipDiv.style.left = "-9999px";
tooltipDiv.style.zIndex = "1000";
tooltipDiv.style.opacity = "0";
- tooltipDiv.style.transition = `opacity ${tooltipTransitionTime}ms ease-in-out, top 0.3s ease, left 0.3s ease`;
+ tooltipDiv.style.transition = `opacity ${tooltipTransitionTime}ms ease-in-out`;
tooltipDiv.style.pointerEvents = "none";
tooltipDiv.style.whiteSpace = "nowrap";
- tooltipDiv.style.zIndex = "1000";
tooltipDiv.classList.add("card");
tooltipDiv.classList.add("card-small");
@@ -81,11 +82,12 @@ const tooltip = (element: HTMLElement) => {
tooltipDiv.innerHTML = content.replace(/\n/g, "<br>");
updateTooltipPosition(x, y);
- setTimeout(() => {
+ requestAnimationFrame(() => {
if (tooltipDiv) {
+ tooltipDiv.style.transition = `opacity ${tooltipTransitionTime}ms ease-in-out, top 0.3s ease, left 0.3s ease`;
tooltipDiv.style.opacity = "1";
}
- }, 10);
+ });
}
};
diff --git a/src/lib/User/BadgeWall/BadgePreview.svelte b/src/lib/User/BadgeWall/BadgePreview.svelte
index fd323564..e651a2aa 100644
--- a/src/lib/User/BadgeWall/BadgePreview.svelte
+++ b/src/lib/User/BadgeWall/BadgePreview.svelte
@@ -118,7 +118,7 @@ const onClick = (event: MouseEvent) => {
<a href={'#'} onclick={onClick} class="badge-container-image">
<ParallaxImage
{source}
- alternativeText="selectedBadge.description"
+ alternativeText={selectedBadge.description ?? ''}
limit={100}
duration={1500}
/>
diff --git a/src/lib/Utility/appOrigin.ts b/src/lib/Utility/appOrigin.ts
index cdc53995..1e2c6708 100644
--- a/src/lib/Utility/appOrigin.ts
+++ b/src/lib/Utility/appOrigin.ts
@@ -11,6 +11,7 @@ const isPrivateHostname = (hostname: string) =>
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname.endsWith(".local") ||
+ hostname.endsWith(".localhost") ||
/^10\./.test(hostname) ||
/^192\.168\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 268e9713..ad3971d8 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import type { Component } from "svelte";
+import { browser } from "$app/environment";
import Spacer from "$lib/Layout/Spacer.svelte";
import { onDestroy, onMount } from "svelte";
import userIdentity from "$stores/identity.js";
@@ -41,6 +42,86 @@ let IndexColumnComponent: IndexColumnSvelteComponent | null = $state(null);
let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null =
$state(null);
let authenticatedHomeSurfaceImport: Promise<void> | null = null;
+let balancedListFlowElement: HTMLDivElement | undefined = $state();
+let balancedListResizeObserver: ResizeObserver | undefined;
+let balancedListMeasurementFrame = 0;
+const balancedListPanelElements = new Map<string, HTMLElement>();
+let balancedListPanelAssignments = $state<Record<string, "left" | "right">>({});
+
+const resetBalancedListLayout = () => {
+ balancedListPanelAssignments = {};
+};
+
+const updateBalancedListLayout = () => {
+ if (!balancedListFlowElement) return;
+
+ const balancedListFlowStyles = getComputedStyle(balancedListFlowElement);
+ const balancedListRowGap = parseFloat(
+ balancedListFlowStyles.getPropertyValue("row-gap"),
+ );
+ const balancedListColumnCount = balancedListFlowStyles
+ .getPropertyValue("grid-template-columns")
+ .split(" ")
+ .filter(Boolean).length;
+
+ if (balancedListColumnCount <= 1 || !Number.isFinite(balancedListRowGap)) {
+ balancedListPanelAssignments = Object.fromEntries(
+ Array.from(balancedListPanelElements.keys()).map((key) => [key, "left"]),
+ );
+
+ return;
+ }
+
+ const balancedListKeys = ["upcoming", "due", "manga"].filter((key) =>
+ balancedListPanelElements.has(key),
+ );
+ const balancedListHeights = { left: 0, right: 0 };
+ const nextBalancedListAssignments: Record<string, "left" | "right"> = {};
+
+ balancedListKeys.forEach((key) => {
+ const balancedListPanel = balancedListPanelElements.get(key);
+ if (!balancedListPanel) return;
+
+ const balancedListColumn =
+ balancedListHeights.left <= balancedListHeights.right ? "left" : "right";
+ const balancedListPanelHeight =
+ balancedListPanel.getBoundingClientRect().height;
+
+ nextBalancedListAssignments[key] = balancedListColumn;
+ balancedListHeights[balancedListColumn] +=
+ balancedListPanelHeight +
+ (balancedListHeights[balancedListColumn] > 0 ? balancedListRowGap : 0);
+ });
+
+ balancedListPanelAssignments = nextBalancedListAssignments;
+};
+
+const queueBalancedListLayout = () => {
+ if (!browser) return;
+
+ cancelAnimationFrame(balancedListMeasurementFrame);
+
+ balancedListMeasurementFrame = requestAnimationFrame(() => {
+ updateBalancedListLayout();
+ });
+};
+
+const observeBalancedListPanel = (panel: HTMLElement, key: string) => {
+ balancedListPanelElements.set(key, panel);
+ balancedListResizeObserver?.observe(panel);
+ queueBalancedListLayout();
+
+ return {
+ destroy: () => {
+ balancedListPanelElements.delete(key);
+ balancedListResizeObserver?.unobserve(panel);
+ queueBalancedListLayout();
+ },
+ };
+};
+
+const isBalancedListPanelInColumn = (key: string, column: "left" | "right") =>
+ (balancedListPanelAssignments[key] ?? "left") === column;
const loadAuthenticatedHomeSurface = () => {
if (data.user === undefined) return null;
@@ -68,10 +149,30 @@ onMount(async () => {
await hydrateStateBin();
$stateBin.upcomingAnimeListOpen ??= true;
$stateBin.dueMangaListOpen ??= true;
+
+ balancedListResizeObserver = new ResizeObserver(() => {
+ queueBalancedListLayout();
+ });
+
+ balancedListPanelElements.forEach((panel) => {
+ balancedListResizeObserver?.observe(panel);
+ });
+
+ window.addEventListener("resize", queueBalancedListLayout);
void loadAuthenticatedHomeSurface();
+
+ queueBalancedListLayout();
});
-onDestroy(() => removeHeightObserver?.());
+onDestroy(() => {
+ removeHeightObserver?.();
+ balancedListResizeObserver?.disconnect();
+ if (browser) {
+ cancelAnimationFrame(balancedListMeasurementFrame);
+ window.removeEventListener("resize", queueBalancedListLayout);
+ }
+ resetBalancedListLayout();
+});
</script>
<HeadTitle />
@@ -87,85 +188,140 @@ onDestroy(() => removeHeightObserver?.());
<Landing />
{:else}
+ {@const balancedListColumnCount =
+ [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga]
+ .map(Number)
+ .reduce((a, b) => a + b) > 1
+ ? 2
+ : 1}
<div
- class="grid-container"
+ bind:this={balancedListFlowElement}
+ class="balanced-list-flow"
style={`
- grid-template-columns: ${
- [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga]
- .map(Number)
- .reduce((a, b) => a + b) > 1
- ? '1fr 1fr'
- : '1fr'
- }
+ --balanced-list-columns: ${balancedListColumnCount}
`}
>
- <div class="left-column">
- {#if !$settings.disableUpcomingAnime}
- <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
- {#if $userIdentity.id !== -2}
- {#if UpcomingAnimeListComponent}
- <UpcomingAnimeListComponent user={data.user} />
+ <div class="balanced-list-column">
+ {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}>
+ <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
+ {#if $userIdentity.id !== -2}
+ {#if UpcomingAnimeListComponent}
+ <UpcomingAnimeListComponent user={data.user} />
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
{:else}
<ListTitle title={$locale().lists.upcoming.episodes} />
<Skeleton card={false} count={5} height="0.9rem" list />
{/if}
+ </details>
+ </div>
+ {/if}
+
+ {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}>
+ {#if IndexColumnComponent}
+ <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
{:else}
- <ListTitle title={$locale().lists.upcoming.episodes} />
+ <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
+ <ListTitle title={$locale().lists.due.episodes} />
- <Skeleton card={false} count={5} height="0.9rem" list />
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ </details>
{/if}
- </details>
+ </div>
{/if}
- {#if !$settings.disableAnime && !$settings.disableManga}
- {#if IndexColumnComponent}
- <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
- {:else}
- <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
- <ListTitle title={$locale().lists.due.episodes} />
+ {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "left")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}>
+ <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
+ {#if $userIdentity.id !== -2}
+ {#if MangaListTemplateComponent}
+ <MangaListTemplateComponent
+ user={data.user}
+ displayUnresolved={$settings.displayUnresolved}
+ due={true}
+ />
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
- <Skeleton card={false} count={5} height="0.9rem" list />
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
</details>
- {/if}
+ </div>
{/if}
</div>
- <div class="right-column">
- {#if !$settings.disableAnime && $settings.disableManga}
- {#if IndexColumnComponent}
- <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
- {:else}
- <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
- <ListTitle title={$locale().lists.due.episodes} />
+ {#if balancedListColumnCount > 1}
+ <div class="balanced-list-column">
+ {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}>
+ <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming">
+ {#if $userIdentity.id !== -2}
+ {#if UpcomingAnimeListComponent}
+ <UpcomingAnimeListComponent user={data.user} />
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
- <Skeleton card={false} count={5} height="0.9rem" list />
- </details>
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.upcoming.episodes} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
{/if}
- {/if}
- {#if !$settings.disableManga}
- <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
- {#if $userIdentity.id !== -2}
- {#if MangaListTemplateComponent}
- <MangaListTemplateComponent
- user={data.user}
- displayUnresolved={$settings.displayUnresolved}
- due={true}
- />
+ {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}>
+ {#if IndexColumnComponent}
+ <IndexColumnComponent user={data.user} userIdentity={$userIdentity} />
{:else}
- <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+ <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due">
+ <ListTitle title={$locale().lists.due.episodes} />
- <Skeleton card={false} count={5} height="0.9rem" list />
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ </details>
{/if}
- {:else}
- <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+ </div>
+ {/if}
- <Skeleton card={false} count={5} height="0.9rem" list />
- {/if}
- </details>
- {/if}
- </div>
+ {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "right")}
+ <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}>
+ <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga">
+ {#if $userIdentity.id !== -2}
+ {#if MangaListTemplateComponent}
+ <MangaListTemplateComponent
+ user={data.user}
+ displayUnresolved={$settings.displayUnresolved}
+ due={true}
+ />
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ {:else}
+ <ListTitle title={$locale().lists.due.mangaAndLightNovels} />
+
+ <Skeleton card={false} count={5} height="0.9rem" list />
+ {/if}
+ </details>
+ </div>
+ {/if}
+ </div>
+ {/if}
{#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga}
<video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop>
@@ -176,30 +332,32 @@ onDestroy(() => removeHeightObserver?.());
{/if}
<style>
- .grid-container {
+ .balanced-list-flow {
display: grid;
+ grid-template-columns: repeat(var(--balanced-list-columns), minmax(0, 1fr));
gap: 1rem;
+ align-items: start;
}
- .left-column {
+ .balanced-list-column {
display: grid;
gap: 1rem;
align-content: start;
+ min-width: 0;
}
- .right-column {
- align-self: start;
+ .balanced-list-panel {
+ min-width: 0;
}
.list {
overflow-y: auto;
- break-inside: avoid;
- page-break-inside: avoid;
+ margin: 0;
}
@media (max-width: 800px) {
- .grid-container {
- grid-template-columns: 1fr !important;
+ .balanced-list-flow {
+ grid-template-columns: 1fr;
}
}
</style>
diff --git a/src/routes/api/badges/+server.ts b/src/routes/api/badges/+server.ts
index 476fb264..46b98cbc 100644
--- a/src/routes/api/badges/+server.ts
+++ b/src/routes/api/badges/+server.ts
@@ -15,10 +15,10 @@ import {
incrementClickCount,
} from "$lib/Database/SB/User/badges";
import { Schema } from "effect";
-import { appOriginHeaders } from "$lib/Utility/appOrigin";
+import { appOrigin, appOriginHeaders } from "$lib/Utility/appOrigin";
import privilegedUser from "$lib/Utility/privilegedUser";
-const unauthorised = new Response("Unauthorised", { status: 401 });
+const unauthorised = () => new Response("Unauthorised", { status: 401 });
const importedBadgeSchema = Schema.Record(Schema.String, Schema.Unknown);
const badges = async (id: number) =>
@@ -33,15 +33,15 @@ export const GET = async ({ url }) => {
export const DELETE = async ({ url, cookies }) => {
const userCookie = cookies.get("user");
- if (!userCookie) return unauthorised;
+ if (!userCookie) return unauthorised();
const user = decodeAuthCookieOrNull(userCookie);
- if (!user) return unauthorised;
+ if (!user) return unauthorised();
const identity = await safeUserIdentity(user);
- if (!identity) return unauthorised;
+ if (!identity) return unauthorised();
if ((url.searchParams.get("prune") || 0) === "true") {
await removeAllUserBadges(identity.id);
@@ -54,6 +54,8 @@ export const DELETE = async ({ url, cookies }) => {
export const PUT = async ({ cookies, url, request }) => {
if (url.searchParams.get("incrementClickCount") || undefined) {
+ if (request.headers.get("origin") !== appOrigin()) return unauthorised();
+
await incrementClickCount(
Number(url.searchParams.get("incrementClickCount")),
);
@@ -63,19 +65,22 @@ export const PUT = async ({ cookies, url, request }) => {
const userCookie = cookies.get("user");
- if (!userCookie) return unauthorised;
+ if (!userCookie) return unauthorised();
const user = decodeAuthCookieOrNull(userCookie);
- if (!user) return unauthorised;
+ if (!user) return unauthorised();
const identity = await safeUserIdentity(user);
- if (!identity) return unauthorised;
+ if (!identity) return unauthorised();
const authorised = privilegedUser(identity.id);
if (url.searchParams.get("shadowHide"))
- setShadowHidden(Number(url.searchParams.get("shadowHide")), authorised);
+ await setShadowHidden(
+ Number(url.searchParams.get("shadowHide")),
+ authorised,
+ );
if (url.searchParams.get("import") || undefined) {
const importedBadges = await decodeRequestJsonOrThrow(
@@ -135,7 +140,7 @@ export const PUT = async ({ cookies, url, request }) => {
}
if (url.searchParams.get("shadowHideBadge") || undefined) {
- if (!authorised) return unauthorised;
+ if (!authorised) return unauthorised();
await setShadowHiddenBadge(
Number(url.searchParams.get("shadowHideBadge")),
diff --git a/src/routes/api/subsplease/+server.ts b/src/routes/api/subsplease/+server.ts
index 6ef2d832..1f678d8c 100644
--- a/src/routes/api/subsplease/+server.ts
+++ b/src/routes/api/subsplease/+server.ts
@@ -1,12 +1,12 @@
import { appOriginHeaders } from "$lib/Utility/appOrigin";
-export const GET = async ({ url }) =>
- Response.json(
+export const GET = async ({ url }) => {
+ const timezone = url.searchParams.get("tz") || "America/Los_Angeles";
+
+ return Response.json(
await (
await fetch(
- `https://subsplease.org/api/?f=schedule&tz=${
- url.searchParams.get("tz") || "America/Los_Angeles"
- }`,
+ `https://subsplease.org/api/?f=schedule&tz=${encodeURIComponent(timezone)}`,
)
).json(),
{
@@ -15,3 +15,4 @@ export const GET = async ({ url }) =>
}),
},
);
+};
diff --git a/src/stores/revalidateAnime.ts b/src/stores/revalidateAnime.ts
index acbbf367..db3ccb2d 100644
--- a/src/stores/revalidateAnime.ts
+++ b/src/stores/revalidateAnime.ts
@@ -1,5 +1,5 @@
import { writable } from "svelte/store";
-const revalidateAnime = writable<boolean>(false);
+const revalidateAnime = writable<number>(0);
export default revalidateAnime;
diff --git a/src/stores/revalidateManga.ts b/src/stores/revalidateManga.ts
new file mode 100644
index 00000000..b72ea1af
--- /dev/null
+++ b/src/stores/revalidateManga.ts
@@ -0,0 +1,5 @@
+import { writable } from "svelte/store";
+
+const revalidateManga = writable<number>(0);
+
+export default revalidateManga;
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index cd0f306d..24d9eef0 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -195,42 +195,47 @@ settings.subscribe((value) => {
if (!browser) return;
if (value.settingsSync && get(settingsSyncPulled) === true) {
- fetch(root(`/api/configuration?id=${get(identity).id}`)).then(
- (response) => {
- if (response.ok)
- response.json().then((data) => {
- const isEqualsJson = (
- firstObject: Settings,
- secondObject: Settings,
- ) => {
- type AnyObject = { [key: string]: unknown };
-
- return (
- Object.keys(firstObject).length ===
- Object.keys(secondObject).length &&
- Object.keys(firstObject).every(
- (key) =>
- (firstObject as unknown as AnyObject)[key] ===
- (secondObject as unknown as AnyObject)[key],
- )
- );
- };
-
- if (data?.configuration && !isEqualsJson(data.configuration, value))
- fetch(root(`/api/configuration`), {
- method: "PUT",
- body: JSON.stringify(value),
- }).then((response) => {
+ fetch(root(`/api/configuration?id=${get(identity).id}`))
+ .then((response) => {
+ if (!response.ok) return;
+
+ return response.json().then((data) => {
+ const isEqualsJson = (
+ firstObject: Settings,
+ secondObject: Settings,
+ ) => {
+ type AnyObject = { [key: string]: unknown };
+
+ return (
+ Object.keys(firstObject).length ===
+ Object.keys(secondObject).length &&
+ Object.keys(firstObject).every(
+ (key) =>
+ (firstObject as unknown as AnyObject)[key] ===
+ (secondObject as unknown as AnyObject)[key],
+ )
+ );
+ };
+
+ if (data?.configuration && !isEqualsJson(data.configuration, value))
+ fetch(root(`/api/configuration`), {
+ method: "PUT",
+ body: JSON.stringify(value),
+ })
+ .then((response) => {
if (response.ok) console.log("Pushed local configuration");
settingsSyncTimes.update((times) => ({
...times,
lastPush: new Date(),
}));
- });
- });
- },
- );
+ })
+ .catch((error) =>
+ console.error("Settings sync push failed", error),
+ );
+ });
+ })
+ .catch((error) => console.error("Settings sync pull failed", error));
}
});
diff --git a/src/trigger/notifications.ts b/src/trigger/notifications.ts
index 30a50a12..e36f913f 100644
--- a/src/trigger/notifications.ts
+++ b/src/trigger/notifications.ts
@@ -25,9 +25,9 @@ const subscriptionEndpoint = (subscription: unknown) =>
export const notificationsTask = schedules.task({
id: "notifications",
- run: async (_payload, { ctx }) => {
- const environment = ctx.environment.slug;
- const triggerProjectReference = ctx.project.ref;
+ run: async (_payload, { ctx: taskContext }) => {
+ const environment = taskContext.environment.slug;
+ const triggerProjectReference = taskContext.project.ref;
const supabase = createClient(
(
await envvars.retrieve(