diff --git a/internal/handlers/reader.go b/internal/handlers/reader.go
index 229a458..e8ffe73 100644
--- a/internal/handlers/reader.go
+++ b/internal/handlers/reader.go
@@ -102,6 +102,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
// Determine if panel detection should be enabled
enablePanelDetection := shouldEnablePanelDetection(libraryWithType.TypeName, mediaItem.FormatGroup)
+ treatAsComic := ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup)
// Get reading progress
var progress database.ReadingProgress
@@ -133,6 +134,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
"progress": progress,
"bookmarks": bookmarks,
"enable_panel_detection": enablePanelDetection,
+ "treat_as_comic": treatAsComic,
"format_group": mediaItem.FormatGroup,
"manga_type": mediaItem.MangaType,
"reading_direction": mediaItem.ReadingDirection,
@@ -639,6 +641,25 @@ func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
})
}
+// ShouldTreatAsComic reports whether a fixed-layout item that lives in a
+// comics or manga library should get comic reader treatment: bookmarks
+// only — no text-selection highlights, no annotations, no in-book search.
+// Comic archives already behave this way; this flag extends it to fixed-
+// layout PDFs shelved in comic libraries (scanned or official manga PDFs).
+func ShouldTreatAsComic(libraryType string, formatGroup string) bool {
+ comicLibraries := map[string]bool{
+ "manga": true,
+ "comics": true,
+ }
+
+ fixedFormats := map[string]bool{
+ "fixed_layout": true,
+ "comic_archive": true,
+ }
+
+ return comicLibraries[libraryType] && fixedFormats[formatGroup]
+}
+
// shouldEnablePanelDetection determines if panel detection should be enabled
// based on library type and format group
func shouldEnablePanelDetection(libraryType string, formatGroup string) bool {
diff --git a/internal/router/reader.go b/internal/router/reader.go
index 0e0d89d..2f73509 100644
--- a/internal/router/reader.go
+++ b/internal/router/reader.go
@@ -66,6 +66,13 @@ func registerReaderRoutes(cfg *Config) {
if !visible {
return renderErrorPage(c, "Access denied", "access_denied")
}
+ // Library type decides comic reader treatment: fixed-layout items
+ // (e.g. manga PDFs) shelved in comics/manga libraries are paged
+ // through like comics — bookmarks only, no highlights or search.
+ libraryWithType, err := cfg.Queries.GetLibraryWithType(c.Request().Context(), mediaItem.LibraryID)
+ if err != nil {
+ return renderErrorPage(c, "Failed to get library info", "library_error")
+ }
// Convert to template types. Reading state is deliberately NOT
// fetched or embedded: the reader pulls position, bookmarks, and
// annotations from the APIs at open time so the page can never
@@ -89,6 +96,7 @@ func registerReaderRoutes(cfg *Config) {
FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}),
TotalCharacters: mediaItem.TotalCharacters.Int64,
EstimatedPages: sync.EstimatedPages(mediaItem.TotalCharacters.Int64),
+ TreatAsComic: handlers.ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup),
}
// Render template
var buf bytes.Buffer
diff --git a/templates/reader.templ b/templates/reader.templ
index d308407..84b7031 100644
--- a/templates/reader.templ
+++ b/templates/reader.templ
@@ -16,6 +16,7 @@ func readerInitExpr(metadata ReaderMetadata) string {
"formatGroup": metadata.FormatGroup,
"readingDirection": metadata.ReadingDirection,
"mangaType": metadata.MangaType,
+ "treatAsComic": metadata.TreatAsComic,
}
jsonBytes, _ := json.Marshal(config)
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
@@ -68,9 +69,7 @@ templ Reader(user User, metadata ReaderMetadata) {
-
@ReaderChrome(metadata)
-
-
@ReaderTOCDrawer()
-
@ReaderSettingsDrawer()
-
-
@ReaderAnnotationsDrawer()
-
@ReaderSearchDrawer()
-
Settingss
Add bookmarkb
Close menus / toggle chromeEsc
-
Zoom & pan (comics, PDFs)
Zoom in / out / reset+−0 or wheel
Move the pagedrag with the mouse
Recenter (keeps zoom)⊙ button
Select text in a PDFSmart mode: drag over text · Shift+drag anywhere · Text mode
-
Highlights
Createselect text → pick a color
Edit / deleteclick the highlight
-
Touch
Page turnswipe, or tap outer edges
Toggle chrometap center
@@ -301,7 +291,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
{ metadata.Title }
-
+
@@ -344,7 +334,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
- —
+ —
@@ -354,92 +344,93 @@ templ ReaderChrome(metadata ReaderMetadata) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 100%
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Smart
- Pan
- Text
-
-
-
-
-
-
-
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
🏷️ 🔍 📝 Aa ?
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -229,7 +230,7 @@ func drawerHeader(title string) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 561, Col: 35}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 552, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
diff --git a/templates/types.go b/templates/types.go
index afdda90..e315eb4 100644
--- a/templates/types.go
+++ b/templates/types.go
@@ -243,6 +243,9 @@ type ReaderMetadata struct {
LibraryID string `json:"library_id"`
TotalCharacters int64 `json:"total_characters"`
EstimatedPages int `json:"estimated_pages"`
+ // TreatAsComic: fixed-layout item in a comics/manga library gets
+ // comic reader treatment (bookmarks only — no highlights, no search).
+ TreatAsComic bool `json:"treat_as_comic"`
}
type ReadingProgress struct {
diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts
index 3fc8d7c..acae2b6 100644
--- a/web/src/reader/reader.ts
+++ b/web/src/reader/reader.ts
@@ -398,6 +398,10 @@ document.addEventListener("alpine:init", () => {
isFixedLayout: false,
isPDF: false,
isComic: false,
+ // Fixed-layout item in a comics/manga library (e.g. a manga PDF):
+ // comic reader treatment — bookmarks only, no text-selection
+ // highlights, no in-book search.
+ treatAsComic: false as boolean,
comicFlow: "paged" as string,
fxBrightness: 1 as number,
fxContrast: 1 as number,
@@ -588,8 +592,10 @@ document.addEventListener("alpine:init", () => {
formatGroup: string;
readingDirection: string;
mangaType: string;
+ treatAsComic: boolean;
}) {
this.mediaItemId = config.mediaItemId;
+ this.treatAsComic = config.treatAsComic;
// Reading state (position, bookmarks, annotations) is never baked
// into the rendered page: the web reader is intrinsically tied to
// the server, so it reads all of it from the APIs at open time —
@@ -724,7 +730,11 @@ document.addEventListener("alpine:init", () => {
// drags, so the popover's pointerup trigger never fires for
// them; a selection that has been stable for a moment is the
// settled gesture — open the popover for it.
- if (this.isFixedLayout && doc.querySelector(".textLayer"))
+ if (
+ this.isFixedLayout &&
+ !this.treatAsComic &&
+ doc.querySelector(".textLayer")
+ )
this.schedulePDFSelectionPopover(doc, index);
else if (!this.isFixedLayout)
this.scheduleSelectionPopover(doc, index);
@@ -785,7 +795,12 @@ document.addEventListener("alpine:init", () => {
// Detected structurally: renderer.isPDF isn't set until the first
// spread renders (during view.init), which is after this listener
// attaches — the stale copy here would always be falsy.
- if (this.isFixedLayout && doc.querySelector(".textLayer")) {
+ // Comic-treated PDFs never attach it: no highlights, like comics.
+ if (
+ this.isFixedLayout &&
+ !this.treatAsComic &&
+ doc.querySelector(".textLayer")
+ ) {
doc.addEventListener(
"pointerup",
(e: PointerEvent) =>
@@ -939,6 +954,9 @@ document.addEventListener("alpine:init", () => {
// Smart|Pan|Text control appear for PDFs.
this.isPDF = !!this.renderer?.isPDF;
if (this.isPDF) {
+ // Comic-treated PDFs are paged through like comics: no text
+ // selection, so pointer interactions stay in pan mode.
+ if (this.treatAsComic) this.interactionMode = "pan";
this.renderer.setAttribute("interaction-mode", this.interactionMode);
}
this.fxZoomed = this.isFixedLayout && this.renderer?.zoom != null;
@@ -1607,7 +1625,12 @@ document.addEventListener("alpine:init", () => {
const rows = await hlResp.json();
this.highlightItems = (rows as any[])
.map((r) => this.mapHighlightRow(r))
- .filter((hl: any) => hl.cfi || hl.pdfPage >= 0);
+ // Comic-treated items show no highlights (bookmarks only),
+ // and previously-created PDF highlights stop rendering.
+ .filter(
+ (hl: any) =>
+ !this.treatAsComic && (hl.cfi || hl.pdfPage >= 0),
+ );
this.renderAllHighlights();
}
if (noteResp.ok) {
@@ -2143,6 +2166,8 @@ document.addEventListener("alpine:init", () => {
},
setInteractionMode(mode: string) {
if (!this.isFixedLayout || !this.isPDF) return;
+ // Comic-treated PDFs have no text selection: pan only.
+ if (this.treatAsComic) return;
if (mode !== "select" && mode !== "pan" && mode !== "text") return;
this.renderer.setAttribute("interaction-mode", mode);
this.interactionMode = mode;
@@ -2237,8 +2262,9 @@ document.addEventListener("alpine:init", () => {
},
toggleSearch() {
// Reflowable books use foliate's DOM search; PDFs use extracted text.
- // Comics have no text at all.
- if (this.isFixedLayout && !this.isPDF) return;
+ // Comics have no text at all — and comic-treated PDFs (manga scans)
+ // opt out of search to match comic treatment: bookmarks only.
+ if ((this.isFixedLayout && !this.isPDF) || this.treatAsComic) return;
const opening = !this.searchOpen;
this.closeDrawers();
this.searchOpen = opening;
@@ -2646,8 +2672,8 @@ document.addEventListener("alpine:init", () => {
this.hyphenate = true;
this.chromeBehavior = "auto-hide";
this.applyChromeBehavior();
- this.interactionMode = "select";
- this.setInteractionMode("select");
+ this.interactionMode = this.treatAsComic ? "pan" : "select";
+ this.setInteractionMode(this.interactionMode);
this.fxBrightness = 1;
this.fxContrast = 1;
this.fxInvert = false;