import React, {useContext, useEffect, useState} from 'react'; import {Link, Route, Routes, useLocation, useNavigate, useParams, useSearchParams} from "react-router"; import {deleteFileGroups, getDocStatistics, tagFileGroup, untagFileGroup} from "../api"; import {Media, ThemeContext} from "../contexts/contexts"; import { APIButton, BackButton, DirectoryLink, encodeMediaPath, ErrorMessage, humanFileSize, isoDatetimeToAgoPopup, PageContainer, PreviewPath, SearchInput, TabLinks, toLocaleString, useTitle } from "./Common"; import { Button, ButtonGroup, Group, Header, Loading, MediaGate, Panel, Statistic, StatisticGroup, Tabs } from "./ui"; import {BulkTagModal} from "./BulkTagModal"; import {TaggedDeleteConfirmModal} from "./TaggedDeleteConfirmModal"; import {DeepSearchHint, docMimetypeFilterOptions, FilesView, SearchControlBar} from "./Files"; import {useAuthors, useDoc, useOneQuery, useSearchDocs, useSubjects} from "../hooks/customHooks"; import {TagsSelector} from "../Tags"; import {toast} from "./ui"; import {CollectionTable} from "./collections/CollectionTable"; import {AddToPlaylistButton} from "./AddToPlaylist"; import {CbzViewer} from "./CbzViewer"; import _ from "lodash"; function DocsPage() { useTitle('Documents'); const searchInputRef = React.useRef(); const { docs, totalPages, activePage, setPage, searchStr, setSearchStr, author, subject, fetchDocs, } = useSearchDocs(); const [selectedDocs, setSelectedDocs] = useState([]); const [bulkTagOpen, setBulkTagOpen] = useState(false); const [taggedFileGroups, setTaggedFileGroups] = useState(null); const onSelect = (path, checked) => { if (checked && path) { setSelectedDocs([...selectedDocs, path]); } else if (path) { setSelectedDocs(selectedDocs.filter(i => i !== path)); } } const onDelete = async (force = false) => { const fileGroupIds = docs.filter(i => selectedDocs.indexOf(i['primary_path']) >= 0).map(i => i['id']); const result = await deleteFileGroups(fileGroupIds, force); if (result && result.tagged) { setTaggedFileGroups(result.file_groups); return; } await fetchDocs(); setSelectedDocs([]); } const invertSelection = () => { const newSelectedDocs = docs.map(doc => doc['key']).filter(i => selectedDocs.indexOf(i) < 0); setSelectedDocs(newSelectedDocs); } const clearSelection = (e) => { if (e) e.preventDefault(); setSelectedDocs([]); } const onBulkTagComplete = async () => { await fetchDocs(); setSelectedDocs([]); } const selectElm =
onDelete(false)} obeyWROLMode={true} >Delete setBulkTagOpen(false)} paths={selectedDocs} onComplete={onBulkTagComplete} />
; let docOrders = [ {value: 'published_datetime', text: 'Published Date', short: 'P.Date'}, {value: 'size', text: 'Size'}, {value: 'title', text: 'Title'}, ]; if (searchStr) { docOrders = [{value: 'rank', text: 'Rank'}, ...docOrders]; } const {body, paginator, viewButton} = FilesView( { files: docs, activePage: activePage, totalPages: totalPages, selectElem: selectElm, selectedKeys: selectedDocs, onSelect: onSelect, setPage: setPage, }, ); return <> {author &&
Author: {author}
} {subject &&
Subject: {subject}
} {body} {paginator} setTaggedFileGroups(null)} onConfirm={async () => { setTaggedFileGroups(null); await onDelete(true); }} /> ; } function DocPage() { const {fileGroupId} = useParams(); const navigate = useNavigate(); // Whether the theme is currently filtering media; a PDF is the one embed it cannot reach. const {mediaFilterEnabled} = useContext(ThemeContext); const {docFile, doc, fetchDoc} = useDoc(parseInt(fileGroupId)); const [searchParams] = useSearchParams(); const [taggedFileGroups, setTaggedFileGroups] = useState(null); // Deep-link params: `loc` = EPUB spine index, `page` = PDF page, `q` = search term. const locParam = searchParams.get('loc'); const pageParam = searchParams.get('page'); const qParam = searchParams.get('q'); const title = docFile ? (docFile.title || docFile.name) : null; useTitle(title); const [selectedFileIndex, setSelectedFileIndex] = useState(0); if (docFile === null) { return ; } if (docFile === undefined) { return <>
Unknown document
This document does not exist
; } const {data, size, directory} = docFile; // Cover image. const coverPath = data && data.cover_path ? data.cover_path : null; const coverUrl = coverPath ? `/media/${encodeMediaPath(coverPath)}` : null; // Filter files to those that can be viewed inline (EPUB, PDF, comic books). const CBZ_EXTENSIONS = ['cbz', 'cbr', 'cbt', 'cb7']; const isViewableMimetype = (mt) => { if (!mt) return false; if (mt.startsWith('application/epub')) return true; if (mt === 'application/pdf') return true; if (mt.includes('cbz') || mt.includes('cbr') || mt.includes('comicbook+zip') || mt.includes('comicbook-rar')) return true; return false; }; const viewableFiles = (docFile.files || []) .filter(f => isViewableMimetype(f.mimetype) || CBZ_EXTENSIONS.includes((f.path || '').toLowerCase().split('.').pop())) .sort((a, b) => { if (String(a.path) === String(docFile.primary_path)) return -1; if (String(b.path) === String(docFile.primary_path)) return 1; return 0; }); const selectedFile = viewableFiles[selectedFileIndex] || viewableFiles[0]; // Derive viewer state from the selected file (or fall back to primary). const activePath = selectedFile ? selectedFile.path : docFile.primary_path; const activeMimetype = selectedFile ? (selectedFile.mimetype || '') : (docFile.mimetype || ''); const activeExt = (String(activePath) || '').toLowerCase().split('.').pop(); const downloadUrl = activePath ? `/download/${encodeMediaPath(activePath)}` : null; const isEpub = activeMimetype.startsWith('application/epub'); const isPdf = activeMimetype === 'application/pdf'; const isCbz = activeMimetype.includes('cbz') || activeMimetype.includes('cbr') || activeMimetype.includes('comicbook+zip') || activeMimetype.includes('comicbook-rar') || CBZ_EXTENSIONS.includes(activeExt); // Build EPUB viewer URL, propagating any deep-link params through to epub.html. let viewerUrl = null; if (isEpub) { const epubParams = new URLSearchParams(); epubParams.set('url', downloadUrl); if (locParam) epubParams.set('loc', locParam); if (qParam) epubParams.set('q', qParam); viewerUrl = `/epub/epub.html?${epubParams.toString()}`; } const openUrl = viewerUrl || (activePath ? `/media/${encodeMediaPath(activePath)}` : null); const canEmbed = isEpub || isPdf; // PDFs use the browser's built-in viewer, which honors `#page=N` fragments. const pdfSrc = isPdf && activePath ? `/media/${encodeMediaPath(activePath)}${pageParam ? `#page=${encodeURIComponent(pageParam)}` : ''}` : null; const embedUrl = isEpub ? viewerUrl : pdfSrc; const handleDelete = async (force = false) => { if (doc && doc.id) { const result = await deleteFileGroups([docFile.id], force); if (result && result.tagged) { setTaggedFileGroups(result.file_groups); return; } toast({ type: 'success', title: 'Document Deleted', description: 'Document was successfully deleted.', time: 2000, }); navigate(-1); } }; const localAddTag = async (name) => { await tagFileGroup(docFile, name); await fetchDoc(); }; const localRemoveTag = async (name) => { await untagFileGroup(docFile, name); await fetchDoc(); }; const publishedDatetimeString = docFile.published_datetime ? isoDatetimeToAgoPopup(docFile.published_datetime, true) : null; // Format badge based on mimetype. const formatLabel = (overrideMt) => { const mt = overrideMt || activeMimetype; if (mt.startsWith('application/epub')) return 'EPUB'; if (mt === 'application/x-mobipocket-ebook') return 'MOBI'; if (mt === 'application/pdf') return 'PDF'; if (mt.includes('wordprocessingml')) return 'DOCX'; if (mt === 'application/msword') return 'DOC'; if (mt.includes('opendocument.text')) return 'ODT'; if (mt.includes('cbz') || mt.includes('comicbook+zip')) return 'CBZ'; if (mt.includes('cbr') || mt.includes('comicbook-rar')) return 'CBR'; if (mt.includes('cbt')) return 'CBT'; if (mt.includes('cb7') || mt.includes('x-7z')) return 'CB7'; return mt.split('/').pop().toUpperCase(); }; // About tab. const aboutTab = <> {doc && doc.description && <>
Description

{doc.description}

} {doc && doc.publisher && <>
Publisher

{doc.publisher}

} {doc && doc.language && <>
Language

{doc.language}

} {doc && doc.subject && <>
Subject

{doc.subject}

} {doc && doc.page_count && <>
Page Count

{doc.page_count}

}
Size

{humanFileSize(size)}

; // Files tab. const tableStyle = {width: '100%', borderCollapse: 'separate', borderSpacing: '0 0.7em'}; const labelStyle = {whiteSpace: 'nowrap', paddingRight: '1.5em', verticalAlign: 'top'}; const filesTab = {docFile.files && docFile.files.map((file, idx) => )}
{file.path.split('.').pop().toUpperCase()} {file.path} {file.size && ({humanFileSize(file.size)}) }
Directory
; return <>
{isCbz && activePath && } {canEmbed && embedUrl && !isCbz &&
{/* * Only the PDF is gated. The EPUB viewer is our own same-origin document, so the * theme's media filter reaches it; Chrome's PDF viewer is composited out of * process and cannot be tinted. Keyed on the path so selecting a different file * re-gates rather than inheriting the last one's reveal. */}