diff --git a/.gitignore b/.gitignore index 94bf15d..5dfa460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,29 @@ node_modules +dist +frontend/dist +frontend/dist/assets +frontend/dist/assets/index.html +frontend/dist/assets/index.html.gz +frontend/dist/assets/index.html.br +frontend/dist/assets/index.html.brotli +frontend/dist/assets/index.html.gzip +frontend/dist/assets/index.html.br +frontend/dist/assets/index.html.brotli +frontend/dist/assets/index.html.gzip +frontend/dist/assets/index.html.br +frontend/dist/assets/index.html.brotli +frontend/dist/assets/index.html.gzip +frontend/dist/assets/index.html.br +frontend/dist/assets/index.html.brotli +frontend/dist/assets/index.html.gzip +frontend/dist/assets/index.html.br +frontend/dist/assets/index.html.brotli +frontend/dist/assets/index.html.gzip +mobile/dist +backend/media +backend/staticfiles +backend/media +backend/staticfiles .pycache__ __pycache__ *.pyc diff --git a/frontend/dist/assets/ReaderToolbar-wmOf7G3U.js b/frontend/dist/assets/ReaderToolbar-wmOf7G3U.js deleted file mode 100644 index d04c80d..0000000 --- a/frontend/dist/assets/ReaderToolbar-wmOf7G3U.js +++ /dev/null @@ -1 +0,0 @@ -import{X as c,j as e}from"./index-BQiEVoRj.js";function h({bookTitle:s,chapterTitle:l,progress:a,onBack:t,onToggleToc:n,onToggleSettings:o,onToggleMarkers:i}){const{t:r}=c(),d=(a==null?void 0:a.percentage)??0;return e.jsxs(e.Fragment,{children:[e.jsxs("header",{className:"reader-top-bar",children:[t?e.jsx("button",{type:"button",className:"reader-bar-btn",onClick:t,"aria-label":r("reader.backToLibraryAria"),children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M20 12H4M10 18l-6-6 6-6"})})}):e.jsx("button",{type:"button",className:"reader-bar-btn",onClick:n,"aria-label":r("reader.tocAria"),children:e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("line",{x1:"3",y1:"6",x2:"21",y2:"6"}),e.jsx("line",{x1:"3",y1:"12",x2:"21",y2:"12"}),e.jsx("line",{x1:"3",y1:"18",x2:"21",y2:"18"})]})}),e.jsxs("div",{className:"reader-bar-title",children:[e.jsx("span",{className:"reader-bar-book",children:s}),e.jsx("span",{className:"reader-bar-chapter",children:l})]}),e.jsxs("div",{className:"reader-bar-actions",children:[t&&e.jsx("button",{type:"button",className:"reader-bar-btn",onClick:n,"aria-label":r("reader.tocAria"),children:e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("line",{x1:"3",y1:"6",x2:"21",y2:"6"}),e.jsx("line",{x1:"3",y1:"12",x2:"21",y2:"12"}),e.jsx("line",{x1:"3",y1:"18",x2:"21",y2:"18"})]})}),i&&e.jsx("button",{type:"button",className:"reader-bar-btn",onClick:i,"aria-label":r("annotations.inBookPanel"),children:e.jsx("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("path",{d:"M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"})})}),e.jsx("button",{type:"button",className:"reader-bar-btn",onClick:o,"aria-label":r("reader.settingsAria"),children:e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("circle",{cx:"12",cy:"12",r:"3"}),e.jsx("path",{d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"})]})})]})]}),e.jsx("div",{className:"reader-progress-bar",children:e.jsx("div",{className:"reader-progress-fill",style:{width:`${Math.min(d,100)}%`}})})]})}export{h as default}; diff --git a/frontend/dist/assets/ReadingPage-BP5NX83v.css b/frontend/dist/assets/ReadingPage-BP5NX83v.css deleted file mode 100644 index 13a3266..0000000 --- a/frontend/dist/assets/ReadingPage-BP5NX83v.css +++ /dev/null @@ -1 +0,0 @@ -:root{--reader-bg: #f5f0eb;--reader-text: #1a1a1a;--reader-ui-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;--reader-margin: 16px;--reader-brightness: 100%;--reader-toolbar-bg: rgba(26, 26, 26, .95);--reader-toolbar-text: #e0e0e0;--reader-accent: #6c8cff}.reader-container{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--reader-bg);color:var(--reader-text);font-family:var(--reader-ui-font);filter:brightness(var(--reader-brightness));display:flex;flex-direction:column;z-index:100;overflow:hidden}@supports (--reader-orientation: portrait){.reader-container{orientation:var(--reader-orientation)}}@media(orientation:landscape){.reader-container{--reader-margin: 24px}}.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 .15s}.reader-bar-btn:hover{background:#ffffff1a}.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:.8rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-family:var(--reader-ui-font)}.reader-bar-chapter{font-size:.7rem;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;font-family:var(--reader-ui-font)}.reader-bar-actions{display:flex;align-items:center;gap:4px}.reader-progress-bar{position:fixed;top:52px;left:0;right:0;height:3px;background:#ffffff1a;z-index:120}.reader-progress-fill{height:100%;background:var(--reader-accent);transition:width .3s ease}.reader-epub-container{position:fixed;top:55px;left:0;right:0;bottom:0;overflow:hidden;height:calc(100vh - 55px);isolation:isolate}.reader-epub-container>div{height:100%!important}.reader-page-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;border:none;background:none;font-size:48px;line-height:1;padding:0 10px;color:#b8a898e6;cursor:pointer;-webkit-user-select:none;user-select:none}.reader-page-nav:hover{color:#8a7a6a}.reader-page-nav--prev{left:4px}.reader-page-nav--next{right:4px}.reader-container[data-theme=dark] .reader-page-nav{color:#666666e6}.reader-container[data-theme=dark] .reader-page-nav:hover{color:#aaa}.reader-container[data-theme=light] .reader-page-nav,.reader-container[data-theme=paper] .reader-page-nav{color:#cccccce6}.reader-container[data-theme=light] .reader-page-nav:hover,.reader-container[data-theme=paper] .reader-page-nav:hover{color:#888}.reader-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:48px;color:var(--reader-text);min-height:100vh}.reader-loading .spinner{width:36px;height:36px;border:3px solid rgba(108,140,255,.2);border-top-color:var(--reader-accent);border-radius:50%;animation:readerSpin .8s linear infinite}@keyframes readerSpin{to{transform:rotate(360deg)}}.back-button{padding:10px 20px;background:var(--reader-accent);border:none;border-radius:8px;color:#fff;font-size:.9rem;cursor:pointer;font-family:var(--reader-ui-font)}.back-button:hover{background:#5a7ae8}.reader-error{color:#ef4444;text-align:center}.settings-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;z-index:130;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-drawer{position:fixed;top:0;bottom:0;width:300px;max-width:85vw;background:var(--reader-toolbar-bg);color:var(--reader-toolbar-text);font-family:var(--reader-ui-font);z-index:140;display:flex;flex-direction:column;transition:transform .3s cubic-bezier(.4,0,.2,1);right:0;transform:translate(100%)}.settings-drawer--open{transform:translate(0)}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid rgba(255,255,255,.1)}.settings-title{font-size:1.1rem;font-weight:700;font-family:var(--reader-ui-font)}.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 .15s}.settings-close-btn:hover{background:#ffffff1a}.toc-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;z-index:130;animation:fadeIn .2s ease}.toc-drawer{position:fixed;top:0;bottom:0;left:0;width:300px;max-width:85vw;background:var(--reader-toolbar-bg);color:var(--reader-toolbar-text);font-family:var(--reader-ui-font);z-index:140;display:flex;flex-direction:column;transform:translate(-100%);transition:transform .3s cubic-bezier(.4,0,.2,1)}.toc-drawer--open{transform:translate(0)}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid rgba(255,255,255,.1);margin-top:52px}.toc-title{font-size:1.1rem;font-weight:700;font-family:var(--reader-ui-font)}.toc-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 .15s}.toc-close-btn:hover{background:#ffffff1a}.toc-list{flex:1;overflow-y:auto;padding:8px 0}.toc-empty{padding:24px 16px;text-align:center;opacity:.6;font-size:.9rem}.toc-item{display:flex;align-items:center;width:100%;padding:10px 16px;background:transparent;border:none;color:var(--reader-toolbar-text);font-size:.9rem;text-align:left;cursor:pointer;transition:background .15s;font-family:var(--reader-ui-font)}.toc-item:hover{background:#ffffff14}.toc-item-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.settings-body{flex:1;overflow-y:auto;padding:12px 16px 32px}.settings-section{margin-bottom:20px}.settings-section-title{font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#ffffff80;margin-bottom:8px;font-family:var(--reader-ui-font)}.theme-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.theme-btn{padding:10px;border:1px solid rgba(255,255,255,.15);border-radius:8px;background:transparent;color:var(--reader-toolbar-text);font-size:.85rem;cursor:pointer;transition:all .15s;font-family:var(--reader-ui-font)}.theme-btn:hover{border-color:#ffffff4d}.theme-btn--active{border-color:var(--reader-accent);background:#6c8cff26;color:var(--reader-accent);font-weight:600}.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,.15);border-radius:8px;background:transparent;color:var(--reader-toolbar-text);font-size:.85rem;cursor:pointer;transition:all .15s;font-family:var(--reader-ui-font)}.font-btn:hover,.orientation-btn:hover{border-color:#ffffff4d}.font-btn--active,.orientation-btn--active{border-color:var(--reader-accent);background:#6c8cff26;color:var(--reader-accent);font-weight:600}.settings-slider{width:100%;height:6px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff3;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 #0000004d}.settings-slider::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:var(--reader-accent);border:2px solid white;cursor:pointer}.start-reading-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 24px;background:var(--reader-accent);border:none;border-radius:8px;color:#fff;font-size:1rem;font-weight:600;cursor:pointer;transition:all .2s;margin-top:16px;margin-bottom:16px;font-family:var(--reader-ui-font)}.start-reading-btn:hover{background:#5a7ae8;transform:translateY(-1px);box-shadow:0 4px 12px #6c8cff4d}@media(max-width:480px){.settings-drawer,.toc-drawer{max-width:100vw}.reader-epub-container{top:52px;height:calc(100vh - 52px)}}@media(orientation:landscape)and (max-height:500px){.reader-top-bar{height:44px}.reader-epub-container{top:47px;height:calc(100vh - 47px)}}.selection-popover-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:transparent}.selection-popover{position:fixed;z-index:210;width:min(280px,calc(100vw - 24px));padding:12px;background:#fff;border-radius:10px;box-shadow:0 8px 32px #0000002e;border:1px solid #e5e7eb}.selection-popover-quote{font-size:.8125rem;font-style:italic;color:#6b7280;margin:0 0 8px;line-height:1.4}.selection-popover-input{width:100%;box-sizing:border-box;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:.875rem;resize:vertical;font-family:var(--reader-ui-font)}.selection-popover-actions{display:flex;gap:8px;margin-top:8px}.reader-panel-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:140;background:#00000059}.reader-side-panel{position:fixed;top:55px;right:0;bottom:0;width:min(360px,100vw);z-index:150;background:#fff;box-shadow:-4px 0 24px #0000001f;display:flex;flex-direction:column}.reader-panel-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #eee}.reader-panel-header h2{margin:0;font-size:1rem;font-weight:700}.reader-panel-close{border:none;background:none;font-size:1.5rem;cursor:pointer;line-height:1;color:#666}.reader-panel-body{flex:1;overflow-y:auto;padding:12px 16px}@media(prefers-color-scheme:dark){.reader-container[data-theme=light]{--reader-toolbar-bg: rgba(255, 255, 255, .95);--reader-toolbar-text: #1a1a1a}} diff --git a/frontend/dist/assets/ReadingSettingsPanel-Bt3-VHc7.js b/frontend/dist/assets/ReadingSettingsPanel-Bt3-VHc7.js deleted file mode 100644 index 3fc33f2..0000000 --- a/frontend/dist/assets/ReadingSettingsPanel-Bt3-VHc7.js +++ /dev/null @@ -1 +0,0 @@ -import{X as m,j as e}from"./index-BQiEVoRj.js";const g=[{value:"sepia",labelKey:"reader.themeSepia"},{value:"dark",labelKey:"reader.themeDark"},{value:"light",labelKey:"reader.themeLight"},{value:"paper",labelKey:"reader.themePaper"}],u=[{value:"sans-serif",labelKey:"reader.fontSans"},{value:"serif",labelKey:"reader.fontSerif"},{value:"monospace",labelKey:"reader.fontMonospace"}],x=[{value:"auto",labelKey:"reader.orientationAuto"},{value:"portrait",labelKey:"reader.orientationPortrait"},{value:"landscape",labelKey:"reader.orientationLandscape"}];function v({settings:t,isOpen:c,onClose:d,onUpdate:o,onFlush:h}){const{t:s}=m(),n=()=>{h(),d()},i=(a,r)=>{o({[a]:r},"debounced")},l=(a,r)=>{o({[a]:r},"immediate")};return e.jsxs(e.Fragment,{children:[c&&e.jsx("div",{className:"settings-overlay",onClick:n,onKeyDown:a=>{a.key==="Escape"&&n()},role:"presentation"}),e.jsxs("aside",{className:`settings-drawer ${c?"settings-drawer--open":""}`,children:[e.jsxs("div",{className:"settings-header",children:[e.jsx("h2",{className:"settings-title",children:s("reader.settingsTitle")}),e.jsx("button",{type:"button",className:"settings-close-btn",onClick:n,"aria-label":s("reader.closeSettingsAria"),children:e.jsxs("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),e.jsx("line",{x1:"6",y1:"6",x2:"18",y2:"18"})]})})]}),e.jsxs("div",{className:"settings-body",children:[e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.theme")}),e.jsx("div",{className:"theme-grid",children:g.map(a=>e.jsx("button",{type:"button",className:`theme-btn ${t.theme===a.value?"theme-btn--active":""}`,onClick:()=>l("theme",a.value),children:s(a.labelKey)},a.value))})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.font")}),e.jsx("div",{className:"font-grid",children:u.map(a=>e.jsx("button",{type:"button",className:`font-btn ${t.font_family===a.value?"font-btn--active":""}`,onClick:()=>l("font_family",a.value),children:s(a.labelKey)},a.value))})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.fontSize",{size:String(t.font_size)})}),e.jsx("input",{type:"range",min:"12",max:"32",value:t.font_size,onChange:a=>i("font_size",Number(a.target.value)),className:"settings-slider","aria-label":s("reader.fontSizeAria")})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.lineHeight",{value:t.line_height.toFixed(1)})}),e.jsx("input",{type:"range",min:"1.2",max:"2.0",step:"0.1",value:t.line_height,onChange:a=>i("line_height",Number(a.target.value)),className:"settings-slider","aria-label":s("reader.lineHeightAria")})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.margins",{value:String(t.margin_width)})}),e.jsx("input",{type:"range",min:"8",max:"48",value:t.margin_width,onChange:a=>i("margin_width",Number(a.target.value)),className:"settings-slider","aria-label":s("reader.marginWidthAria")})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.brightness",{value:String(t.brightness)})}),e.jsx("input",{type:"range",min:"0",max:"100",value:t.brightness,onChange:a=>i("brightness",Number(a.target.value)),className:"settings-slider","aria-label":s("reader.brightnessAria")})]}),e.jsxs("section",{className:"settings-section",children:[e.jsx("h3",{className:"settings-section-title",children:s("reader.orientation")}),e.jsx("div",{className:"orientation-grid",children:x.map(a=>e.jsx("button",{type:"button",className:`orientation-btn ${t.orientation_lock===a.value?"orientation-btn--active":""}`,onClick:()=>l("orientation_lock",a.value),children:s(a.labelKey)},a.value))})]})]})]})]})}export{v as default}; diff --git a/frontend/package.json b/frontend/package.json index 6d65c73..5b88604 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,13 +12,12 @@ }, "dependencies": { "axios": "^1.7.9", - "dompurify": "^3.4.7", + "react-router-dom": "^7.1.0", "pdfjs-dist": "^4.10.38", "react": "^19.0.0", "react-dom": "^19.0.0", "react-i18n-lite": "^1.0.10", - "react-reader": "^2.0.15", - "react-router-dom": "^7.1.0" + "react-reader": "^2.0.15" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9dd51ba..45ba735 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React, { lazy, Suspense, useState } from "react"; +import React, { lazy, Suspense } from "react"; import { BrowserRouter, Navigate, Route, Routes, useParams } from "react-router-dom"; import { AuthProvider, useAuth } from "./context/AuthContext"; import { I18nProvider } from "./i18n/I18nProvider"; @@ -13,14 +13,7 @@ const SettingsPage = lazy(() => import("./pages/Settings").then((m) => ({ defaul const BookmarksNotesPage = lazy(() => import("./components/annotations/BookmarksNotesPage").then((m) => ({ default: m.BookmarksNotesPage }))); const ReadingPage = lazy(() => import("./pages/ReadingPage").then((m) => ({ default: m.default }))); -const AuthPage = lazy(() => - import("./pages/AuthPage").then((m) => ({ - default: () => { - const [isLogin, setIsLogin] = useState(true); - return isLogin ? setIsLogin(false)} /> : setIsLogin(true)} />; - }, - })), -); +const AuthPage = lazy(() => import("./pages/AuthPage")); function LoadingFallback() { const { t } = useTranslation(); diff --git a/frontend/src/api/annotations.ts b/frontend/src/api/annotations.ts index c16efb9..c86c125 100644 --- a/frontend/src/api/annotations.ts +++ b/frontend/src/api/annotations.ts @@ -27,6 +27,3 @@ export async function createMarker( export async function deleteBookmark(id: string): Promise { await api.delete(`/annotations/bookmarks/${id}/`); } - -/** @deprecated Use createMarker */ -export const createBookmark = createMarker; diff --git a/frontend/src/api/loadEbookWithProgress.ts b/frontend/src/api/loadEbookWithProgress.ts new file mode 100644 index 0000000..2bd4e01 --- /dev/null +++ b/frontend/src/api/loadEbookWithProgress.ts @@ -0,0 +1,14 @@ +import { booksApi } from "@/api/books"; +import { getReadingProgress } from "@/api/reader"; +import type { ReadingProgress } from "@/types/reader"; + +export async function loadEbookWithProgress(bookId: number): Promise<{ + progressData: ReadingProgress | null; + blob: Blob; +}> { + const [progressData, blob] = await Promise.all([ + getReadingProgress(bookId).catch(() => null), + booksApi.getEbookFile(bookId), + ]); + return { progressData, blob }; +} diff --git a/frontend/src/api/reader.ts b/frontend/src/api/reader.ts index bbe467e..1263852 100644 --- a/frontend/src/api/reader.ts +++ b/frontend/src/api/reader.ts @@ -1,22 +1,10 @@ /** - * API client for the reader module — reading settings, chapters, and progress. + * API client for the reader module — reading settings and progress. * Uses the shared axios client so JWT auth is attached automatically. */ import api from "./client"; -import type { - ChapterDetail, - ChapterSummary, - ReadingProgress, - ReadingSettings, -} from "../types/reader"; - -interface TocChapter { - id: number; - title: string; - index: number; - href?: string; -} +import type { ReadingProgress, ReadingSettings } from "../types/reader"; export async function getReadingSettings(): Promise { const { data } = await api.get("/reader/settings/"); @@ -30,39 +18,6 @@ export async function updateReadingSettings( return data; } -export async function getChapters(bookId: number): Promise { - const { data } = await api.get<{ chapters: TocChapter[] }>( - `/books/ebooks/${bookId}/toc/`, - ); - const chapters = data.chapters ?? []; - return chapters.map((ch) => ({ - id: ch.id, - book: bookId, - title: ch.title, - number: ch.index + 1, - })); -} - -export async function getChapterContent( - bookId: number, - chapterNumber: number, -): Promise { - const { data } = await api.get<{ - page: number; - chapter_title: string; - content: string; - }>(`/books/ebooks/${bookId}/content/`, { params: { page: chapterNumber } }); - return { - id: chapterNumber, - book: bookId, - title: data.chapter_title ?? "", - number: data.page ?? chapterNumber, - content: data.content ?? "", - created_at: "", - updated_at: "", - }; -} - export async function getReadingProgress( bookId: number, ): Promise { diff --git a/frontend/src/components/annotations/MarkerPassageActions.tsx b/frontend/src/components/annotations/MarkerPassageActions.tsx new file mode 100644 index 0000000..d4dbe4d --- /dev/null +++ b/frontend/src/components/annotations/MarkerPassageActions.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18n-lite"; + +interface MarkerPassageActionsProps { + onGoToPassage: () => void; + onDelete: () => void; +} + +export function MarkerPassageActions({ onGoToPassage, onDelete }: MarkerPassageActionsProps) { + const { t } = useTranslation(); + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/annotations/MarkerThreadsView.tsx b/frontend/src/components/annotations/MarkerThreadsView.tsx index 27c85e4..19eba5b 100644 --- a/frontend/src/components/annotations/MarkerThreadsView.tsx +++ b/frontend/src/components/annotations/MarkerThreadsView.tsx @@ -6,6 +6,7 @@ import { CollapsibleMarkerPassage, CollapsibleMarkerThought, } from "@/components/annotations/CollapsibleMarkerText"; +import { MarkerPassageActions } from "@/components/annotations/MarkerPassageActions"; interface MarkerThreadsViewProps { ebookIdFilter?: string; @@ -91,22 +92,10 @@ export function MarkerThreadsView({ ) : ( {t("annotations.bookmarkOnly")} )} -
- - -
+ onGoToPassage(m.ebook_id, m.epub_cfi)} + onDelete={() => removeBookmark(m.id)} + /> ))} diff --git a/frontend/src/components/annotations/index.ts b/frontend/src/components/annotations/index.ts deleted file mode 100644 index 58d16b2..0000000 --- a/frontend/src/components/annotations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BookmarksNotesPage } from "./BookmarksNotesPage"; -export { MarkerThreadsView } from "./MarkerThreadsView"; diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx deleted file mode 100644 index e226627..0000000 --- a/frontend/src/components/layout/Layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18n-lite"; - -interface LayoutProps { - children: React.ReactNode; - title?: string; -} - -export function Layout({ - children, - title, -}: LayoutProps): React.ReactElement { - const { t } = useTranslation(); - const pageTitle = title ?? t("common.appName"); - - return ( -
-
-

{pageTitle}

- -
-
{children}
-
- ); -} diff --git a/frontend/src/components/layout/SimpleFormPageLayout.tsx b/frontend/src/components/layout/SimpleFormPageLayout.tsx new file mode 100644 index 0000000..3eb5401 --- /dev/null +++ b/frontend/src/components/layout/SimpleFormPageLayout.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18n-lite"; + +interface SimpleFormPageLayoutProps { + title: string; + onBack: () => void; + children: ReactNode; +} + +export function SimpleFormPageLayout({ title, onBack, children }: SimpleFormPageLayoutProps) { + const { t } = useTranslation(); + return ( +
+
+ +

