fix(import): 修复本地 TXT 文件选择与导入

- 书架和文件管理统一使用 TXT 文件选择器,支持大小写扩展名及多选,排除 JSON 书源
- 保留所选文件访问权限至异步导入完成,等待选择器关闭后展示导入确认
- 补充双入口文件选择回归、导入校验和 TXT 测试样本
This commit is contained in:
Aminark
2026-10-22 10:51:09 +08:00
parent 7daa6b5635
commit bf72ebc894
9 changed files with 252 additions and 37 deletions
+79
View File
@@ -207,9 +207,88 @@ struct ReadingPhotoPickerController: UIViewControllerRepresentable {
}
}
private struct ReadingTXTFileImporterModifier: ViewModifier {
@Binding var isPresented: Bool
let onFiles: ([LocalBookImportFile]) -> Void
let onError: (Error) -> Void
@State private var pickedFiles: [LocalBookImportFile] = []
@State private var pickerHasDismissed = false
func body(content: Content) -> some View {
content.sheet(isPresented: $isPresented, onDismiss: {
pickerHasDismissed = true
deliverFilesWhenReady()
}) {
ReadingTXTFilePicker { files in
pickedFiles = files
isPresented = false
deliverFilesWhenReady()
} onCancel: {
pickedFiles = []
isPresented = false
}
.onAppear {
pickedFiles = []
pickerHasDismissed = false
}
}
}
private func deliverFilesWhenReady() {
// Providers may return their copies before or after dismissing Files.
guard pickerHasDismissed, !pickedFiles.isEmpty else { return }
let files = pickedFiles
pickedFiles = []
guard files.allSatisfy({ $0.url.pathExtension.lowercased() == "txt" && !$0.url.hasDirectoryPath }) else {
onError(LocalBookError.unsupportedFormat)
return
}
onFiles(files)
}
}
private struct ReadingTXTFilePicker: UIViewControllerRepresentable {
let onPick: @MainActor ([LocalBookImportFile]) -> Void
let onCancel: @MainActor () -> Void
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
// Plain text includes UTF-8/UTF-16 TXT, while .text and .data also allow JSON.
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.plainText], asCopy: true)
picker.allowsMultipleSelection = true
picker.shouldShowFileExtensions = true
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ controller: UIDocumentPickerViewController, context: Context) {}
@MainActor
final class Coordinator: NSObject, UIDocumentPickerDelegate {
let parent: ReadingTXTFilePicker
init(parent: ReadingTXTFilePicker) { self.parent = parent }
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { parent.onCancel() }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.onPick(urls.map { LocalBookImportFile(url: $0) })
}
}
}
enum ReadingSheetDetent { case medium, large }
extension View {
func readingTXTFileImporter(
isPresented: Binding<Bool>,
onFiles: @escaping ([LocalBookImportFile]) -> Void,
onError: @escaping (Error) -> Void
) -> some View {
modifier(ReadingTXTFileImporterModifier(isPresented: isPresented, onFiles: onFiles, onError: onError))
}
func readingPhotoPicker(
isPresented: Binding<Bool>,
onData: @escaping (Data) -> Void,