From 7cdc3d096931b273833a29c70243ec3a17b1f32a Mon Sep 17 00:00:00 2001 From: "Marko (Hermes Implementer)" Date: Fri, 29 May 2026 07:04:53 +0000 Subject: [PATCH] =?UTF-8?q?Remove=20stale=20web/=20directory=20=E2=80=94?= =?UTF-8?q?=20content=20already=20ported=20to=20frontend/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reid's second review flagged that the old web/ files still had: - XSS vulnerability (no DOMPurify) - CSS typo (landascape) - API endpoint mismatches All fix: delete web/ entirely. The correct, sanitized, fixed versions already live in frontend/src/. --- web/src/api/reader.ts | 112 ---- web/src/components/ReaderToolbar.tsx | 67 --- web/src/components/ReadingSettingsPanel.tsx | 232 -------- web/src/components/TableOfContents.tsx | 80 --- web/src/hooks/useChapters.ts | 129 ---- web/src/hooks/useReadingProgress.ts | 99 ---- web/src/hooks/useReadingSettings.ts | 96 --- web/src/pages/ReadingPage.tsx | 199 ------- web/src/reader.css | 621 -------------------- web/src/types/reader.ts | 44 -- 10 files changed, 1679 deletions(-) delete mode 100644 web/src/api/reader.ts delete mode 100644 web/src/components/ReaderToolbar.tsx delete mode 100644 web/src/components/ReadingSettingsPanel.tsx delete mode 100644 web/src/components/TableOfContents.tsx delete mode 100644 web/src/hooks/useChapters.ts delete mode 100644 web/src/hooks/useReadingProgress.ts delete mode 100644 web/src/hooks/useReadingSettings.ts delete mode 100644 web/src/pages/ReadingPage.tsx delete mode 100644 web/src/reader.css delete mode 100644 web/src/types/reader.ts diff --git a/web/src/api/reader.ts b/web/src/api/reader.ts deleted file mode 100644 index cbf73e7..0000000 --- a/web/src/api/reader.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * API client for the reader module — reading settings, chapters, and progress. - */ - -import type { - ChapterDetail, - ChapterSummary, - ReadingProgress, - ReadingSettings, -} from "../types/reader"; - -const API_BASE = "/api"; - -/** - * Fetch the current user's reading settings. - * Auto-creates defaults on the server if none exist. - */ -export async function getReadingSettings(): Promise { - const response = await fetch(`${API_BASE}/reader/settings/`); - if (!response.ok) { - throw new Error( - `Failed to fetch reading settings: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} - -/** - * Update (full or partial) the user's reading settings. - */ -export async function updateReadingSettings( - settings: Partial -): Promise { - const method = settings.theme !== undefined ? "PUT" : "PATCH"; - const response = await fetch(`${API_BASE}/reader/settings/`, { - method, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(settings), - }); - if (!response.ok) { - throw new Error( - `Failed to update reading settings: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} - -/** - * Fetch the table of contents (chapter list) for a book. - */ -export async function getChapters(bookId: number): Promise { - const response = await fetch(`${API_BASE}/books/${bookId}/chapters/`); - if (!response.ok) { - throw new Error( - `Failed to fetch chapters: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} - -/** - * Fetch a specific chapter with full content for reading. - */ -export async function getChapterContent( - bookId: number, - chapterNumber: number -): Promise { - const response = await fetch( - `${API_BASE}/books/${bookId}/chapters/${chapterNumber}/` - ); - if (!response.ok) { - throw new Error( - `Failed to fetch chapter ${chapterNumber}: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} - -/** - * Fetch reading progress for a book. - */ -export async function getReadingProgress( - bookId: number -): Promise { - const response = await fetch(`${API_BASE}/books/${bookId}/progress/`); - if (!response.ok) { - throw new Error( - `Failed to fetch reading progress: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} - -/** - * Update reading progress for a book. - */ -export async function updateReadingProgress( - bookId: number, - progress: Partial -): Promise { - const response = await fetch(`${API_BASE}/books/${bookId}/progress/`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(progress), - }); - if (!response.ok) { - throw new Error( - `Failed to update reading progress: ${response.status} ${response.statusText}` - ); - } - return response.json() as Promise; -} \ No newline at end of file diff --git a/web/src/components/ReaderToolbar.tsx b/web/src/components/ReaderToolbar.tsx deleted file mode 100644 index 65884f3..0000000 --- a/web/src/components/ReaderToolbar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * ReaderToolbar — fixed bottom toolbar for the reading view. - * Provides TOC toggle, settings toggle, and progress indicator. - */ - -import type { ReadingProgress } from "../types/reader"; - -interface ReaderToolbarProps { - bookTitle: string; - chapterTitle: string; - progress: ReadingProgress | null; - onToggleToc: () => void; - onToggleSettings: () => void; -} - -export default function ReaderToolbar({ - bookTitle, - chapterTitle, - progress, - onToggleToc, - onToggleSettings, -}: ReaderToolbarProps) { - const percentage = progress?.percentage ?? 0; - - return ( - <> - {/* Top bar */} -
- -
- {bookTitle} - {chapterTitle} -
- -
- - {/* Bottom progress bar */} -
-
-
- - ); -} \ No newline at end of file diff --git a/web/src/components/ReadingSettingsPanel.tsx b/web/src/components/ReadingSettingsPanel.tsx deleted file mode 100644 index 9d7d11a..0000000 --- a/web/src/components/ReadingSettingsPanel.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/** - * ReadingSettingsPanel — slide-in drawer from the right for customizing - * the reading experience: theme, font, sizing, orientation. - */ - -import { useState } from "react"; -import type { - FontFamily, - OrientationLock, - ReadingSettings, - ThemePreset, -} from "../types/reader"; - -interface ReadingSettingsPanelProps { - settings: ReadingSettings; - isOpen: boolean; - onClose: () => void; - onUpdate: (partial: Partial) => Promise; -} - -const THEME_OPTIONS: { value: ThemePreset; label: string }[] = [ - { value: "sepia", label: "Sepia" }, - { value: "dark", label: "Dark" }, - { value: "light", label: "Light" }, - { value: "paper", label: "Paper" }, -]; - -const FONT_OPTIONS: { value: FontFamily; label: string }[] = [ - { value: "sans-serif", label: "Sans-serif" }, - { value: "serif", label: "Serif" }, - { value: "monospace", label: "Monospace" }, -]; - -const ORIENTATION_OPTIONS: { value: OrientationLock; label: string }[] = [ - { value: "auto", label: "Auto" }, - { value: "portrait", label: "Portrait" }, - { value: "landscape", label: "Landscape" }, -]; - -export default function ReadingSettingsPanel({ - settings, - isOpen, - onClose, - onUpdate, -}: ReadingSettingsPanelProps) { - const [saving, setSaving] = useState>({}); - - const handleChange = async ( - key: keyof ReadingSettings, - value: string | number - ) => { - setSaving((prev) => ({ ...prev, [key]: true })); - try { - await onUpdate({ [key]: value as never }); - } finally { - setSaving((prev) => ({ ...prev, [key]: false })); - } - }; - - return ( - <> - {/* Overlay */} - {isOpen && ( -
{ - if (e.key === "Escape") onClose(); - }} - role="presentation" - /> - )} - - {/* Drawer */} - - - ); -} \ No newline at end of file diff --git a/web/src/components/TableOfContents.tsx b/web/src/components/TableOfContents.tsx deleted file mode 100644 index 7a4db64..0000000 --- a/web/src/components/TableOfContents.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * TableOfContents — slide-in drawer listing all chapters. - * Tap a chapter to navigate. Current chapter is highlighted. - */ - -import type { ChapterSummary } from "../types/reader"; - -interface TableOfContentsProps { - chapters: ChapterSummary[]; - currentChapterNumber: number; - isOpen: boolean; - onClose: () => void; - onNavigate: (number: number) => void; -} - -export default function TableOfContents({ - chapters, - currentChapterNumber, - isOpen, - onClose, - onNavigate, -}: TableOfContentsProps) { - const handleChapterClick = (number: number) => { - onNavigate(number); - onClose(); - }; - - return ( - <> - {/* Overlay */} - {isOpen && ( -
{ - if (e.key === "Escape") onClose(); - }} - role="presentation" - /> - )} - - {/* Drawer */} - - - ); -} \ No newline at end of file diff --git a/web/src/hooks/useChapters.ts b/web/src/hooks/useChapters.ts deleted file mode 100644 index e388fa2..0000000 --- a/web/src/hooks/useChapters.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * useChapters — fetch chapter list and manage current chapter navigation. - */ - -import { useCallback, useEffect, useState } from "react"; -import { getChapterContent, getChapters } from "../api/reader"; -import type { ChapterDetail, ChapterSummary } from "../types/reader"; - -export interface UseChaptersReturn { - chapters: ChapterSummary[]; - currentChapter: ChapterDetail | null; - currentChapterNumber: number; - isLoading: boolean; - error: string | null; - navigateToChapter: (number: number) => Promise; - goToNextChapter: () => Promise; - goToPreviousChapter: () => Promise; - hasNext: boolean; - hasPrevious: boolean; -} - -export function useChapters( - bookId: number, - initialChapter: number = 1 -): UseChaptersReturn { - const [chapters, setChapters] = useState([]); - const [currentChapter, setCurrentChapter] = useState( - null - ); - const [currentChapterNumber, setCurrentChapterNumber] = - useState(initialChapter); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // Fetch chapter list on mount - useEffect(() => { - let cancelled = false; - setIsLoading(true); - getChapters(bookId) - .then((data) => { - if (!cancelled) { - setChapters(data); - // If chapters exist and initial chapter is valid, fetch it - if ( - data.length > 0 && - data.some((c) => c.number === currentChapterNumber) - ) { - return getChapterContent(bookId, currentChapterNumber); - } - return null; - } - return null; - }) - .then((chapter) => { - if (!cancelled && chapter) { - setCurrentChapter(chapter); - } - }) - .catch((err: unknown) => { - if (!cancelled) { - setError( - err instanceof Error ? err.message : "Failed to load chapters" - ); - } - }) - .finally(() => { - if (!cancelled) setIsLoading(false); - }); - return () => { - cancelled = true; - }; - }, [bookId, currentChapterNumber]); - - const fetchChapter = useCallback( - async (number: number) => { - setIsLoading(true); - setError(null); - try { - const chapter = await getChapterContent(bookId, number); - setCurrentChapter(chapter); - setCurrentChapterNumber(number); - } catch (err: unknown) { - setError( - err instanceof Error ? err.message : "Failed to load chapter" - ); - } finally { - setIsLoading(false); - } - }, - [bookId] - ); - - const navigateToChapter = useCallback( - (number: number) => { - fetchChapter(number); - }, - [fetchChapter] - ); - - const goToNextChapter = useCallback(() => { - const next = currentChapterNumber + 1; - if (chapters.some((c) => c.number === next)) { - fetchChapter(next); - } - }, [currentChapterNumber, chapters, fetchChapter]); - - const goToPreviousChapter = useCallback(() => { - const prev = currentChapterNumber - 1; - if (prev >= 1 && chapters.some((c) => c.number === prev)) { - fetchChapter(prev); - } - }, [currentChapterNumber, chapters, fetchChapter]); - - const hasNext = chapters.some((c) => c.number === currentChapterNumber + 1); - const hasPrevious = chapters.some((c) => c.number === currentChapterNumber - 1); - - return { - chapters, - currentChapter, - currentChapterNumber, - isLoading, - error, - navigateToChapter, - goToNextChapter, - goToPreviousChapter, - hasNext, - hasPrevious, - }; -} \ No newline at end of file diff --git a/web/src/hooks/useReadingProgress.ts b/web/src/hooks/useReadingProgress.ts deleted file mode 100644 index 1d8034d..0000000 --- a/web/src/hooks/useReadingProgress.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * useReadingProgress — fetch and update reading progress for a book. - * Auto-saves when chapter or position changes. - */ - -import { useCallback, useEffect, useRef, useState } from "react"; -import { getReadingProgress, updateReadingProgress } from "../api/reader"; -import type { ReadingProgress } from "../types/reader"; - -export interface UseReadingProgressReturn { - progress: ReadingProgress | null; - isLoading: boolean; - error: string | null; - saveProgress: ( - chapter: number, - position: number, - percentage: number - ) => Promise; - /** Schedule a debounced save — fires at most once per 3 seconds */ - debouncedSave: ( - chapter: number, - position: number, - percentage: number - ) => void; -} - -export function useReadingProgress( - bookId: number -): UseReadingProgressReturn { - const [progress, setProgress] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const debounceTimer = useRef | null>(null); - - // Fetch progress on mount - useEffect(() => { - let cancelled = false; - setIsLoading(true); - getReadingProgress(bookId) - .then((data) => { - if (!cancelled) setProgress(data); - }) - .catch((err: unknown) => { - if (!cancelled) { - setError( - err instanceof Error ? err.message : "Failed to load progress" - ); - } - }) - .finally(() => { - if (!cancelled) setIsLoading(false); - }); - return () => { - cancelled = true; - }; - }, [bookId]); - - const saveProgress = useCallback( - async (chapter: number, position: number, percentage: number) => { - try { - const updated = await updateReadingProgress(bookId, { - current_chapter: chapter, - current_position: position, - percentage, - }); - setProgress(updated); - setError(null); - } catch (err: unknown) { - setError( - err instanceof Error ? err.message : "Failed to save progress" - ); - } - }, - [bookId] - ); - - const debouncedSave = useCallback( - (chapter: number, position: number, percentage: number) => { - if (debounceTimer.current) { - clearTimeout(debounceTimer.current); - } - debounceTimer.current = setTimeout(() => { - saveProgress(chapter, position, percentage); - }, 3000); - }, - [saveProgress] - ); - - // Cleanup timer on unmount - useEffect(() => { - return () => { - if (debounceTimer.current) { - clearTimeout(debounceTimer.current); - } - }; - }, []); - - return { progress, isLoading, error, saveProgress, debouncedSave }; -} \ No newline at end of file diff --git a/web/src/hooks/useReadingSettings.ts b/web/src/hooks/useReadingSettings.ts deleted file mode 100644 index 7e5df8d..0000000 --- a/web/src/hooks/useReadingSettings.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * useReadingSettings — fetch and manage user reading preferences. - * Applies settings as CSS custom properties on the document root. - */ - -import { useCallback, useEffect, useState } from "react"; -import { - getReadingSettings, - updateReadingSettings, -} from "../api/reader"; -import type { ReadingSettings } from "../types/reader"; - -const DEFAULT_SETTINGS: ReadingSettings = { - font_family: "serif", - font_size: 18, - line_height: 1.6, - margin_width: 16, - background_color: "#f5f0eb", - text_color: "#1a1a1a", - brightness: 100, - orientation_lock: "auto", - theme: "sepia", - created_at: "", - updated_at: "", -}; - -function applyCssVariables(settings: ReadingSettings): void { - const root = document.documentElement; - root.style.setProperty("--reader-bg", settings.background_color); - root.style.setProperty("--reader-text", settings.text_color); - root.style.setProperty("--reader-font-family", settings.font_family); - root.style.setProperty("--reader-font-size", `${settings.font_size}px`); - root.style.setProperty("--reader-line-height", String(settings.line_height)); - root.style.setProperty("--reader-margin", `${settings.margin_width}px`); - root.style.setProperty("--reader-brightness", `${settings.brightness}%`); -} - -export interface UseReadingSettingsReturn { - settings: ReadingSettings; - isLoading: boolean; - error: string | null; - updateSettings: (partial: Partial) => Promise; -} - -export function useReadingSettings(): UseReadingSettingsReturn { - const [settings, setSettings] = useState(DEFAULT_SETTINGS); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - // Fetch settings on mount - useEffect(() => { - let cancelled = false; - setIsLoading(true); - getReadingSettings() - .then((data) => { - if (!cancelled) { - setSettings(data); - applyCssVariables(data); - } - }) - .catch((err: unknown) => { - if (!cancelled) { - setError( - err instanceof Error ? err.message : "Failed to load reading settings" - ); - // Apply defaults - applyCssVariables(DEFAULT_SETTINGS); - } - }) - .finally(() => { - if (!cancelled) setIsLoading(false); - }); - return () => { - cancelled = true; - }; - }, []); - - const updateSettings = useCallback( - async (partial: Partial) => { - try { - const updated = await updateReadingSettings(partial); - setSettings(updated); - applyCssVariables(updated); - setError(null); - } catch (err: unknown) { - setError( - err instanceof Error ? err.message : "Failed to update reading settings" - ); - throw err; - } - }, - [] - ); - - return { settings, isLoading, error, updateSettings }; -} \ No newline at end of file diff --git a/web/src/pages/ReadingPage.tsx b/web/src/pages/ReadingPage.tsx deleted file mode 100644 index 9701ba6..0000000 --- a/web/src/pages/ReadingPage.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * ReadingPage — full-screen reading view for ebooks. - * Integrates TOC drawer, settings panel, chapter navigation, and progress tracking. - */ - -import { lazy, Suspense, useCallback, useEffect, useState } from "react"; -import { useChapters } from "../hooks/useChapters"; -import { useReadingProgress } from "../hooks/useReadingProgress"; -import { useReadingSettings } from "../hooks/useReadingSettings"; -import type { BookDetail as BookDetailType } from "../types"; - -const ReaderToolbar = lazy(() => import("../components/ReaderToolbar")); -const TableOfContents = lazy(() => import("../components/TableOfContents")); -const ReadingSettingsPanel = lazy( - () => import("../components/ReadingSettingsPanel") -); - -interface ReadingPageProps { - book: BookDetailType; - onBack: () => void; -} - -export default function ReadingPage({ book, onBack }: ReadingPageProps) { - const [tocOpen, setTocOpen] = useState(false); - const [settingsOpen, setSettingsOpen] = useState(false); - - const { settings, updateSettings } = useReadingSettings(); - const { - chapters, - currentChapter, - currentChapterNumber, - isLoading, - error, - navigateToChapter, - goToNextChapter, - goToPreviousChapter, - hasNext, - hasPrevious, - } = useChapters(book.id, 1); - - const { progress, debouncedSave } = useReadingProgress(book.id); - - // Auto-save progress when chapter changes - useEffect(() => { - if (currentChapter && currentChapterNumber > 0) { - debouncedSave(currentChapterNumber, 0, (currentChapterNumber / Math.max(chapters.length, 1)) * 100); - } - }, [currentChapterNumber, currentChapter?.id]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === "ArrowLeft" && hasPrevious) { - goToPreviousChapter(); - } else if (e.key === "ArrowRight" && hasNext) { - goToNextChapter(); - } else if (e.key === "Escape") { - setTocOpen(false); - setSettingsOpen(false); - } - }, - [hasNext, hasPrevious, goToNextChapter, goToPreviousChapter] - ); - - useEffect(() => { - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [handleKeyDown]); - - // Lock/unlock orientation via CSS - useEffect(() => { - const root = document.documentElement; - if (settings.orientation_lock !== "auto") { - root.style.setProperty( - "--reader-orientation", - settings.orientation_lock === "portrait" ? "portrait" : "landscape" - ); - } else { - root.style.removeProperty("--reader-orientation"); - } - }, [settings.orientation_lock]); - - // Show loading state - if (isLoading && !currentChapter) { - return ( -
-
-

Loading reader...

-
- ); - } - - if (error) { - return ( -
-

{error}

- -
- ); - } - - return ( - -
-
- } - > -
- setTocOpen((v) => !v)} - onToggleSettings={() => setSettingsOpen((v) => !v)} - /> - - setTocOpen(false)} - onNavigate={navigateToChapter} - /> - - setSettingsOpen(false)} - onUpdate={updateSettings} - /> - - {/* Main reading area */} -
- {isLoading ? ( -
-
-

Loading chapter...

-
- ) : currentChapter ? ( -
-

{currentChapter.title}

-
-
- ) : ( -
-

Select a chapter from the table of contents to start reading.

-
- )} -
- - {/* Bottom navigation bar */} - -
-
- ); -} \ No newline at end of file diff --git a/web/src/reader.css b/web/src/reader.css deleted file mode 100644 index 2bf74a9..0000000 --- a/web/src/reader.css +++ /dev/null @@ -1,621 +0,0 @@ -/* ============================================================ - Cloud Reader — Reading View Styles - Mobile-first reading experience with theme support - ============================================================ */ - -/* --- CSS Custom Properties (overridden by JS) --- */ -:root { - --reader-bg: #f5f0eb; - --reader-text: #1a1a1a; - --reader-font-family: Georgia, "Times New Roman", serif; - --reader-font-size: 18px; - --reader-line-height: 1.6; - --reader-margin: 16px; - --reader-brightness: 100%; - --reader-toolbar-bg: rgba(26, 26, 26, 0.95); - --reader-toolbar-text: #e0e0e0; - --reader-accent: #6c8cff; -} - -/* --- Reading Container --- */ -.reader-container { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--reader-bg); - color: var(--reader-text); - font-family: var(--reader-font-family); - font-size: var(--reader-font-size); - line-height: var(--reader-line-height); - filter: brightness(var(--reader-brightness)); - display: flex; - flex-direction: column; - z-index: 100; - overflow: hidden; -} - -/* Orientation lock */ -@supports (--reader-orientation: portrait) { - .reader-container { - orientation: var(--reader-orientation); - } -} - -@media (orientation: landascape) { - .reader-container { - --reader-margin: 24px; - } -} - -/* --- Top Toolbar --- */ -.reader-top-bar { - position: fixed; - top: 0; - left: 0; - right: 0; - height: 52px; - background: var(--reader-toolbar-bg); - color: var(--reader-toolbar-text); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px; - z-index: 120; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); -} - -.reader-bar-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: transparent; - border: none; - color: var(--reader-toolbar-text); - cursor: pointer; - border-radius: 8px; - transition: background 0.15s; -} - -.reader-bar-btn:hover { - background: rgba(255, 255, 255, 0.1); -} - -.reader-bar-title { - display: flex; - flex-direction: column; - align-items: center; - gap: 1px; - min-width: 0; - flex: 1; - padding: 0 8px; -} - -.reader-bar-book { - font-size: 0.8rem; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 200px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -} - -.reader-bar-chapter { - font-size: 0.7rem; - opacity: 0.7; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 200px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -} - -/* --- Progress Bar --- */ -.reader-progress-bar { - position: fixed; - top: 52px; - left: 0; - right: 0; - height: 3px; - background: rgba(255, 255, 255, 0.1); - z-index: 120; -} - -.reader-progress-fill { - height: 100%; - background: var(--reader-accent); - transition: width 0.3s ease; -} - -/* --- Main Content Area --- */ -.reader-content { - flex: 1; - overflow-y: auto; - padding: 72px var(--reader-margin) 72px; - -webkit-overflow-scrolling: touch; - scroll-behavior: smooth; -} - -/* --- Chapter Article --- */ -.reader-chapter { - max-width: 680px; - margin: 0 auto; -} - -.reader-chapter-title { - font-size: 1.6em; - font-weight: 700; - line-height: 1.3; - margin-bottom: 0.75em; - padding-bottom: 0.5em; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - color: var(--reader-text); -} - -/* Chapter body — content from API (could be HTML/markdown-rendered) */ -.reader-chapter-body { - font-size: 1em; - line-height: var(--reader-line-height); -} - -.reader-chapter-body p { - margin-bottom: 1.2em; - text-align: justify; - hyphens: auto; -} - -.reader-chapter-body h2, -.reader-chapter-body h3, -.reader-chapter-body h4 { - margin-top: 1.5em; - margin-bottom: 0.6em; - line-height: 1.3; -} - -.reader-chapter-body blockquote { - border-left: 3px solid var(--reader-accent); - padding-left: 1em; - margin: 1em 0; - opacity: 0.85; - font-style: italic; -} - -.reader-chapter-body img { - max-width: 100%; - height: auto; - border-radius: 4px; - margin: 1em 0; -} - -.reader-chapter-body ul, -.reader-chapter-body ol { - padding-left: 1.5em; - margin-bottom: 1.2em; -} - -.reader-chapter-body li { - margin-bottom: 0.4em; -} - -/* --- Navigation Bar (Bottom) --- */ -.reader-nav { - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 56px; - background: var(--reader-toolbar-bg); - color: var(--reader-toolbar-text); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px; - z-index: 120; - gap: 8px; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); -} - -.reader-nav-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 14px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; - color: var(--reader-toolbar-text); - font-size: 0.85rem; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - cursor: pointer; - transition: all 0.15s; -} - -.reader-nav-btn:hover:not(.reader-nav-btn--disabled) { - border-color: var(--reader-accent); - color: var(--reader-accent); -} - -.reader-nav-btn--disabled { - opacity: 0.3; - cursor: not-allowed; -} - -.reader-nav-btn--back { - background: rgba(108, 140, 255, 0.15); - border-color: rgba(108, 140, 255, 0.3); -} - -.reader-nav-btn--back:hover { - background: rgba(108, 140, 255, 0.25); - color: var(--reader-accent); -} - -/* --- TOC Drawer --- */ -.toc-overlay, -.settings-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 130; - animation: fadeIn 0.2s ease; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.toc-drawer, -.settings-drawer { - position: fixed; - top: 0; - bottom: 0; - width: 300px; - max-width: 85vw; - background: var(--reader-toolbar-bg); - color: var(--reader-toolbar-text); - z-index: 140; - display: flex; - flex-direction: column; - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.toc-drawer { - left: 0; - transform: translateX(-100%); -} - -.toc-drawer--open { - transform: translateX(0); -} - -.settings-drawer { - right: 0; - transform: translateX(100%); -} - -.settings-drawer--open { - transform: translateX(0); -} - -.toc-header, -.settings-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.toc-title, -.settings-title { - font-size: 1.1rem; - font-weight: 700; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -} - -.toc-close-btn, -.settings-close-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - background: transparent; - border: none; - color: var(--reader-toolbar-text); - cursor: pointer; - border-radius: 6px; - transition: background 0.15s; -} - -.toc-close-btn:hover, -.settings-close-btn:hover { - background: rgba(255, 255, 255, 0.1); -} - -/* --- TOC List --- */ -.toc-list { - flex: 1; - overflow-y: auto; - padding: 8px 0; -} - -.toc-empty { - padding: 24px 16px; - text-align: center; - opacity: 0.6; - font-size: 0.9rem; -} - -.toc-item { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - padding: 10px 16px; - background: transparent; - border: none; - color: var(--reader-toolbar-text); - font-size: 0.9rem; - text-align: left; - cursor: pointer; - transition: background 0.15s; - font-family: inherit; -} - -.toc-item:hover { - background: rgba(255, 255, 255, 0.08); -} - -.toc-item--active { - background: rgba(108, 140, 255, 0.15); - color: var(--reader-accent); - font-weight: 600; -} - -.toc-item-number { - display: flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 24px; - font-size: 0.75rem; - font-weight: 700; - border-radius: 4px; - background: rgba(255, 255, 255, 0.1); - padding: 0 4px; -} - -.toc-item-title { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* --- Settings Panel --- */ -.settings-body { - flex: 1; - overflow-y: auto; - padding: 12px 16px 32px; -} - -.settings-section { - margin-bottom: 20px; -} - -.settings-section-title { - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: rgba(255, 255, 255, 0.5); - margin-bottom: 8px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; -} - -/* Theme grid */ -.theme-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; -} - -.theme-btn { - padding: 10px; - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 8px; - background: transparent; - color: var(--reader-toolbar-text); - font-size: 0.85rem; - cursor: pointer; - transition: all 0.15s; - font-family: inherit; -} - -.theme-btn:hover { - border-color: rgba(255, 255, 255, 0.3); -} - -.theme-btn--active { - border-color: var(--reader-accent); - background: rgba(108, 140, 255, 0.15); - color: var(--reader-accent); - font-weight: 600; -} - -/* Font grid */ -.font-grid, -.orientation-grid { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 8px; -} - -.font-btn, -.orientation-btn { - padding: 10px 8px; - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 8px; - background: transparent; - color: var(--reader-toolbar-text); - font-size: 0.85rem; - cursor: pointer; - transition: all 0.15s; - font-family: inherit; -} - -.font-btn:hover, -.orientation-btn:hover { - border-color: rgba(255, 255, 255, 0.3); -} - -.font-btn--active, -.orientation-btn--active { - border-color: var(--reader-accent); - background: rgba(108, 140, 255, 0.15); - color: var(--reader-accent); - font-weight: 600; -} - -/* Sliders */ -.settings-slider { - width: 100%; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; - outline: none; - cursor: pointer; -} - -.settings-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--reader-accent); - border: 2px solid white; - cursor: pointer; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} - -.settings-slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--reader-accent); - border: 2px solid white; - cursor: pointer; -} - -/* --- Start Reading Button (in book detail) --- */ -.start-reading-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 24px; - background: var(--reader-accent); - border: none; - border-radius: 8px; - color: white; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - margin-top: 16px; - margin-bottom: 16px; - font-family: inherit; -} - -.start-reading-btn:hover { - background: #5a7ae8; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(108, 140, 255, 0.3); -} - -/* --- Loading State --- */ -.reader-loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - padding: 48px; - color: var(--reader-text); -} - -.reader-error { - color: #ef4444; - text-align: center; -} - -/* --- Responsive --- */ -@media (max-width: 480px) { - .reader-chapter { - max-width: 100%; - } - - .reader-nav-btn { - padding: 8px 10px; - font-size: 0.8rem; - } - - .reader-nav-btn span { - display: none; - } - - .toc-drawer, - .settings-drawer { - max-width: 100vw; - } - - .reader-chapter-title { - font-size: 1.3em; - } -} - -@media (min-width: 768px) { - .reader-chapter { - max-width: 720px; - } - - .reader-chapter-body { - font-size: 1.05em; - } -} - -/* --- Landscape on Mobile --- */ -@media (orientation: landscape) and (max-height: 500px) { - .reader-top-bar { - height: 44px; - } - - .reader-content { - padding-top: 60px; - padding-bottom: 60px; - } - - .reader-chapter-title { - font-size: 1.2em; - } -} - -/* --- Dark mode theme override when reader uses light theme but system is dark --- */ -@media (prefers-color-scheme: dark) { - .reader-container[data-theme="light"] { - --reader-toolbar-bg: rgba(255, 255, 255, 0.95); - --reader-toolbar-text: #1a1a1a; - } -} \ No newline at end of file diff --git a/web/src/types/reader.ts b/web/src/types/reader.ts deleted file mode 100644 index d478d9a..0000000 --- a/web/src/types/reader.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * TypeScript interfaces for the Cloud Reader reader module. - * Reading view, settings, chapters, and progress types. - */ - -export interface ChapterSummary { - id: number; - book: number; - title: string; - number: number; -} - -export interface ChapterDetail extends ChapterSummary { - content: string; - created_at: string; - updated_at: string; -} - -export interface ReadingSettings { - font_family: "sans-serif" | "serif" | "monospace"; - font_size: number; - line_height: number; - margin_width: number; - background_color: string; - text_color: string; - brightness: number; - orientation_lock: "auto" | "portrait" | "landscape"; - theme: "sepia" | "dark" | "light" | "paper"; - created_at: string; - updated_at: string; -} - -export type ThemePreset = ReadingSettings["theme"]; -export type FontFamily = ReadingSettings["font_family"]; -export type OrientationLock = ReadingSettings["orientation_lock"]; - -export interface ReadingProgress { - id: number; - book: number; - current_chapter: number; - current_position: number; - percentage: number; - updated_at: string; -} \ No newline at end of file