Refactor code structure for improved readability and maintainability
Build and Push Reader Image / docker (push) Successful in 1m51s
Build and Push Reader Image / docker (push) Successful in 1m51s
This commit is contained in:
@@ -4,8 +4,13 @@ export const runtime = "nodejs"
|
|||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const googleClientId =
|
const candidates = [
|
||||||
(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || process.env.GOOGLE_CLIENT_ID || "").trim()
|
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
|
||||||
|
process.env.WEB_GOOGLE_CLIENT_ID,
|
||||||
|
process.env.GOOGLE_CLIENT_ID,
|
||||||
|
]
|
||||||
|
const raw = String(candidates.find((value) => String(value || "").trim()) || "").trim()
|
||||||
|
const googleClientId = raw.includes(",") ? raw.split(",").map((part) => part.trim()).find(Boolean) || "" : raw
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { Check, Loader2, Search, Sparkles, Trash2, UploadCloud, X } from "lucide-react"
|
import { Check, Loader2, Sparkles, Trash2, UploadCloud, X, Search } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
@@ -10,18 +10,13 @@ import { toast } from "sonner"
|
|||||||
|
|
||||||
type AssetItem = {
|
type AssetItem = {
|
||||||
id: string
|
id: string
|
||||||
path: string
|
path?: string
|
||||||
title?: string | null
|
title?: string | null
|
||||||
author?: string | null
|
author?: string | null
|
||||||
status: string
|
status: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResponse = {
|
|
||||||
items: AssetItem[]
|
|
||||||
pagination: { page: number; limit: number; total: number; totalPages: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParsePreviewSample = {
|
type ParsePreviewSample = {
|
||||||
bucket: string
|
bucket: string
|
||||||
number: number
|
number: number
|
||||||
@@ -37,10 +32,9 @@ type Genre = {
|
|||||||
|
|
||||||
export function ImportClient() {
|
export function ImportClient() {
|
||||||
const [step, setStep] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
const [query, setQuery] = useState("")
|
const [uploadingEpub, setUploadingEpub] = useState(false)
|
||||||
const [searching, setSearching] = useState(false)
|
|
||||||
const [assets, setAssets] = useState<AssetItem[]>([])
|
|
||||||
const [asset, setAsset] = useState<AssetItem | null>(null)
|
const [asset, setAsset] = useState<AssetItem | null>(null)
|
||||||
|
const [epubFile, setEpubFile] = useState<File | null>(null)
|
||||||
const [coverDetected, setCoverDetected] = useState(false)
|
const [coverDetected, setCoverDetected] = useState(false)
|
||||||
const [coverPreviewUrl, setCoverPreviewUrl] = useState("")
|
const [coverPreviewUrl, setCoverPreviewUrl] = useState("")
|
||||||
const [uploadingCover, setUploadingCover] = useState(false)
|
const [uploadingCover, setUploadingCover] = useState(false)
|
||||||
@@ -63,11 +57,7 @@ export function ImportClient() {
|
|||||||
|
|
||||||
const [aiLoading, setAiLoading] = useState(false)
|
const [aiLoading, setAiLoading] = useState(false)
|
||||||
const [importing, setImporting] = useState(false)
|
const [importing, setImporting] = useState(false)
|
||||||
const [sessionId, setSessionId] = useState("")
|
|
||||||
const [phase, setPhase] = useState("prepare")
|
|
||||||
const [progress, setProgress] = useState(0)
|
|
||||||
const [result, setResult] = useState<Record<string, unknown> | null>(null)
|
const [result, setResult] = useState<Record<string, unknown> | null>(null)
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
||||||
|
|
||||||
const normalizedGenreQuery = genreQuery.trim().toLowerCase()
|
const normalizedGenreQuery = genreQuery.trim().toLowerCase()
|
||||||
const exactMatchedGenre = useMemo(
|
const exactMatchedGenre = useMemo(
|
||||||
@@ -176,39 +166,40 @@ export function ImportClient() {
|
|||||||
return [...new Set(ids)].slice(0, 6)
|
return [...new Set(ids)].slice(0, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSearch = async () => {
|
const onUploadEpub = async (file: File) => {
|
||||||
if (query.trim().length < 2) {
|
setUploadingEpub(true)
|
||||||
toast.warning("Nhập ít nhất 2 ký tự để tìm")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSearching(true)
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/search?q=${encodeURIComponent(query.trim())}&page=1&limit=20`, { credentials: "include" })
|
const form = new FormData()
|
||||||
const data = (await res.json()) as SearchResponse
|
form.append("file", file)
|
||||||
if (!res.ok) throw new Error((data as unknown as { detail?: string }).detail || "Search failed")
|
const res = await fetch("/api/import/uploads/preview", { method: "POST", credentials: "include", body: form })
|
||||||
setAssets(data.items || [])
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data?.detail || "Không tải được EPUB")
|
||||||
|
setEpubFile(file)
|
||||||
|
const item: AssetItem = {
|
||||||
|
id: `preview-${Date.now()}`,
|
||||||
|
title: data.suggested?.title || file.name,
|
||||||
|
author: data.suggested?.author || "Unknown",
|
||||||
|
status: "discovered",
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await onSelectAsset(item, data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Không tìm được asset")
|
toast.error(error instanceof Error ? error.message : "Không tải được EPUB")
|
||||||
} finally {
|
} finally {
|
||||||
setSearching(false)
|
setUploadingEpub(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelectAsset = async (item: AssetItem) => {
|
const onSelectAsset = async (item: AssetItem, previewData?: any) => {
|
||||||
setAsset(item)
|
setAsset(item)
|
||||||
setStep(2)
|
setStep(2)
|
||||||
setCoverPreviewUrl("")
|
setCoverPreviewUrl("")
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/${item.id}/preview-metadata`, { credentials: "include" })
|
const data = previewData || {}
|
||||||
const data = await res.json()
|
|
||||||
if (!res.ok) throw new Error(data?.detail || "Không lấy được metadata")
|
|
||||||
const suggested = data?.suggested || {}
|
const suggested = data?.suggested || {}
|
||||||
setCoverDetected(Boolean(data?.asset?.coverDetected))
|
setCoverDetected(Boolean(data?.coverDetected))
|
||||||
if (data?.asset?.coverDetected) {
|
setCoverPreviewUrl(typeof data?.coverPreviewDataUrl === "string" ? data.coverPreviewDataUrl : "")
|
||||||
const ts = Date.now()
|
setTitle(suggested.title || item.title || "")
|
||||||
setCoverPreviewUrl(`/api/import/assets/${item.id}/preview-cover?t=${ts}`)
|
|
||||||
}
|
|
||||||
setTitle(suggested.title || item.title || item.path.split("/").pop()?.replace(/\.epub$/i, "") || "")
|
|
||||||
setAuthor(suggested.author || item.author || "Unknown")
|
setAuthor(suggested.author || item.author || "Unknown")
|
||||||
setShortDescription(suggested.shortDescription || "")
|
setShortDescription(suggested.shortDescription || "")
|
||||||
const genreList = await fetchGenres()
|
const genreList = await fetchGenres()
|
||||||
@@ -227,19 +218,8 @@ export function ImportClient() {
|
|||||||
if (!asset) return
|
if (!asset) return
|
||||||
setUploadingCover(true)
|
setUploadingCover(true)
|
||||||
try {
|
try {
|
||||||
const form = new FormData()
|
|
||||||
form.append("file", file)
|
|
||||||
const res = await fetch(`/api/import/assets/${asset.id}/upload-cover`, {
|
|
||||||
method: "POST",
|
|
||||||
credentials: "include",
|
|
||||||
body: form,
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
if (!res.ok) throw new Error(data?.detail || "Upload cover that bai")
|
|
||||||
setCoverDetected(true)
|
setCoverDetected(true)
|
||||||
if (data?.coverUrl) {
|
setCoverPreviewUrl(URL.createObjectURL(file))
|
||||||
setCoverPreviewUrl(data.coverUrl)
|
|
||||||
}
|
|
||||||
toast.success("Da upload cover thay the")
|
toast.success("Da upload cover thay the")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Upload cover that bai")
|
toast.error(error instanceof Error ? error.message : "Upload cover that bai")
|
||||||
@@ -249,24 +229,24 @@ export function ImportClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onAiSuggest = async () => {
|
const onAiSuggest = async () => {
|
||||||
if (!asset) return
|
if (!asset || !epubFile) return
|
||||||
setAiLoading(true)
|
setAiLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/${asset.id}/ai-suggest`, {
|
const form = new FormData()
|
||||||
method: "POST",
|
form.append("file", epubFile)
|
||||||
headers: { "Content-Type": "application/json" },
|
form.append("preview", "true")
|
||||||
credentials: "include",
|
form.append("splitMode", splitMode)
|
||||||
body: JSON.stringify({ splitMode, chapterStartPattern: splitMode === "regex" ? chapterStartPattern : null }),
|
if (splitMode === "regex") form.append("chapterRegex", chapterStartPattern)
|
||||||
})
|
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data?.detail || "AI suggest failed")
|
if (!res.ok) throw new Error(data?.detail || "AI suggest failed")
|
||||||
const suggestedGenres: string[] = (data?.suggestedGenres || []).slice(0, 6)
|
const suggestedGenres: string[] = (data?.novel?.detectedGenres || []).slice(0, 6)
|
||||||
setGenreQuery("")
|
setGenreQuery("")
|
||||||
if (suggestedGenres.length > 0) {
|
if (suggestedGenres.length > 0) {
|
||||||
const ensuredIds = await ensureGenreIdsByNames(suggestedGenres)
|
const ensuredIds = await ensureGenreIdsByNames(suggestedGenres)
|
||||||
setSelectedGenreIds(ensuredIds)
|
setSelectedGenreIds(ensuredIds)
|
||||||
}
|
}
|
||||||
setShortDescription(data?.shortDescription || "")
|
if (!shortDescription) setShortDescription(data?.novel?.description || "")
|
||||||
toast.success("Đã áp dụng gợi ý AI")
|
toast.success("Đã áp dụng gợi ý AI")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "AI suggest lỗi")
|
toast.error(error instanceof Error ? error.message : "AI suggest lỗi")
|
||||||
@@ -278,21 +258,6 @@ export function ImportClient() {
|
|||||||
const onSaveReview = async () => {
|
const onSaveReview = async () => {
|
||||||
if (!asset) return
|
if (!asset) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/${asset.id}/review`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({
|
|
||||||
title,
|
|
||||||
author,
|
|
||||||
shortDescription,
|
|
||||||
genres: selectedGenreItems.map((g) => g.name),
|
|
||||||
targetMode: "new",
|
|
||||||
replaceExisting,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
if (!res.ok) throw new Error(data?.detail || "Lưu review thất bại")
|
|
||||||
toast.success("Đã lưu review")
|
toast.success("Đã lưu review")
|
||||||
setPreviewItems([])
|
setPreviewItems([])
|
||||||
setChapterCount(0)
|
setChapterCount(0)
|
||||||
@@ -303,23 +268,24 @@ export function ImportClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onParsePreview = async () => {
|
const onParsePreview = async () => {
|
||||||
if (!asset) return
|
if (!asset || !epubFile) return
|
||||||
setPreviewLoading(true)
|
setPreviewLoading(true)
|
||||||
setPreviewItems([])
|
setPreviewItems([])
|
||||||
setChapterCount(0)
|
setChapterCount(0)
|
||||||
setParseError("")
|
setParseError("")
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/${asset.id}/parse-preview`, {
|
const form = new FormData()
|
||||||
method: "POST",
|
form.append("file", epubFile)
|
||||||
headers: { "Content-Type": "application/json" },
|
form.append("preview", "true")
|
||||||
credentials: "include",
|
form.append("splitMode", splitMode)
|
||||||
body: JSON.stringify({ splitMode, chapterStartPattern: splitMode === "regex" ? chapterStartPattern : null }),
|
if (splitMode === "regex") form.append("chapterRegex", chapterStartPattern)
|
||||||
})
|
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data?.detail || "Parse preview thất bại")
|
if (!res.ok) throw new Error(data?.detail || "Parse preview thất bại")
|
||||||
setPreviewItems(data?.sample || [])
|
const chapters = Array.isArray(data?.chaptersPreview) ? data.chaptersPreview : []
|
||||||
setChapterCount(data?.chapterCount || 0)
|
setPreviewItems(chapters.map((c: any) => ({ bucket: "preview", number: c.number || 0, title: c.title || "", chars: (c.excerpt || "").length, preview: c.excerpt || "" })))
|
||||||
if ((data?.chapterCount || 0) <= 0) {
|
setChapterCount(Number(data?.novel?.totalChapters || chapters.length || 0))
|
||||||
|
if ((Number(data?.novel?.totalChapters || chapters.length || 0)) <= 0) {
|
||||||
setParseError("Không tách được chương từ EPUB này với cấu hình hiện tại. Thử đổi TOC/Regex rồi parse lại.")
|
setParseError("Không tách được chương từ EPUB này với cấu hình hiện tại. Thử đổi TOC/Regex rồi parse lại.")
|
||||||
}
|
}
|
||||||
toast.success("Đã tạo preview chương")
|
toast.success("Đã tạo preview chương")
|
||||||
@@ -331,76 +297,32 @@ export function ImportClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollSession = async (id: string) => {
|
|
||||||
if (pollRef.current) {
|
|
||||||
clearInterval(pollRef.current)
|
|
||||||
pollRef.current = null
|
|
||||||
}
|
|
||||||
pollRef.current = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/import/sessions/${id}`, { credentials: "include" })
|
|
||||||
const data = await res.json()
|
|
||||||
if (!res.ok) throw new Error(data?.detail || "Không lấy được tiến trình")
|
|
||||||
setPhase(data.phase || "prepare")
|
|
||||||
setProgress(Number(data.progressPct || 0))
|
|
||||||
if (data.status === "completed" || data.status === "failed") {
|
|
||||||
if (pollRef.current) {
|
|
||||||
clearInterval(pollRef.current)
|
|
||||||
pollRef.current = null
|
|
||||||
}
|
|
||||||
setImporting(false)
|
|
||||||
setResult(data.resultJson || null)
|
|
||||||
if (data.status === "completed") {
|
|
||||||
toast.success("Import hoàn tất")
|
|
||||||
} else {
|
|
||||||
toast.error(data.log || "Import thất bại")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (pollRef.current) {
|
|
||||||
clearInterval(pollRef.current)
|
|
||||||
pollRef.current = null
|
|
||||||
}
|
|
||||||
setImporting(false)
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStartImport = async () => {
|
const onStartImport = async () => {
|
||||||
if (!asset) return
|
if (!asset || !epubFile) return
|
||||||
setImporting(true)
|
setImporting(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/import/assets/${asset.id}/start-import`, {
|
const form = new FormData()
|
||||||
method: "POST",
|
form.append("file", epubFile)
|
||||||
headers: { "Content-Type": "application/json" },
|
form.append("preview", "false")
|
||||||
credentials: "include",
|
form.append("splitMode", splitMode)
|
||||||
body: JSON.stringify({
|
if (splitMode === "regex") form.append("chapterRegex", chapterStartPattern)
|
||||||
replaceExisting,
|
form.append("title", title)
|
||||||
splitMode,
|
form.append("authorName", author)
|
||||||
chapterStartPattern: splitMode === "regex" ? chapterStartPattern : null,
|
form.append("description", shortDescription)
|
||||||
}),
|
form.append("replaceExisting", String(replaceExisting))
|
||||||
})
|
const res = await fetch("/api/mod/epub", { method: "POST", credentials: "include", body: form })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data?.detail || "Start import thất bại")
|
if (!res.ok) throw new Error(data?.detail || "Start import thất bại")
|
||||||
if (!data?.sessionId) throw new Error("Missing sessionId from start-import")
|
setResult(data || null)
|
||||||
setSessionId(data.sessionId)
|
|
||||||
setStep(4)
|
setStep(4)
|
||||||
pollSession(data.sessionId)
|
setImporting(false)
|
||||||
|
toast.success("Import hoàn tất")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setImporting(false)
|
setImporting(false)
|
||||||
toast.error(error instanceof Error ? error.message : "Không bắt đầu import được")
|
toast.error(error instanceof Error ? error.message : "Không bắt đầu import được")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (pollRef.current) {
|
|
||||||
clearInterval(pollRef.current)
|
|
||||||
pollRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreviewItems([])
|
setPreviewItems([])
|
||||||
setChapterCount(0)
|
setChapterCount(0)
|
||||||
@@ -423,7 +345,7 @@ export function ImportClient() {
|
|||||||
<div className="space-y-6 p-4 md:p-6">
|
<div className="space-y-6 p-4 md:p-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">Import EPUB Wizard</h1>
|
<h1 className="text-2xl font-bold">Import EPUB Wizard</h1>
|
||||||
<p className="text-sm text-muted-foreground">4 bước: Search -> Metadata -> Chapter Preview -> Import</p>
|
<p className="text-sm text-muted-foreground">4 bước: Upload -> Metadata -> Chapter Preview -> Import</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 md:grid-cols-4">
|
<div className="grid gap-2 md:grid-cols-4">
|
||||||
@@ -436,30 +358,20 @@ export function ImportClient() {
|
|||||||
|
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<section className="space-y-3 rounded-xl border p-4">
|
<section className="space-y-3 rounded-xl border p-4">
|
||||||
<div className="flex gap-2">
|
<p className="text-sm text-muted-foreground">Chọn file EPUB từ máy của bạn để bắt đầu.</p>
|
||||||
<Input
|
<Input
|
||||||
value={query}
|
type="file"
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
accept=".epub,application/epub+zip"
|
||||||
onKeyDown={(e) => {
|
disabled={uploadingEpub}
|
||||||
if (e.key === "Enter") {
|
onChange={(e) => {
|
||||||
e.preventDefault()
|
const file = e.target.files?.[0]
|
||||||
if (!searching) {
|
if (file) {
|
||||||
onSearch()
|
void onUploadEpub(file)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
placeholder="Tìm theo tên EPUB..."
|
|
||||||
/>
|
/>
|
||||||
<Button onClick={onSearch} disabled={searching}>{searching ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />} Tìm</Button>
|
{uploadingEpub && <p className="text-xs text-muted-foreground">Đang tải và phân tích EPUB...</p>}
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{assets.map((item) => (
|
|
||||||
<button key={item.id} type="button" onClick={() => onSelectAsset(item)} className="w-full rounded-lg border p-3 text-left hover:border-primary/60">
|
|
||||||
<p className="font-medium">{item.title || item.path.split("/").pop()}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{item.path}</p>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -474,7 +386,16 @@ export function ImportClient() {
|
|||||||
<div className="flex flex-wrap items-start gap-4 rounded-md border bg-muted/20 p-3">
|
<div className="flex flex-wrap items-start gap-4 rounded-md border bg-muted/20 p-3">
|
||||||
<div className="h-44 w-32 overflow-hidden rounded border bg-background">
|
<div className="h-44 w-32 overflow-hidden rounded border bg-background">
|
||||||
{coverPreviewUrl ? (
|
{coverPreviewUrl ? (
|
||||||
<img src={coverPreviewUrl} alt="Cover preview" className="h-full w-full object-cover" />
|
<img
|
||||||
|
src={coverPreviewUrl}
|
||||||
|
alt="Cover preview"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
onError={() => {
|
||||||
|
setCoverPreviewUrl("")
|
||||||
|
setCoverDetected(false)
|
||||||
|
toast.error("Khong tai duoc cover preview tu server")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">Khong co preview</div>
|
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">Khong co preview</div>
|
||||||
)}
|
)}
|
||||||
@@ -600,11 +521,7 @@ export function ImportClient() {
|
|||||||
|
|
||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<section className="space-y-3 rounded-xl border p-4">
|
<section className="space-y-3 rounded-xl border p-4">
|
||||||
<h2 className="font-semibold">Import progress</h2>
|
<h2 className="font-semibold">Import result</h2>
|
||||||
<p className="text-sm text-muted-foreground">Session: {sessionId}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Phase: {phase}</p>
|
|
||||||
<Progress value={progress} />
|
|
||||||
<p className="text-sm">{Math.round(progress)}%</p>
|
|
||||||
{result && (
|
{result && (
|
||||||
<pre className="overflow-auto rounded bg-muted p-3 text-xs">{JSON.stringify(result, null, 2)}</pre>
|
<pre className="overflow-auto rounded bg-muted p-3 text-xs">{JSON.stringify(result, null, 2)}</pre>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ interface EpubPreviewData {
|
|||||||
splitMode: "toc" | "regex"
|
splitMode: "toc" | "regex"
|
||||||
detectedStructureType: "standard" | "light_novel"
|
detectedStructureType: "standard" | "light_novel"
|
||||||
hasCoverFromEpub?: boolean
|
hasCoverFromEpub?: boolean
|
||||||
|
coverPreviewDataUrl?: string | null
|
||||||
parserInfo?: {
|
parserInfo?: {
|
||||||
splitMode: "toc" | "regex"
|
splitMode: "toc" | "regex"
|
||||||
chapterRegexUsed?: string | null
|
chapterRegexUsed?: string | null
|
||||||
@@ -1385,6 +1386,15 @@ export function NovelClient() {
|
|||||||
<span className="font-semibold">Cover từ EPUB:</span>{" "}
|
<span className="font-semibold">Cover từ EPUB:</span>{" "}
|
||||||
{epubPreviewData.hasCoverFromEpub ? "Có (sẽ tự gán làm ảnh bìa)" : "Không tìm thấy cover"}
|
{epubPreviewData.hasCoverFromEpub ? "Có (sẽ tự gán làm ảnh bìa)" : "Không tìm thấy cover"}
|
||||||
</p>
|
</p>
|
||||||
|
{epubPreviewData.coverPreviewDataUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={epubPreviewData.coverPreviewDataUrl}
|
||||||
|
alt="EPUB cover preview"
|
||||||
|
className="h-40 w-28 rounded-md border object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p>
|
<p>
|
||||||
<span className="font-semibold">Nhận diện cấu trúc:</span>{" "}
|
<span className="font-semibold">Nhận diện cấu trúc:</span>{" "}
|
||||||
{epubPreviewData.detectedStructureType === "light_novel" ? "Theo quyển" : "Chuẩn"}
|
{epubPreviewData.detectedStructureType === "light_novel" ? "Theo quyển" : "Chuẩn"}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Generated
+7181
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user