feat(reader): wire up reading progress save and restore
The web reader had all the infrastructure for progress persistence
(updateReadingProgress/getReadingProgress API functions, PUT/GET
endpoints, database queries) but the reader.ts never called them.
Changes:
- Add debounced (2s) saveProgress call on every relocate event that
PUTs percentage, current_page, total_pages, and epubcfi to the
existing /api/media-items/:id/progress endpoint
- Replace renderer.next() with view.init({ lastLocation }) to restore
the reader to the last saved position on load (CFI first, then
fraction fallback, then default first page)
- Pass savedPercentage and savedCfi from server-side progress data
through readerInitExpr config to the JS initReader function
- Add mediaItemId and saveTimeout to the Alpine data object
This fixes both the blank /progress page and the missing progress
section on book detail pages — both were empty because the
reading_progress table never received any data from the web reader.
This commit is contained in:
@@ -5,14 +5,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readerInitExpr(metadata ReaderMetadata) string {
|
func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string {
|
||||||
config := map[string]string{
|
config := map[string]interface{}{
|
||||||
"mediaItemId": metadata.MediaItemID,
|
"mediaItemId": metadata.MediaItemID,
|
||||||
"fileUrl": metadata.FileURL,
|
"fileUrl": metadata.FileURL,
|
||||||
"formatGroup": metadata.FormatGroup,
|
"formatGroup": metadata.FormatGroup,
|
||||||
"readingDirection": metadata.ReadingDirection,
|
"readingDirection": metadata.ReadingDirection,
|
||||||
"mangaType": metadata.MangaType,
|
"mangaType": metadata.MangaType,
|
||||||
}
|
}
|
||||||
|
if progress.Percentage > 0 {
|
||||||
|
config["savedPercentage"] = progress.Percentage
|
||||||
|
}
|
||||||
|
if progress.EpubCfi != "" {
|
||||||
|
config["savedCfi"] = progress.EpubCfi
|
||||||
|
}
|
||||||
jsonBytes, _ := json.Marshal(config)
|
jsonBytes, _ := json.Marshal(config)
|
||||||
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
|
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
|
||||||
}
|
}
|
||||||
@@ -32,7 +38,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
|||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
x-data="readerShell"
|
x-data="readerShell"
|
||||||
x-init={ readerInitExpr(metadata) }
|
x-init={ readerInitExpr(metadata, progress) }
|
||||||
class="theme-tokyo-night h-screen overflow-hidden"
|
class="theme-tokyo-night h-screen overflow-hidden"
|
||||||
>
|
>
|
||||||
@ReaderChrome(user, metadata, progress)
|
@ReaderChrome(user, metadata, progress)
|
||||||
|
|||||||
+17
-11
@@ -13,14 +13,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readerInitExpr(metadata ReaderMetadata) string {
|
func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string {
|
||||||
config := map[string]string{
|
config := map[string]interface{}{
|
||||||
"mediaItemId": metadata.MediaItemID,
|
"mediaItemId": metadata.MediaItemID,
|
||||||
"fileUrl": metadata.FileURL,
|
"fileUrl": metadata.FileURL,
|
||||||
"formatGroup": metadata.FormatGroup,
|
"formatGroup": metadata.FormatGroup,
|
||||||
"readingDirection": metadata.ReadingDirection,
|
"readingDirection": metadata.ReadingDirection,
|
||||||
"mangaType": metadata.MangaType,
|
"mangaType": metadata.MangaType,
|
||||||
}
|
}
|
||||||
|
if progress.Percentage > 0 {
|
||||||
|
config["savedPercentage"] = progress.Percentage
|
||||||
|
}
|
||||||
|
if progress.EpubCfi != "" {
|
||||||
|
config["savedCfi"] = progress.EpubCfi
|
||||||
|
}
|
||||||
jsonBytes, _ := json.Marshal(config)
|
jsonBytes, _ := json.Marshal(config)
|
||||||
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
|
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
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -64,9 +70,9 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var3 string
|
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 {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -156,7 +162,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
var templ_7745c5c3_Var5 templ.SafeURL
|
var templ_7745c5c3_Var5 templ.SafeURL
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -169,7 +175,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -182,7 +188,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -321,7 +327,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.CfiPosition)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -334,7 +340,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -347,7 +353,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
|||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||||
if templ_7745c5c3_Err != nil {
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
+262
-104
@@ -22,28 +22,138 @@ interface FontFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FONT_FILES: 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: '"Literata"',
|
||||||
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Variable.woff2", weight: "100 900", style: "normal" },
|
url: "/static/fonts/literata/Literata-Variable.woff2",
|
||||||
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
weight: "100 900",
|
||||||
{ family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Variable.woff2", weight: "100 900", style: "normal" },
|
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: '"Literata"',
|
||||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Regular.woff2", weight: "400", style: "normal" },
|
url: "/static/fonts/literata/Literata-Italic-Variable.woff2",
|
||||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Bold.woff2", weight: "700", style: "normal" },
|
weight: "100 900",
|
||||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Italic.woff2", weight: "400", style: "italic" },
|
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: '"Crimson Pro"',
|
||||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Regular.woff2", weight: "400", style: "normal" },
|
url: "/static/fonts/crimson/CrimsonPro-Variable.woff2",
|
||||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Bold.woff2", weight: "700", style: "normal" },
|
weight: "100 900",
|
||||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Italic.woff2", weight: "400", style: "italic" },
|
style: "normal",
|
||||||
{ 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: '"Crimson Pro"',
|
||||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2", weight: "400", style: "italic" },
|
url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2",
|
||||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2", weight: "700", style: "italic" },
|
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<string, string> = {};
|
let fontBlobUrls: Record<string, string> = {};
|
||||||
@@ -87,80 +197,81 @@ interface ThemeColors {
|
|||||||
link: string;
|
link: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_COLORS: Record<string, { light: ThemeColors; dark: ThemeColors }> = {
|
const THEME_COLORS: Record<string, { light: ThemeColors; dark: ThemeColors }> =
|
||||||
light: {
|
{
|
||||||
light: { fg: "#1a1a1a", bg: "#fafafa", link: "#0066cc" },
|
light: {
|
||||||
dark: { fg: "#e8e8e8", bg: "#1a1a1a", link: "#60a5fa" },
|
light: { fg: "#1a1a1a", bg: "#fafafa", link: "#0066cc" },
|
||||||
},
|
dark: { fg: "#e8e8e8", bg: "#1a1a1a", link: "#60a5fa" },
|
||||||
paper: {
|
},
|
||||||
light: { fg: "#2d2d2d", bg: "#fdfbf7", link: "#0284c7" },
|
paper: {
|
||||||
dark: { fg: "#ebe5dd", bg: "#1c1917", link: "#38bdf8" },
|
light: { fg: "#2d2d2d", bg: "#fdfbf7", link: "#0284c7" },
|
||||||
},
|
dark: { fg: "#ebe5dd", bg: "#1c1917", link: "#38bdf8" },
|
||||||
sepia: {
|
},
|
||||||
light: { fg: "#4a3728", bg: "#f4ecd8", link: "#8b4513" },
|
sepia: {
|
||||||
dark: { fg: "#d4c4a8", bg: "#2b2420", link: "#d97706" },
|
light: { fg: "#4a3728", bg: "#f4ecd8", link: "#8b4513" },
|
||||||
},
|
dark: { fg: "#d4c4a8", bg: "#2b2420", link: "#d97706" },
|
||||||
parchment: {
|
},
|
||||||
light: { fg: "#5c4033", bg: "#f0e6d3", link: "#92400e" },
|
parchment: {
|
||||||
dark: { fg: "#e8dcc8", bg: "#3d2b1f", link: "#b45309" },
|
light: { fg: "#5c4033", bg: "#f0e6d3", link: "#92400e" },
|
||||||
},
|
dark: { fg: "#e8dcc8", bg: "#3d2b1f", link: "#b45309" },
|
||||||
warm: {
|
},
|
||||||
light: { fg: "#2b2b2b", bg: "#faf8f0", link: "#d97706" },
|
warm: {
|
||||||
dark: { fg: "#f5e6d3", bg: "#2d2416", link: "#fbbf24" },
|
light: { fg: "#2b2b2b", bg: "#faf8f0", link: "#d97706" },
|
||||||
},
|
dark: { fg: "#f5e6d3", bg: "#2d2416", link: "#fbbf24" },
|
||||||
candlelight: {
|
},
|
||||||
light: { fg: "#3d2b1f", bg: "#fef3c7", link: "#b45309" },
|
candlelight: {
|
||||||
dark: { fg: "#fde68a", bg: "#451a03", link: "#fb923c" },
|
light: { fg: "#3d2b1f", bg: "#fef3c7", link: "#b45309" },
|
||||||
},
|
dark: { fg: "#fde68a", bg: "#451a03", link: "#fb923c" },
|
||||||
azure: {
|
},
|
||||||
light: { fg: "#262d48", bg: "#cedef5", link: "#2d53e5" },
|
azure: {
|
||||||
dark: { fg: "#babee1", bg: "#282e47", link: "#0ea5e9" },
|
light: { fg: "#262d48", bg: "#cedef5", link: "#2d53e5" },
|
||||||
},
|
dark: { fg: "#babee1", bg: "#282e47", link: "#0ea5e9" },
|
||||||
sky: {
|
},
|
||||||
light: { fg: "#1e3a5f", bg: "#e0f2fe", link: "#0284c7" },
|
sky: {
|
||||||
dark: { fg: "#bae6fd", bg: "#0c4a6e", link: "#0ea5e9" },
|
light: { fg: "#1e3a5f", bg: "#e0f2fe", link: "#0284c7" },
|
||||||
},
|
dark: { fg: "#bae6fd", bg: "#0c4a6e", link: "#0ea5e9" },
|
||||||
arctic: {
|
},
|
||||||
light: { fg: "#1e3a5f", bg: "#f0f9ff", link: "#0284c7" },
|
arctic: {
|
||||||
dark: { fg: "#bfdbfe", bg: "#1e293b", link: "#38bdf8" },
|
light: { fg: "#1e3a5f", bg: "#f0f9ff", link: "#0284c7" },
|
||||||
},
|
dark: { fg: "#bfdbfe", bg: "#1e293b", link: "#38bdf8" },
|
||||||
frost: {
|
},
|
||||||
light: { fg: "#334155", bg: "#f8fafc", link: "#38bdf8" },
|
frost: {
|
||||||
dark: { fg: "#e0f2fe", bg: "#0f172a", link: "#7dd3fc" },
|
light: { fg: "#334155", bg: "#f8fafc", link: "#38bdf8" },
|
||||||
},
|
dark: { fg: "#e0f2fe", bg: "#0f172a", link: "#7dd3fc" },
|
||||||
dusk: {
|
},
|
||||||
light: { fg: "#3d2914", bg: "#fef3e2", link: "#ea580c" },
|
dusk: {
|
||||||
dark: { fg: "#f5d5b8", bg: "#2d1f14", link: "#fb923c" },
|
light: { fg: "#3d2914", bg: "#fef3e2", link: "#ea580c" },
|
||||||
},
|
dark: { fg: "#f5d5b8", bg: "#2d1f14", link: "#fb923c" },
|
||||||
sunset: {
|
},
|
||||||
light: { fg: "#4a1d1d", bg: "#fff7ed", link: "#f97316" },
|
sunset: {
|
||||||
dark: { fg: "#fed7aa", bg: "#431407", link: "#fb923c" },
|
light: { fg: "#4a1d1d", bg: "#fff7ed", link: "#f97316" },
|
||||||
},
|
dark: { fg: "#fed7aa", bg: "#431407", link: "#fb923c" },
|
||||||
twilight: {
|
},
|
||||||
light: { fg: "#4c1d95", bg: "#f5f3ff", link: "#8b5cf6" },
|
twilight: {
|
||||||
dark: { fg: "#ddd6fe", bg: "#2e1065", link: "#a78bfa" },
|
light: { fg: "#4c1d95", bg: "#f5f3ff", link: "#8b5cf6" },
|
||||||
},
|
dark: { fg: "#ddd6fe", bg: "#2e1065", link: "#a78bfa" },
|
||||||
forest: {
|
},
|
||||||
light: { fg: "#1a2e1a", bg: "#e8efe8", link: "#15803d" },
|
forest: {
|
||||||
dark: { fg: "#c8dcc8", bg: "#1a2e1a", link: "#22c55e" },
|
light: { fg: "#1a2e1a", bg: "#e8efe8", link: "#15803d" },
|
||||||
},
|
dark: { fg: "#c8dcc8", bg: "#1a2e1a", link: "#22c55e" },
|
||||||
moss: {
|
},
|
||||||
light: { fg: "#14532d", bg: "#dcfce7", link: "#22c55e" },
|
moss: {
|
||||||
dark: { fg: "#86efac", bg: "#052e16", link: "#4ade80" },
|
light: { fg: "#14532d", bg: "#dcfce7", link: "#22c55e" },
|
||||||
},
|
dark: { fg: "#86efac", bg: "#052e16", link: "#4ade80" },
|
||||||
slate: {
|
},
|
||||||
light: { fg: "#334155", bg: "#f8fafc", link: "#475569" },
|
slate: {
|
||||||
dark: { fg: "#e2e8f0", bg: "#1e293b", link: "#94a3b8" },
|
light: { fg: "#334155", bg: "#f8fafc", link: "#475569" },
|
||||||
},
|
dark: { fg: "#e2e8f0", bg: "#1e293b", link: "#94a3b8" },
|
||||||
oled: {
|
},
|
||||||
light: { fg: "#000000", bg: "#ffffff", link: "#0066cc" },
|
oled: {
|
||||||
dark: { fg: "#ffffff", bg: "#000000", link: "#3b82f6" },
|
light: { fg: "#000000", bg: "#ffffff", link: "#0066cc" },
|
||||||
},
|
dark: { fg: "#ffffff", bg: "#000000", link: "#3b82f6" },
|
||||||
solarized: {
|
},
|
||||||
light: { fg: "#657b83", bg: "#fdf6e3", link: "#268bd2" },
|
solarized: {
|
||||||
dark: { fg: "#839496", bg: "#002b36", link: "#268bd2" },
|
light: { fg: "#657b83", bg: "#fdf6e3", link: "#268bd2" },
|
||||||
},
|
dark: { fg: "#839496", bg: "#002b36", link: "#268bd2" },
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const getCSS = ({
|
const getCSS = ({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
@@ -182,13 +293,15 @@ const getCSS = ({
|
|||||||
const themeSet = THEME_COLORS[themeName] ?? THEME_COLORS.light;
|
const themeSet = THEME_COLORS[themeName] ?? THEME_COLORS.light;
|
||||||
const theme = themeMode === "dark" ? themeSet.dark : themeSet.light;
|
const theme = themeMode === "dark" ? themeSet.dark : themeSet.light;
|
||||||
const font = FONT_MAP[fontFamily] ?? '"Literata"';
|
const font = FONT_MAP[fontFamily] ?? '"Literata"';
|
||||||
const fontFamilyRule = fontFamily ? `
|
const fontFamilyRule = fontFamily
|
||||||
|
? `
|
||||||
body {
|
body {
|
||||||
font-family: ${font}, serif !important;
|
font-family: ${font}, serif !important;
|
||||||
}
|
}
|
||||||
body * {
|
body * {
|
||||||
font-family: inherit !important;
|
font-family: inherit !important;
|
||||||
}` : "";
|
}`
|
||||||
|
: "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${buildFontFaceCSS()}
|
${buildFontFaceCSS()}
|
||||||
@@ -265,6 +378,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
bookmarksOpen: false,
|
bookmarksOpen: false,
|
||||||
navigatorOpen: false,
|
navigatorOpen: false,
|
||||||
tocItems: [] as any[],
|
tocItems: [] as any[],
|
||||||
|
mediaItemId: "" as string,
|
||||||
|
saveTimeout: null as ReturnType<typeof setTimeout> | null,
|
||||||
readingTheme: "light" as string,
|
readingTheme: "light" as string,
|
||||||
readingMode: "light" as string,
|
readingMode: "light" as string,
|
||||||
readingFont: "literata" as string,
|
readingFont: "literata" as string,
|
||||||
@@ -276,7 +391,10 @@ document.addEventListener("alpine:init", () => {
|
|||||||
formatGroup: string;
|
formatGroup: string;
|
||||||
readingDirection: string;
|
readingDirection: string;
|
||||||
mangaType: string;
|
mangaType: string;
|
||||||
|
savedPercentage?: number;
|
||||||
|
savedCfi?: string;
|
||||||
}) {
|
}) {
|
||||||
|
this.mediaItemId = config.mediaItemId;
|
||||||
this.settings = await loadSettings();
|
this.settings = await loadSettings();
|
||||||
if (this.settings) {
|
if (this.settings) {
|
||||||
this.readingTheme = this.settings.reading_theme || "light";
|
this.readingTheme = this.settings.reading_theme || "light";
|
||||||
@@ -291,7 +409,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
const viewport = document.getElementById("reader-viewport")!;
|
const viewport = document.getElementById("reader-viewport")!;
|
||||||
const themeSet = THEME_COLORS[this.readingTheme] ?? THEME_COLORS.light;
|
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;
|
viewport.style.backgroundColor = themeColors.bg;
|
||||||
this.view = document.getElementById("reader-view") as any;
|
this.view = document.getElementById("reader-view") as any;
|
||||||
await loadFontBlobUrls();
|
await loadFontBlobUrls();
|
||||||
@@ -324,7 +443,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.view.addEventListener("relocate", (e: any) => {
|
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", {
|
const percent = new Intl.NumberFormat("en", {
|
||||||
style: "percent",
|
style: "percent",
|
||||||
}).format(fraction);
|
}).format(fraction);
|
||||||
@@ -340,6 +459,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
slider.value = fraction;
|
slider.value = fraction;
|
||||||
slider.title = `${percent} · ${loc}`;
|
slider.title = `${percent} · ${loc}`;
|
||||||
}
|
}
|
||||||
|
this.debouncedSaveProgress(fraction, location, cfi);
|
||||||
});
|
});
|
||||||
const slider = document.getElementById(
|
const slider = document.getElementById(
|
||||||
"progress-slider",
|
"progress-slider",
|
||||||
@@ -360,7 +480,40 @@ document.addEventListener("alpine:init", () => {
|
|||||||
document.addEventListener("keydown", (ev: KeyboardEvent) =>
|
document.addEventListener("keydown", (ev: KeyboardEvent) =>
|
||||||
this.handleKeydown(ev),
|
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() {
|
zoomIn() {
|
||||||
if (!this.isFixedLayout) return;
|
if (!this.isFixedLayout) return;
|
||||||
@@ -451,7 +604,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
applyTheme() {
|
applyTheme() {
|
||||||
const viewport = document.getElementById("reader-viewport")!;
|
const viewport = document.getElementById("reader-viewport")!;
|
||||||
const themeSet = THEME_COLORS[this.readingTheme] ?? THEME_COLORS.light;
|
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;
|
viewport.style.backgroundColor = themeColors.bg;
|
||||||
this.applyStyles();
|
this.applyStyles();
|
||||||
saveSettings({
|
saveSettings({
|
||||||
@@ -465,8 +619,12 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
detectChromeDarkMode(): boolean {
|
detectChromeDarkMode(): boolean {
|
||||||
const darkChromeThemes = [
|
const darkChromeThemes = [
|
||||||
"theme-tokyo-night", "theme-dracula", "theme-nord",
|
"theme-tokyo-night",
|
||||||
"theme-monokai", "theme-one-dark", "theme-material",
|
"theme-dracula",
|
||||||
|
"theme-nord",
|
||||||
|
"theme-monokai",
|
||||||
|
"theme-one-dark",
|
||||||
|
"theme-material",
|
||||||
"theme-catppuccin",
|
"theme-catppuccin",
|
||||||
];
|
];
|
||||||
return darkChromeThemes.some((t) => document.body.classList.contains(t));
|
return darkChromeThemes.some((t) => document.body.classList.contains(t));
|
||||||
|
|||||||
Reference in New Issue
Block a user