{title}

+
+ {children} +
+ ); +} diff --git a/frontend/src/components/layout/index.ts b/frontend/src/components/layout/index.ts deleted file mode 100644 index caf75aa..0000000 --- a/frontend/src/components/layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Layout } from "./Layout"; \ No newline at end of file diff --git a/frontend/src/components/library/FinishedBooksShelf.tsx b/frontend/src/components/library/FinishedBooksShelf.tsx index 09987db..a309339 100644 --- a/frontend/src/components/library/FinishedBooksShelf.tsx +++ b/frontend/src/components/library/FinishedBooksShelf.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18n-lite"; import styles from "./FinishedBooksShelf.module.css"; -export interface ShelfBook { +interface ShelfBook { id: number; title: string; author: string; diff --git a/frontend/src/components/library/LibraryBookCard.tsx b/frontend/src/components/library/LibraryBookCard.tsx index bac9be1..8e4ef91 100644 --- a/frontend/src/components/library/LibraryBookCard.tsx +++ b/frontend/src/components/library/LibraryBookCard.tsx @@ -2,14 +2,14 @@ import { useTranslation } from "react-i18n-lite"; import { readingStatusKey } from "@/locales"; import styles from "../../pages/Library.module.css"; -export const LIBRARY_STATUS_COLORS: Record = { +const LIBRARY_STATUS_COLORS: Record = { want_to_read: { bg: "#dbeafe", text: "#1d4ed8" }, reading: { bg: "#dcfce7", text: "#16a34a" }, finished: { bg: "#f3e8ff", text: "#9333ea" }, dnf: { bg: "#fef3c7", text: "#b45309" }, }; -export interface LibraryBookCardData { +interface LibraryBookCardData { id: number; title: string; author: string; diff --git a/frontend/src/components/reader/BookMarkersPanel.tsx b/frontend/src/components/reader/BookMarkersPanel.tsx index e7252a6..63fdd26 100644 --- a/frontend/src/components/reader/BookMarkersPanel.tsx +++ b/frontend/src/components/reader/BookMarkersPanel.tsx @@ -6,6 +6,7 @@ import { CollapsibleMarkerPassage, CollapsibleMarkerThought, } from "@/components/annotations/CollapsibleMarkerText"; +import { MarkerPassageActions } from "@/components/annotations/MarkerPassageActions"; interface BookMarkersPanelProps { ebookId: number; @@ -67,18 +68,10 @@ export function BookMarkersPanel({ ) : ( {t("annotations.bookmarkOnly")} )} -
- - -
+ onGoToPassage(m)} + onDelete={() => removeBookmark(m.id)} + /> ))} diff --git a/frontend/src/components/reader/EpubReadingView.tsx b/frontend/src/components/reader/EpubReadingView.tsx index bb497c6..e864a5b 100644 --- a/frontend/src/components/reader/EpubReadingView.tsx +++ b/frontend/src/components/reader/EpubReadingView.tsx @@ -2,7 +2,7 @@ * EpubReadingView — full-screen EPUB reading powered by react-reader (epub.js). */ -import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { lazy, useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18n-lite"; import { EpubView, EpubViewStyle } from "react-reader"; @@ -12,6 +12,10 @@ import { useEpubHighlights } from "../../hooks/useEpubHighlights"; import { useEpubReader } from "../../hooks/useEpubReader"; import { useEpubSelection } from "../../hooks/useEpubSelection"; import { useReadingSettings } from "../../hooks/useReadingSettings"; +import { useReaderOrientationCss } from "../../hooks/useReaderOrientationCss"; +import { ReaderErrorScreen } from "./ReaderErrorScreen"; +import { ReaderLoadingScreen } from "./ReaderLoadingScreen"; +import { ReaderSuspenseShell } from "./ReaderSuspenseShell"; import type { EBookDetail } from "../../types/book"; import type { EpubTocItem } from "./TableOfContents"; import { SelectionPopover } from "./SelectionPopover"; @@ -82,17 +86,7 @@ export function EpubReadingView({ book, bookId, initialEpubLocation }: EpubReadi applySettings(settings); }, [settings, applySettings]); - 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]); + useReaderOrientationCss(settings.orientation_lock); const handleTocChanged = useCallback((toc: EpubTocItem[]) => { setTocItems(toc); @@ -122,34 +116,20 @@ export function EpubReadingView({ book, bookId, initialEpubLocation }: EpubReadi ); if (epubLoading) { - return ( -
-
-

{t("reader.loading")}

-
- ); + return ; } if (epubError || !epubUrl) { return ( -
-

{epubError ?? t("reader.unableToOpen")}

- -
+ navigate("/")} + /> ); } return ( - -
-
- } - > -
+ -
-
+ ); } diff --git a/frontend/src/components/reader/PanelCloseButton.tsx b/frontend/src/components/reader/PanelCloseButton.tsx new file mode 100644 index 0000000..4398217 --- /dev/null +++ b/frontend/src/components/reader/PanelCloseButton.tsx @@ -0,0 +1,16 @@ +interface PanelCloseButtonProps { + className: string; + onClick: () => void; + ariaLabel: string; +} + +export function PanelCloseButton({ className, onClick, ariaLabel }: PanelCloseButtonProps) { + return ( + + ); +} diff --git a/frontend/src/components/reader/PdfReadingView.tsx b/frontend/src/components/reader/PdfReadingView.tsx index f3b7e5e..0ba6a21 100644 --- a/frontend/src/components/reader/PdfReadingView.tsx +++ b/frontend/src/components/reader/PdfReadingView.tsx @@ -2,13 +2,17 @@ * PdfReadingView — full-screen PDF reading powered by PDF.js. */ -import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { lazy, useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18n-lite"; import { useAnnotations } from "@/context/AnnotationsContext"; import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout"; import { usePdfReader } from "../../hooks/usePdfReader"; import { useReadingSettings } from "../../hooks/useReadingSettings"; +import { useReaderOrientationCss } from "../../hooks/useReaderOrientationCss"; +import { ReaderErrorScreen } from "./ReaderErrorScreen"; +import { ReaderLoadingScreen } from "./ReaderLoadingScreen"; +import { ReaderSuspenseShell } from "./ReaderSuspenseShell"; import { useToast } from "../../hooks/useToast"; import { booksApi } from "../../api/books"; import type { EBookDetail } from "../../types/book"; @@ -96,17 +100,7 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr ); }, [bookId, pageCount, t]); - 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]); + useReaderOrientationCss(settings.orientation_lock); const handleTocNavigate = useCallback( (href: string) => { @@ -165,36 +159,17 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr }, [bookMarkers, currentPage]); if (isLoading) { - return ( -
-
-

{t("reader.loading")}

-
- ); + return ; } if (error || !pdfDocument) { const message = error === "PDF_PASSWORD" ? t("reader.pdfPassword") : (error ?? t("reader.unableToOpen")); - return ( -
-

{message}

- -
- ); + return navigate("/")} />; } return ( - -
-
- } - > -
+ -
-
+ ); } diff --git a/frontend/src/components/reader/ReaderErrorScreen.tsx b/frontend/src/components/reader/ReaderErrorScreen.tsx new file mode 100644 index 0000000..bc9ef49 --- /dev/null +++ b/frontend/src/components/reader/ReaderErrorScreen.tsx @@ -0,0 +1,18 @@ +import { useTranslation } from "react-i18n-lite"; + +interface ReaderErrorScreenProps { + message: string; + onBack: () => void; +} + +export function ReaderErrorScreen({ message, onBack }: ReaderErrorScreenProps) { + const { t } = useTranslation(); + return ( +
+

{message}

+ +
+ ); +} diff --git a/frontend/src/components/reader/ReaderLoadingScreen.tsx b/frontend/src/components/reader/ReaderLoadingScreen.tsx new file mode 100644 index 0000000..fbcb8ff --- /dev/null +++ b/frontend/src/components/reader/ReaderLoadingScreen.tsx @@ -0,0 +1,15 @@ +import { useTranslation } from "react-i18n-lite"; + +interface ReaderLoadingScreenProps { + showMessage?: boolean; +} + +export function ReaderLoadingScreen({ showMessage = true }: ReaderLoadingScreenProps) { + const { t } = useTranslation(); + return ( +
+
+ {showMessage &&

{t("reader.loading")}

} +
+ ); +} diff --git a/frontend/src/components/reader/ReaderSuspenseShell.tsx b/frontend/src/components/reader/ReaderSuspenseShell.tsx new file mode 100644 index 0000000..104ff0a --- /dev/null +++ b/frontend/src/components/reader/ReaderSuspenseShell.tsx @@ -0,0 +1,17 @@ +import { Suspense, type ReactNode } from "react"; +import { ReaderLoadingScreen } from "./ReaderLoadingScreen"; + +interface ReaderSuspenseShellProps { + theme: string; + children: ReactNode; +} + +export function ReaderSuspenseShell({ theme, children }: ReaderSuspenseShellProps) { + return ( + }> +
+ {children} +
+
+ ); +} diff --git a/frontend/src/components/reader/ReaderTocButton.tsx b/frontend/src/components/reader/ReaderTocButton.tsx new file mode 100644 index 0000000..7275546 --- /dev/null +++ b/frontend/src/components/reader/ReaderTocButton.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from "react-i18n-lite"; + +interface ReaderTocButtonProps { + onClick: () => void; +} + +export function ReaderTocButton({ onClick }: ReaderTocButtonProps) { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/frontend/src/components/reader/ReaderToolbar.tsx b/frontend/src/components/reader/ReaderToolbar.tsx index 862dad5..6ab6baa 100644 --- a/frontend/src/components/reader/ReaderToolbar.tsx +++ b/frontend/src/components/reader/ReaderToolbar.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18n-lite"; import type { ReadingProgress } from "../../types/reader"; +import { ReaderTocButton } from "./ReaderTocButton"; interface ReaderToolbarProps { bookTitle: string; @@ -47,38 +48,14 @@ export default function ReaderToolbar({ ) : ( - + )}
{bookTitle} {chapterTitle}
- {onBack && ( - - )} + {onBack && } {onBookmarkPage && ( + ariaLabel={t("reader.closeSettingsAria")} + />
@@ -124,7 +119,7 @@ export default function ReadingSettingsPanel({ {format === "pdf" && onPdfScaleChange && (

- {t("reader.pdfZoom", { value: Math.round(pdfScale * 100) })} + {t("reader.pdfZoom", { value: String(Math.round(pdfScale * 100)) })}

{t("reader.tocTitle")}

- + ariaLabel={t("reader.closeTocAria")} + />