openreader/src/components/doclist/DocumentListItem.tsx
Richard Roberson 42665884d7 feat(audiobook): add chapter-based export with UI and API
Introduce end-to-end chapterized audiobook generation with persistent
storage, resumable workflows, and MP3/M4B support.

API:
- add /api/audio/convert/chapter (GET/DELETE) for per-chapter ops
- add /api/audio/convert/chapters (GET/DELETE) for listing/reset
- enhance /api/audio/convert:
  - accept mp3|m4b, stream combined file, cache complete output
  - robust chapter indexing, docstore persistence, list concat
  - AbortSignal-aware ffmpeg/ffprobe, 499 on cancel

UI/UX:
- add AudiobookExportModal with progress, resume, regenerate, download
- add ProgressCard, enhance ProgressPopup (click-to-focus, richer info)
- add Header, ZoomControl; move TTSPlayer to sticky bottom bar
- redesign EPUB/HTML/PDF pages for full-height layout and controls
- add HomeContent; compact uploader variant; document list polish

TTS/Contexts:
- EPUB/PDF contexts now generate per-chapter to disk, return bookId
- support chapter regeneration; progress/cancel propagation
- pass provider/model/instructions; standardize MP3 TTS output
- HTML context updates for model/instructions

Styling:
- globals: overlay-dim, scrollbar styles, prism gradient utilities
- theme vars: secondary-accent, prism-gradient; tailwind color addition

Misc:
- fix PDF scale calc to use container height
- EPUB theme reader area fills height
- time estimation update cadence stab
- audio util passes format to combine endpoint
2025-11-11 13:32:30 -07:00

96 lines
No EOL
3.6 KiB
TypeScript

import Link from 'next/link';
import { DragEvent, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@headlessui/react';
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
import { DocumentListDocument } from '@/types/documents';
interface DocumentListItemProps {
doc: DocumentListDocument;
onDelete: (doc: DocumentListDocument) => void;
dragEnabled?: boolean;
onDragStart?: (doc: DocumentListDocument) => void;
onDragEnd?: () => void;
onDragOver?: (e: DragEvent, doc: DocumentListDocument) => void;
onDragLeave?: () => void;
onDrop?: (e: DragEvent, doc: DocumentListDocument) => void;
isDropTarget?: boolean;
}
export function DocumentListItem({
doc,
onDelete,
dragEnabled = true,
onDragStart,
onDragEnd,
onDragOver,
onDragLeave,
onDrop,
isDropTarget = false,
}: DocumentListItemProps) {
const [loading, setLoading] = useState(false);
const router = useRouter();
// Only allow drag and drop interactions for documents not in folders
const isDraggable = dragEnabled && !doc.folderId;
const allowDropTarget = !doc.folderId;
const handleDocumentClick = (e: React.MouseEvent) => {
e.preventDefault();
setLoading(true);
router.push(`/${doc.type}/${encodeURIComponent(doc.id)}`);
};
return (
<div
draggable={isDraggable}
onDragStart={() => onDragStart?.(doc)}
onDragEnd={onDragEnd}
onDragOver={(e) => allowDropTarget && onDragOver?.(e, doc)}
onDragLeave={() => allowDropTarget && onDragLeave?.()}
onDrop={(e) => allowDropTarget && onDrop?.(e, doc)}
aria-busy={loading}
className={`
w-full group
${allowDropTarget && isDropTarget ? 'ring-2 ring-accent' : ''}
${loading ? 'prism-outline' : 'bg-base hover:bg-offbase'}
border border-offbase rounded-md p-1
transition-colors duration-150 relative
`}
>
<div className="flex items-center">
<Link
href={`/${doc.type}/${encodeURIComponent(doc.id)}`}
draggable={false}
className="document-link flex items-center align-center gap-2 w-full truncate rounded-md py-0.5 px-0.5"
onClick={handleDocumentClick}
>
<div className="flex-shrink-0">
{doc.type === 'pdf' ? (
<PDFIcon className="w-5 h-5 sm:w-6 sm:h-6 text-red-500" />
) : doc.type === 'epub' ? (
<EPUBIcon className="w-5 h-5 sm:w-6 sm:h-6 text-blue-500" />
) : (
<FileIcon className="w-6 h-6 sm:w-6 sm:h-6 text-muted" />
)}
</div>
<div className="flex flex-col min-w-0 transform transition-transform duration-150 ease-in-out hover:scale-[1.009] w-full truncate">
<p className="text-[12px] sm:text-[13px] leading-tight text-foreground font-medium truncate group-hover:text-accent">{doc.name}</p>
<p className="text-[9px] sm:text-[10px] leading-tight text-muted truncate">
{(doc.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</Link>
<Button
onClick={() => onDelete(doc)}
className="ml-1 p-1.5 text-muted hover:text-accent rounded-md hover:bg-offbase transition-colors"
aria-label="Delete document"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</Button>
</div>
</div>
);
}