fix: offline-content renamed to downloads

This commit is contained in:
gosangam
2026-07-14 16:51:17 +05:30
parent 12a5c15121
commit 1683abc2e9
6 changed files with 39 additions and 27 deletions
@@ -1,7 +1,7 @@
"use client"; "use client";
/** /**
* /offline-content — the reader's device-local offline library. * /downloads — the reader's device-local offline library.
* *
* Lists everything saved via the header's "Save offline" button and lets them * Lists everything saved via the header's "Save offline" button and lets them
* open (offline-capable) or remove each item. Items link with a plain <a> (a * open (offline-capable) or remove each item. Items link with a plain <a> (a
@@ -10,7 +10,7 @@
* fail. This page also doubles as the SW's offline navigation fallback. * fail. This page also doubles as the SW's offline navigation fallback.
*/ */
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { import {
BookOpen, BookOpen,
FileText, FileText,
@@ -67,33 +67,45 @@ function formatDate(ms: number): string {
} }
} }
export default function OfflineContentPage() { // External-store readers for the two client-only, non-React values. Using
// useSyncExternalStore (instead of setState-in-an-effect) is SSR-safe and
// avoids the "setState synchronously within an effect" cascade warning.
function subscribeOnline(cb: () => void) {
window.addEventListener("online", cb);
window.addEventListener("offline", cb);
return () => {
window.removeEventListener("online", cb);
window.removeEventListener("offline", cb);
};
}
const noopSubscribe = () => () => {};
export default function DownloadsPage() {
const [items, setItems] = useState<OfflineItem[] | null>(null); const [items, setItems] = useState<OfflineItem[] | null>(null);
const [usage, setUsage] = useState<number | null>(null); const [usage, setUsage] = useState<number | null>(null);
const [online, setOnline] = useState(true);
const [supported, setSupported] = useState(true); // Client-only reads via external stores (SSR snapshot → server-safe default).
const online = useSyncExternalStore(
subscribeOnline,
() => navigator.onLine,
() => true,
);
const supported = useSyncExternalStore(
noopSubscribe,
() => offlineSupported(),
() => false,
);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
setItems(await listOffline()); setItems(await listOffline());
setUsage(await usageBytes()); setUsage(await usageBytes());
}, []); }, []);
// Load the library once mounted. The setState calls happen after an await
// (asynchronously), not synchronously in the effect body.
useEffect(() => { useEffect(() => {
if (!offlineSupported()) { if (!offlineSupported()) return;
setSupported(false);
setItems([]);
return;
}
setOnline(navigator.onLine);
void refresh(); void refresh();
const on = () => setOnline(true);
const off = () => setOnline(false);
window.addEventListener("online", on);
window.addEventListener("offline", off);
return () => {
window.removeEventListener("online", on);
window.removeEventListener("offline", off);
};
}, [refresh]); }, [refresh]);
async function remove(key: string) { async function remove(key: string) {
+1 -1
View File
@@ -71,7 +71,7 @@ export default function SettingsPage() {
description="Pages and lectures you've saved to read or listen to without a connection. Saved on this device — open the library to remove downloads and free up space." description="Pages and lectures you've saved to read or listen to without a connection. Saved on this device — open the library to remove downloads and free up space."
> >
<ul className="divide-y divide-border"> <ul className="divide-y divide-border">
<LegalLink href="/offline-content" label="Manage offline library" /> <LegalLink href="/downloads" label="Manage offline downloads" />
</ul> </ul>
</Section> </Section>
+1 -1
View File
@@ -33,7 +33,7 @@ const SECONDARY: NavItem[] = [
{ href: "/journals", label: "Journals" }, { href: "/journals", label: "Journals" },
{ href: "/magazines", label: "Magazines" }, { href: "/magazines", label: "Magazines" },
{ href: "/other-documents", label: "Other Documents" }, { href: "/other-documents", label: "Other Documents" },
{ href: "/offline-content", label: "Offline library" }, { href: "/downloads", label: "Downloads" },
{ href: "/about", label: "About" }, { href: "/about", label: "About" },
]; ];
+2 -2
View File
@@ -97,8 +97,8 @@ const TOP_ITEMS: NavItem[] = [
icon: <AtSign className="size-5" />, icon: <AtSign className="size-5" />,
}, },
{ {
href: "/offline-content", href: "/downloads",
label: "Offline", label: "Downloads",
icon: <DownloadCloud className="size-5" />, icon: <DownloadCloud className="size-5" />,
}, },
]; ];
+3 -3
View File
@@ -9,7 +9,7 @@
* - **Transcript audio** → a Blob in IndexedDB, played through a pre-created * - **Transcript audio** → a Blob in IndexedDB, played through a pre-created
* object URL (full native seeking; see `primeOfflineAudio`). * object URL (full native seeking; see `primeOfflineAudio`).
* *
* An `items` index (IndexedDB) records what's saved so `/offline-content` can * An `items` index (IndexedDB) records what's saved so `/downloads` can
* list it and the header button can show saved state. * list it and the header button can show saved state.
*/ */
@@ -212,13 +212,13 @@ async function cachePageAndAssets(
return { htmlLength: html.length, assetCount: assetUrls.length }; return { htmlLength: html.length, assetCount: assetUrls.length };
} }
/** Precache the /offline-content library page so it works as the SW's offline /** Precache the /downloads library page so it works as the SW's offline
* navigation fallback even if the user never opened it online. Best-effort. */ * navigation fallback even if the user never opened it online. Best-effort. */
export async function precacheOfflineLibrary(): Promise<void> { export async function precacheOfflineLibrary(): Promise<void> {
if (!offlineSupported()) return; if (!offlineSupported()) return;
try { try {
await cachePageAndAssets( await cachePageAndAssets(
new URL("/offline-content", window.location.origin).href, new URL("/downloads", window.location.origin).href,
); );
} catch { } catch {
/* best-effort */ /* best-effort */
+1 -1
View File
@@ -13,7 +13,7 @@ const PAGES_CACHE = `offline-pages-${CACHE_VERSION}`; // saved page HTML documen
const ASSETS_CACHE = `offline-assets-${CACHE_VERSION}`; // /_next/* + static files const ASSETS_CACHE = `offline-assets-${CACHE_VERSION}`; // /_next/* + static files
const OFFLINE_CACHES = [PAGES_CACHE, ASSETS_CACHE]; const OFFLINE_CACHES = [PAGES_CACHE, ASSETS_CACHE];
// The library page doubles as the navigation fallback when offline. // The library page doubles as the navigation fallback when offline.
const OFFLINE_FALLBACK = "/offline-content"; const OFFLINE_FALLBACK = "/downloads";
self.addEventListener("install", () => self.skipWaiting()); self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => self.addEventListener("activate", (event) =>