diff --git a/templates/reader.templ b/templates/reader.templ index 9f4f9b0..3c7fd99 100644 --- a/templates/reader.templ +++ b/templates/reader.templ @@ -5,14 +5,20 @@ import ( "fmt" ) -func readerInitExpr(metadata ReaderMetadata) string { - config := map[string]string{ +func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string { + config := map[string]interface{}{ "mediaItemId": metadata.MediaItemID, "fileUrl": metadata.FileURL, "formatGroup": metadata.FormatGroup, "readingDirection": metadata.ReadingDirection, "mangaType": metadata.MangaType, } + if progress.Percentage > 0 { + config["savedPercentage"] = progress.Percentage + } + if progress.EpubCfi != "" { + config["savedCfi"] = progress.EpubCfi + } jsonBytes, _ := json.Marshal(config) return fmt.Sprintf("initReader(%s)", string(jsonBytes)) } @@ -32,7 +38,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm @ReaderChrome(user, metadata, progress) diff --git a/templates/reader_templ.go b/templates/reader_templ.go index 78c3c8d..800d00d 100644 --- a/templates/reader_templ.go +++ b/templates/reader_templ.go @@ -13,14 +13,20 @@ import ( "fmt" ) -func readerInitExpr(metadata ReaderMetadata) string { - config := map[string]string{ +func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string { + config := map[string]interface{}{ "mediaItemId": metadata.MediaItemID, "fileUrl": metadata.FileURL, "formatGroup": metadata.FormatGroup, "readingDirection": metadata.ReadingDirection, "mangaType": metadata.MangaType, } + if progress.Percentage > 0 { + config["savedPercentage"] = progress.Percentage + } + if progress.EpubCfi != "" { + config["savedCfi"] = progress.EpubCfi + } jsonBytes, _ := json.Marshal(config) return fmt.Sprintf("initReader(%s)", string(jsonBytes)) } @@ -53,7 +59,7 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 26, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 32, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -64,9 +70,9 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(readerInitExpr(metadata)) + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(readerInitExpr(metadata, progress)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 35, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 41, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -156,7 +162,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) var templ_7745c5c3_Var5 templ.SafeURL templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 73, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 79, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -169,7 +175,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 76, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 82, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -182,7 +188,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress) var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 152, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 158, Col: 70} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -321,7 +327,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 368, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 374, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -334,7 +340,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 371, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 377, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -347,7 +353,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 373, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 379, Col: 27} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index b167328..8f5459f 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -22,28 +22,138 @@ interface FontFile { } const FONT_FILES: FontFile[] = [ - { family: '"Literata"', url: "/static/fonts/literata/Literata-Variable.woff2", weight: "100 900", style: "normal" }, - { family: '"Literata"', url: "/static/fonts/literata/Literata-Italic-Variable.woff2", weight: "100 900", style: "italic" }, - { family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Variable.woff2", weight: "100 900", style: "normal" }, - { family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2", weight: "100 900", style: "italic" }, - { family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Variable.woff2", weight: "100 900", style: "normal" }, - { family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2", weight: "100 900", style: "italic" }, - { family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Variable.woff2", weight: "100 900", style: "normal" }, - { family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2", weight: "100 900", style: "italic" }, - { family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Regular.woff2", weight: "400", style: "normal" }, - { family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Bold.woff2", weight: "700", style: "normal" }, - { family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Italic.woff2", weight: "400", style: "italic" }, - { family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2", weight: "700", style: "italic" }, - { family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Variable.woff2", weight: "100 900", style: "normal" }, - { family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2", weight: "100 900", style: "italic" }, - { family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Regular.woff2", weight: "400", style: "normal" }, - { family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Bold.woff2", weight: "700", style: "normal" }, - { family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Italic.woff2", weight: "400", style: "italic" }, - { family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2", weight: "700", style: "italic" }, - { family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2", weight: "400", style: "normal" }, - { family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2", weight: "700", style: "normal" }, - { family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2", weight: "400", style: "italic" }, - { family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2", weight: "700", style: "italic" }, + { + family: '"Literata"', + url: "/static/fonts/literata/Literata-Variable.woff2", + weight: "100 900", + style: "normal", + }, + { + family: '"Literata"', + url: "/static/fonts/literata/Literata-Italic-Variable.woff2", + weight: "100 900", + style: "italic", + }, + { + family: '"Crimson Pro"', + url: "/static/fonts/crimson/CrimsonPro-Variable.woff2", + weight: "100 900", + style: "normal", + }, + { + family: '"Crimson Pro"', + url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2", + weight: "100 900", + style: "italic", + }, + { + family: '"Source Serif 4"', + url: "/static/fonts/source-serif/SourceSerif4-Variable.woff2", + weight: "100 900", + style: "normal", + }, + { + family: '"Source Serif 4"', + url: "/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2", + weight: "100 900", + style: "italic", + }, + { + family: '"EB Garamond"', + url: "/static/fonts/eb-garamond/EBGaramond-Variable.woff2", + weight: "100 900", + style: "normal", + }, + { + family: '"EB Garamond"', + url: "/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2", + weight: "100 900", + style: "italic", + }, + { + family: '"Libertinus Serif"', + url: "/static/fonts/libertinus/LibertinusSerif-Regular.woff2", + weight: "400", + style: "normal", + }, + { + family: '"Libertinus Serif"', + url: "/static/fonts/libertinus/LibertinusSerif-Bold.woff2", + weight: "700", + style: "normal", + }, + { + family: '"Libertinus Serif"', + url: "/static/fonts/libertinus/LibertinusSerif-Italic.woff2", + weight: "400", + style: "italic", + }, + { + family: '"Libertinus Serif"', + url: "/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2", + weight: "700", + style: "italic", + }, + { + family: '"Noto Serif"', + url: "/static/fonts/noto-serif/NotoSerif-Variable.woff2", + weight: "100 900", + style: "normal", + }, + { + family: '"Noto Serif"', + url: "/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2", + weight: "100 900", + style: "italic", + }, + { + family: '"Charis SIL"', + url: "/static/fonts/charis-sil/CharisSIL-Regular.woff2", + weight: "400", + style: "normal", + }, + { + family: '"Charis SIL"', + url: "/static/fonts/charis-sil/CharisSIL-Bold.woff2", + weight: "700", + style: "normal", + }, + { + family: '"Charis SIL"', + url: "/static/fonts/charis-sil/CharisSIL-Italic.woff2", + weight: "400", + style: "italic", + }, + { + family: '"Charis SIL"', + url: "/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2", + weight: "700", + style: "italic", + }, + { + family: '"IBM Plex Serif"', + url: "/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2", + weight: "400", + style: "normal", + }, + { + family: '"IBM Plex Serif"', + url: "/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2", + weight: "700", + style: "normal", + }, + { + family: '"IBM Plex Serif"', + url: "/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2", + weight: "400", + style: "italic", + }, + { + family: '"IBM Plex Serif"', + url: "/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2", + weight: "700", + style: "italic", + }, ]; let fontBlobUrls: Record = {}; @@ -87,80 +197,81 @@ interface ThemeColors { link: string; } -const THEME_COLORS: Record = { - light: { - light: { fg: "#1a1a1a", bg: "#fafafa", link: "#0066cc" }, - dark: { fg: "#e8e8e8", bg: "#1a1a1a", link: "#60a5fa" }, - }, - paper: { - light: { fg: "#2d2d2d", bg: "#fdfbf7", link: "#0284c7" }, - dark: { fg: "#ebe5dd", bg: "#1c1917", link: "#38bdf8" }, - }, - sepia: { - light: { fg: "#4a3728", bg: "#f4ecd8", link: "#8b4513" }, - dark: { fg: "#d4c4a8", bg: "#2b2420", link: "#d97706" }, - }, - parchment: { - light: { fg: "#5c4033", bg: "#f0e6d3", link: "#92400e" }, - dark: { fg: "#e8dcc8", bg: "#3d2b1f", link: "#b45309" }, - }, - warm: { - light: { fg: "#2b2b2b", bg: "#faf8f0", link: "#d97706" }, - dark: { fg: "#f5e6d3", bg: "#2d2416", link: "#fbbf24" }, - }, - candlelight: { - light: { fg: "#3d2b1f", bg: "#fef3c7", link: "#b45309" }, - dark: { fg: "#fde68a", bg: "#451a03", link: "#fb923c" }, - }, - azure: { - light: { fg: "#262d48", bg: "#cedef5", link: "#2d53e5" }, - dark: { fg: "#babee1", bg: "#282e47", link: "#0ea5e9" }, - }, - sky: { - light: { fg: "#1e3a5f", bg: "#e0f2fe", link: "#0284c7" }, - dark: { fg: "#bae6fd", bg: "#0c4a6e", link: "#0ea5e9" }, - }, - arctic: { - light: { fg: "#1e3a5f", bg: "#f0f9ff", link: "#0284c7" }, - dark: { fg: "#bfdbfe", bg: "#1e293b", link: "#38bdf8" }, - }, - frost: { - light: { fg: "#334155", bg: "#f8fafc", link: "#38bdf8" }, - dark: { fg: "#e0f2fe", bg: "#0f172a", link: "#7dd3fc" }, - }, - dusk: { - light: { fg: "#3d2914", bg: "#fef3e2", link: "#ea580c" }, - dark: { fg: "#f5d5b8", bg: "#2d1f14", link: "#fb923c" }, - }, - sunset: { - light: { fg: "#4a1d1d", bg: "#fff7ed", link: "#f97316" }, - dark: { fg: "#fed7aa", bg: "#431407", link: "#fb923c" }, - }, - twilight: { - light: { fg: "#4c1d95", bg: "#f5f3ff", link: "#8b5cf6" }, - dark: { fg: "#ddd6fe", bg: "#2e1065", link: "#a78bfa" }, - }, - forest: { - light: { fg: "#1a2e1a", bg: "#e8efe8", link: "#15803d" }, - dark: { fg: "#c8dcc8", bg: "#1a2e1a", link: "#22c55e" }, - }, - moss: { - light: { fg: "#14532d", bg: "#dcfce7", link: "#22c55e" }, - dark: { fg: "#86efac", bg: "#052e16", link: "#4ade80" }, - }, - slate: { - light: { fg: "#334155", bg: "#f8fafc", link: "#475569" }, - dark: { fg: "#e2e8f0", bg: "#1e293b", link: "#94a3b8" }, - }, - oled: { - light: { fg: "#000000", bg: "#ffffff", link: "#0066cc" }, - dark: { fg: "#ffffff", bg: "#000000", link: "#3b82f6" }, - }, - solarized: { - light: { fg: "#657b83", bg: "#fdf6e3", link: "#268bd2" }, - dark: { fg: "#839496", bg: "#002b36", link: "#268bd2" }, - }, -}; +const THEME_COLORS: Record = + { + light: { + light: { fg: "#1a1a1a", bg: "#fafafa", link: "#0066cc" }, + dark: { fg: "#e8e8e8", bg: "#1a1a1a", link: "#60a5fa" }, + }, + paper: { + light: { fg: "#2d2d2d", bg: "#fdfbf7", link: "#0284c7" }, + dark: { fg: "#ebe5dd", bg: "#1c1917", link: "#38bdf8" }, + }, + sepia: { + light: { fg: "#4a3728", bg: "#f4ecd8", link: "#8b4513" }, + dark: { fg: "#d4c4a8", bg: "#2b2420", link: "#d97706" }, + }, + parchment: { + light: { fg: "#5c4033", bg: "#f0e6d3", link: "#92400e" }, + dark: { fg: "#e8dcc8", bg: "#3d2b1f", link: "#b45309" }, + }, + warm: { + light: { fg: "#2b2b2b", bg: "#faf8f0", link: "#d97706" }, + dark: { fg: "#f5e6d3", bg: "#2d2416", link: "#fbbf24" }, + }, + candlelight: { + light: { fg: "#3d2b1f", bg: "#fef3c7", link: "#b45309" }, + dark: { fg: "#fde68a", bg: "#451a03", link: "#fb923c" }, + }, + azure: { + light: { fg: "#262d48", bg: "#cedef5", link: "#2d53e5" }, + dark: { fg: "#babee1", bg: "#282e47", link: "#0ea5e9" }, + }, + sky: { + light: { fg: "#1e3a5f", bg: "#e0f2fe", link: "#0284c7" }, + dark: { fg: "#bae6fd", bg: "#0c4a6e", link: "#0ea5e9" }, + }, + arctic: { + light: { fg: "#1e3a5f", bg: "#f0f9ff", link: "#0284c7" }, + dark: { fg: "#bfdbfe", bg: "#1e293b", link: "#38bdf8" }, + }, + frost: { + light: { fg: "#334155", bg: "#f8fafc", link: "#38bdf8" }, + dark: { fg: "#e0f2fe", bg: "#0f172a", link: "#7dd3fc" }, + }, + dusk: { + light: { fg: "#3d2914", bg: "#fef3e2", link: "#ea580c" }, + dark: { fg: "#f5d5b8", bg: "#2d1f14", link: "#fb923c" }, + }, + sunset: { + light: { fg: "#4a1d1d", bg: "#fff7ed", link: "#f97316" }, + dark: { fg: "#fed7aa", bg: "#431407", link: "#fb923c" }, + }, + twilight: { + light: { fg: "#4c1d95", bg: "#f5f3ff", link: "#8b5cf6" }, + dark: { fg: "#ddd6fe", bg: "#2e1065", link: "#a78bfa" }, + }, + forest: { + light: { fg: "#1a2e1a", bg: "#e8efe8", link: "#15803d" }, + dark: { fg: "#c8dcc8", bg: "#1a2e1a", link: "#22c55e" }, + }, + moss: { + light: { fg: "#14532d", bg: "#dcfce7", link: "#22c55e" }, + dark: { fg: "#86efac", bg: "#052e16", link: "#4ade80" }, + }, + slate: { + light: { fg: "#334155", bg: "#f8fafc", link: "#475569" }, + dark: { fg: "#e2e8f0", bg: "#1e293b", link: "#94a3b8" }, + }, + oled: { + light: { fg: "#000000", bg: "#ffffff", link: "#0066cc" }, + dark: { fg: "#ffffff", bg: "#000000", link: "#3b82f6" }, + }, + solarized: { + light: { fg: "#657b83", bg: "#fdf6e3", link: "#268bd2" }, + dark: { fg: "#839496", bg: "#002b36", link: "#268bd2" }, + }, + }; const getCSS = ({ fontFamily, @@ -182,13 +293,15 @@ const getCSS = ({ const themeSet = THEME_COLORS[themeName] ?? THEME_COLORS.light; const theme = themeMode === "dark" ? themeSet.dark : themeSet.light; const font = FONT_MAP[fontFamily] ?? '"Literata"'; - const fontFamilyRule = fontFamily ? ` + const fontFamilyRule = fontFamily + ? ` body { font-family: ${font}, serif !important; } body * { font-family: inherit !important; - }` : ""; + }` + : ""; return ` ${buildFontFaceCSS()} @@ -265,6 +378,8 @@ document.addEventListener("alpine:init", () => { bookmarksOpen: false, navigatorOpen: false, tocItems: [] as any[], + mediaItemId: "" as string, + saveTimeout: null as ReturnType | null, readingTheme: "light" as string, readingMode: "light" as string, readingFont: "literata" as string, @@ -276,7 +391,10 @@ document.addEventListener("alpine:init", () => { formatGroup: string; readingDirection: string; mangaType: string; + savedPercentage?: number; + savedCfi?: string; }) { + this.mediaItemId = config.mediaItemId; this.settings = await loadSettings(); if (this.settings) { this.readingTheme = this.settings.reading_theme || "light"; @@ -291,7 +409,8 @@ document.addEventListener("alpine:init", () => { } const viewport = document.getElementById("reader-viewport")!; const themeSet = THEME_COLORS[this.readingTheme] ?? THEME_COLORS.light; - const themeColors = this.readingMode === "dark" ? themeSet.dark : themeSet.light; + const themeColors = + this.readingMode === "dark" ? themeSet.dark : themeSet.light; viewport.style.backgroundColor = themeColors.bg; this.view = document.getElementById("reader-view") as any; await loadFontBlobUrls(); @@ -324,7 +443,7 @@ document.addEventListener("alpine:init", () => { ); }); this.view.addEventListener("relocate", (e: any) => { - const { fraction, location, pageItem } = e.detail; + const { fraction, location, pageItem, cfi } = e.detail; const percent = new Intl.NumberFormat("en", { style: "percent", }).format(fraction); @@ -340,6 +459,7 @@ document.addEventListener("alpine:init", () => { slider.value = fraction; slider.title = `${percent} · ${loc}`; } + this.debouncedSaveProgress(fraction, location, cfi); }); const slider = document.getElementById( "progress-slider", @@ -360,7 +480,40 @@ document.addEventListener("alpine:init", () => { document.addEventListener("keydown", (ev: KeyboardEvent) => this.handleKeydown(ev), ); - this.renderer.next(); + if (config.savedCfi) { + await this.view.init({ lastLocation: config.savedCfi }); + } else if (config.savedPercentage && config.savedPercentage > 0) { + await this.view.init({ lastLocation: { fraction: config.savedPercentage } }); + } else { + await this.view.init({}); + } + }, + debouncedSaveProgress(fraction: number, location: any, cfi: string) { + if (this.saveTimeout) clearTimeout(this.saveTimeout); + this.saveTimeout = setTimeout(() => { + this.saveProgress(fraction, location, cfi); + }, 2000); + }, + async saveProgress(fraction: number, location: any, cfi: string) { + const token = getToken(); + if (!token || !this.mediaItemId) return; + try { + await fetch(`/api/media-items/${this.mediaItemId}/progress`, { + method: "PUT", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + percentage: fraction, + current_page: location?.current ?? 0, + total_pages: location?.total ?? 0, + epubcfi: cfi || "", + }), + }); + } catch (_e) { + // silent fail — progress save is non-critical + } }, zoomIn() { if (!this.isFixedLayout) return; @@ -451,7 +604,8 @@ document.addEventListener("alpine:init", () => { applyTheme() { const viewport = document.getElementById("reader-viewport")!; const themeSet = THEME_COLORS[this.readingTheme] ?? THEME_COLORS.light; - const themeColors = this.readingMode === "dark" ? themeSet.dark : themeSet.light; + const themeColors = + this.readingMode === "dark" ? themeSet.dark : themeSet.light; viewport.style.backgroundColor = themeColors.bg; this.applyStyles(); saveSettings({ @@ -465,8 +619,12 @@ document.addEventListener("alpine:init", () => { }, detectChromeDarkMode(): boolean { const darkChromeThemes = [ - "theme-tokyo-night", "theme-dracula", "theme-nord", - "theme-monokai", "theme-one-dark", "theme-material", + "theme-tokyo-night", + "theme-dracula", + "theme-nord", + "theme-monokai", + "theme-one-dark", + "theme-material", "theme-catppuccin", ]; return darkChromeThemes.some((t) => document.body.classList.contains(t));