docs: Update manga EPUB implementation guide

- Add markdownlint disable directives for linting
- Update SQL examples for consistency
- Update templ examples for panel detection integration
- Update TypeScript examples for reader shell configuration
This commit is contained in:
2026-04-12 20:44:02 -04:00
parent 03f7c15445
commit a13d2cc3bb
+67 -64
View File
@@ -1,3 +1,6 @@
<!-- markdownlint-disable MD010 -->
<!-- markdownlint-disable MD013 -->
# Manga EPUB and Panel Detection Implementation # Manga EPUB and Panel Detection Implementation
**Status:** Ready to implement **Status:** Ready to implement
@@ -35,7 +38,7 @@
-- ============================================================================ -- ============================================================================
-- Allow EPUB in manga library -- Allow EPUB in manga library
UPDATE library_types UPDATE library_types
SET allowed_extensions = array_append(allowed_extensions, '.epub') SET allowed_extensions = array_append(allowed_extensions, '.epub')
WHERE name = 'manga'; WHERE name = 'manga';
@@ -47,22 +50,22 @@ BEGIN
IF opf_content LIKE '%rendition:layout">pre-paginated<%' THEN IF opf_content LIKE '%rendition:layout">pre-paginated<%' THEN
RETURN TRUE; RETURN TRUE;
END IF; END IF;
IF opf_content LIKE '%rendition:layout="pre-paginated"%' THEN IF opf_content LIKE '%rendition:layout="pre-paginated"%' THEN
RETURN TRUE; RETURN TRUE;
END IF; END IF;
-- Check for RTL page progression (manga indicator) -- Check for RTL page progression (manga indicator)
IF opf_content LIKE '%page-progression-direction="rtl"%' THEN IF opf_content LIKE '%page-progression-direction="rtl"%' THEN
RETURN TRUE; RETURN TRUE;
END IF; END IF;
-- Check for image-heavy content (count <img> tags) -- Check for image-heavy content (count <img> tags)
-- Threshold of 50 images suggests manga/comic vs novel -- Threshold of 50 images suggests manga/comic vs novel
IF (SELECT COUNT(*) FROM regexp_matches(opf_content, '<img[^>]+>', 'g')) > 50 THEN IF (SELECT COUNT(*) FROM regexp_matches(opf_content, '<img[^>]+>', 'g')) > 50 THEN
RETURN TRUE; RETURN TRUE;
END IF; END IF;
RETURN FALSE; RETURN FALSE;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
@@ -82,9 +85,9 @@ CREATE TABLE IF NOT EXISTS processing_issues (
); );
-- Indexes for querying problem items -- Indexes for querying problem items
CREATE INDEX IF NOT EXISTS idx_processing_issues_library CREATE INDEX IF NOT EXISTS idx_processing_issues_library
ON processing_issues(library_id, resolved); ON processing_issues(library_id, resolved);
CREATE INDEX IF NOT EXISTS idx_processing_issues_severity CREATE INDEX IF NOT EXISTS idx_processing_issues_severity
ON processing_issues(severity, resolved); ON processing_issues(severity, resolved);
-- Add comment for documentation -- Add comment for documentation
@@ -111,7 +114,7 @@ COMMENT ON TABLE processing_issues IS 'Tracks media items that cannot be properl
-- name: CreateProcessingIssue :one -- name: CreateProcessingIssue :one
INSERT INTO processing_issues (media_item_id, library_id, issue_type, issue_description, severity) INSERT INTO processing_issues (media_item_id, library_id, issue_type, issue_description, severity)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (media_item_id, issue_type) ON CONFLICT (media_item_id, issue_type)
DO UPDATE SET issue_description = EXCLUDED.issue_description, DO UPDATE SET issue_description = EXCLUDED.issue_description,
severity = EXCLUDED.severity, severity = EXCLUDED.severity,
resolved = false, resolved = false,
@@ -119,7 +122,7 @@ DO UPDATE SET issue_description = EXCLUDED.issue_description,
RETURNING *; RETURNING *;
-- name: ListProcessingIssuesByLibrary :many -- name: ListProcessingIssuesByLibrary :many
SELECT SELECT
pi.id, pi.id,
pi.media_item_id, pi.media_item_id,
pi.issue_type, pi.issue_type,
@@ -138,7 +141,7 @@ JOIN libraries l ON pi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.id JOIN library_types lt ON l.library_type_id = lt.id
WHERE pi.library_id = $1 WHERE pi.library_id = $1
AND pi.resolved = false AND pi.resolved = false
ORDER BY ORDER BY
CASE pi.severity CASE pi.severity
WHEN 'error' THEN 1 WHEN 'error' THEN 1
WHEN 'warning' THEN 2 WHEN 'warning' THEN 2
@@ -147,7 +150,7 @@ ORDER BY
pi.created_at DESC; pi.created_at DESC;
-- name: GetProcessingIssueStats :one -- name: GetProcessingIssueStats :one
SELECT SELECT
COUNT(*) FILTER (WHERE severity = 'error' AND resolved = false) as error_count, COUNT(*) FILTER (WHERE severity = 'error' AND resolved = false) as error_count,
COUNT(*) FILTER (WHERE severity = 'warning' AND resolved = false) as warning_count, COUNT(*) FILTER (WHERE severity = 'warning' AND resolved = false) as warning_count,
COUNT(*) FILTER (WHERE severity = 'info' AND resolved = false) as info_count COUNT(*) FILTER (WHERE severity = 'info' AND resolved = false) as info_count
@@ -155,7 +158,7 @@ FROM processing_issues
WHERE library_id = $1; WHERE library_id = $1;
-- name: ResolveProcessingIssue :one -- name: ResolveProcessingIssue :one
UPDATE processing_issues UPDATE processing_issues
SET resolved = true, SET resolved = true,
resolved_at = NOW() resolved_at = NOW()
WHERE id = $1 WHERE id = $1
@@ -172,7 +175,7 @@ RETURNING *;
-- ============================================================================ -- ============================================================================
-- name: GetLibraryWithType :one -- name: GetLibraryWithType :one
SELECT SELECT
l.*, l.*,
lt.name as type_name, lt.name as type_name,
lt.description as type_description, lt.description as type_description,
@@ -713,7 +716,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
</head> </head>
<body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }"> <body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }">
@Header(user, "/admin/libraries/"+libraryID) @Header(user, "/admin/libraries/"+libraryID)
<main class="flex-1 p-8"> <main class="flex-1 p-8">
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
@@ -726,7 +729,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
</a> </a>
</div> </div>
</div> </div>
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 { if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
@@ -750,7 +753,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
} }
</div> </div>
} }
if len(issues) == 0 { if len(issues) == 0 {
<div class="card p-8 rounded-lg text-center"> <div class="card p-8 rounded-lg text-center">
<p class="text-gray-600">No processing issues found for this library.</p> <p class="text-gray-600">No processing issues found for this library.</p>
@@ -780,7 +783,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
</div> </div>
<div class="flex gap-3 mt-4"> <div class="flex gap-3 mt-4">
if issue.Severity == "warning" || issue.Severity == "info" { if issue.Severity == "warning" || issue.Severity == "info" {
<button @click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')" <button @click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
class="btn-secondary px-4 py-2 rounded text-sm"> class="btn-secondary px-4 py-2 rounded text-sm">
Dismiss Dismiss
</button> </button>
@@ -813,7 +816,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
**Replace with:** **Replace with:**
```templ ```templ
<body x-data="readerShell" <body x-data="readerShell"
x-init="initReader({ x-init="initReader({
mediaItemId: '{ readerData.MediaItemID }', mediaItemId: '{ readerData.MediaItemID }',
title: '{ readerData.Title }', title: '{ readerData.Title }',
@@ -822,7 +825,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
formatGroup: '{ readerData.FormatGroup }', formatGroup: '{ readerData.FormatGroup }',
mangaType: '{ readerData.MangaType }', mangaType: '{ readerData.MangaType }',
readingDirection: '{ readerData.ReadingDirection }' readingDirection: '{ readerData.ReadingDirection }'
})" })"
class="theme-tokyo-night"> class="theme-tokyo-night">
``` ```
@@ -841,58 +844,58 @@ import "foliate-js/view.js";
import { Alpine } from "../alpine"; import { Alpine } from "../alpine";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("readerShell", () => ({ Alpine.data("readerShell", () => ({
enablePanelDetection: false, enablePanelDetection: false,
libraryType: '', libraryType: "",
formatGroup: '', formatGroup: "",
mangaType: '', mangaType: "",
readingDirection: '', readingDirection: "",
panelDetector: null, panelDetector: null,
initReader(config: any) { initReader(config: any) {
this.enablePanelDetection = config.enablePanelDetection; this.enablePanelDetection = config.enablePanelDetection;
this.libraryType = config.libraryType; this.libraryType = config.libraryType;
this.formatGroup = config.formatGroup; this.formatGroup = config.formatGroup;
this.mangaType = config.mangaType; this.mangaType = config.mangaType;
this.readingDirection = config.readingDirection; this.readingDirection = config.readingDirection;
console.log("Reader initialized with:", { console.log("Reader initialized with:", {
panelDetection: this.enablePanelDetection, panelDetection: this.enablePanelDetection,
library: this.libraryType, library: this.libraryType,
format: this.formatGroup format: this.formatGroup,
}); });
// Only load panel detection if enabled // Only load panel detection if enabled
if (this.enablePanelDetection) { if (this.enablePanelDetection) {
this.loadPanelDetection(); this.loadPanelDetection();
} }
}, },
async loadPanelDetection() { async loadPanelDetection() {
try { try {
// Dynamic import to only load when needed // Dynamic import to only load when needed
const { PanelDetector } = await import('foliate-js/panel-detection.js'); const { PanelDetector } = await import("foliate-js/panel-detection.js");
this.panelDetector = new PanelDetector(); this.panelDetector = new PanelDetector();
console.log("Panel detection loaded successfully"); console.log("Panel detection loaded successfully");
} catch (error) { } catch (error) {
console.error("Failed to load panel detection:", error); console.error("Failed to load panel detection:", error);
} }
}, },
nextPage() { nextPage() {
const view = document.querySelector("#reader-view"); const view = document.querySelector("#reader-view");
// @ts-ignore - foliate custom element // @ts-ignore - foliate custom element
view?.next?.(); view?.next?.();
}, },
previousPage() { previousPage() {
const view = document.querySelector("#reader-view"); const view = document.querySelector("#reader-view");
// @ts-ignore - foliate custom element // @ts-ignore - foliate custom element
view?.prev?.(); view?.prev?.();
} },
})); }));
Alpine.start(); Alpine.start();
}); });
``` ```