mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
153 lines
6.9 KiB
Swift
153 lines
6.9 KiB
Swift
import SwiftUI
|
|
|
|
@MainActor
|
|
final class LocalBookDetailViewModel: ObservableObject {
|
|
@Published private(set) var chapters: [BookChapter] = []
|
|
@Published private(set) var isLoading = false
|
|
@Published private(set) var errorMessage: String?
|
|
|
|
private let service = LocalBookService.live()
|
|
|
|
func load(book: Book) async {
|
|
guard chapters.isEmpty, !isLoading else { return }
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
do {
|
|
let report = try await Task.detached { [service] in try service.loadChapters(book: book) }.value
|
|
chapters = report.chapters
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LocalBookDetailView: View {
|
|
@EnvironmentObject private var library: BookLibraryModel
|
|
@Environment(\.dismiss) private var dismiss
|
|
let book: Book
|
|
@StateObject private var model = LocalBookDetailViewModel()
|
|
@State private var showsTableOfContents = false
|
|
@State private var showsRemoveConfirmation = false
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 16) {
|
|
VStack(spacing: 14) {
|
|
BookCoverArtwork(title: book.name, author: book.author, width: 108, height: 154)
|
|
Text(book.name).font(.title2.bold())
|
|
HStack {
|
|
StatusPill(text: "本地 TXT", systemImage: "doc.text.fill")
|
|
if let charset = book.charset { StatusPill(text: charset, color: ReadingTheme.coral, systemImage: "character") }
|
|
}
|
|
Text(book.originName).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
.padding(.top, 12)
|
|
|
|
ReadingCard {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack {
|
|
Label("目录", systemImage: "list.bullet.rectangle.portrait.fill").font(.headline)
|
|
Spacer()
|
|
Text("\(model.chapters.count) 章").font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
.padding(.bottom, 12)
|
|
if model.isLoading {
|
|
HStack { Spacer(); ProgressView("正在识别章节…"); Spacer() }.padding(.vertical, 28)
|
|
} else if let error = model.errorMessage {
|
|
ReadingUnavailableView("无法读取本地书", systemImage: "doc.badge.ellipsis", description: Text(error))
|
|
} else {
|
|
ForEach(model.chapters.prefix(8)) { chapter in
|
|
Divider()
|
|
NavigationLink {
|
|
ReaderView(book: book, chapters: model.chapters, initialIndex: chapter.index)
|
|
} label: {
|
|
HStack(spacing: 12) {
|
|
Text(String(format: "%02d", chapter.index + 1))
|
|
.font(.caption.monospacedDigit()).foregroundStyle(.tertiary).frame(width: 28)
|
|
Text(chapter.title).font(.subheadline).foregroundStyle(.primary).lineLimit(2)
|
|
Spacer()
|
|
Image(systemName: "chevron.right").font(.caption.bold()).foregroundStyle(.tertiary)
|
|
}
|
|
.padding(.vertical, 12)
|
|
}
|
|
}
|
|
if model.chapters.count > 8 {
|
|
Divider()
|
|
Button("查看全部目录", systemImage: "list.bullet") {
|
|
showsTableOfContents = true
|
|
}
|
|
.frame(maxWidth: .infinity).padding(.vertical, 10)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 16).padding(.bottom, 30)
|
|
}
|
|
.background(ReadingTheme.canvas)
|
|
.navigationTitle(book.name)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Menu {
|
|
Button("全部目录", systemImage: "list.bullet") { showsTableOfContents = true }
|
|
Button("移出书架", systemImage: "trash", role: .destructive) { showsRemoveConfirmation = true }
|
|
} label: { Image(systemName: "ellipsis.circle") }
|
|
}
|
|
}
|
|
.task { await model.load(book: book) }
|
|
.sheet(isPresented: $showsTableOfContents) {
|
|
LocalBookTableOfContentsView(book: book, chapters: model.chapters)
|
|
}
|
|
.confirmationDialog("从书架移除《\(book.name)》?", isPresented: $showsRemoveConfirmation) {
|
|
Button("移出书架", role: .destructive) {
|
|
Task { await library.remove(bookURL: book.bookUrl); dismiss() }
|
|
}
|
|
Button("取消", role: .cancel) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct LocalBookTableOfContentsView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
let book: Book
|
|
let chapters: [BookChapter]
|
|
@State private var query = ""
|
|
@State private var descending = false
|
|
|
|
private var displayed: [BookChapter] {
|
|
let filtered = query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
? chapters
|
|
: chapters.filter { $0.title.localizedCaseInsensitiveContains(query) }
|
|
return descending ? Array(filtered.reversed()) : filtered
|
|
}
|
|
|
|
var body: some View {
|
|
ReadingNavigationStack {
|
|
List(displayed) { chapter in
|
|
NavigationLink {
|
|
ReaderView(book: book, chapters: chapters, initialIndex: chapter.index)
|
|
} label: {
|
|
HStack {
|
|
Text("\(chapter.index + 1)").font(.caption.monospacedDigit()).foregroundStyle(.secondary).frame(width: 42, alignment: .trailing)
|
|
Text(chapter.title).lineLimit(2)
|
|
Spacer()
|
|
if chapter.index == book.durChapterIndex {
|
|
Image(systemName: "location.fill").foregroundStyle(ReadingTheme.accent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.searchable(text: $query, prompt: "搜索目录")
|
|
.navigationTitle("目录")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) { Button("关闭") { dismiss() } }
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button(descending ? "正序" : "倒序", systemImage: "arrow.up.arrow.down") { descending.toggle() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|