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:
@@ -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();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user