'update'
This commit is contained in:
130
ipad_app/Sources/Services/BundleImporter.swift
Normal file
130
ipad_app/Sources/Services/BundleImporter.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
// BundleImporter.swift
|
||||
// 导入 ZIP 包并解析为 BundleIndex
|
||||
|
||||
import Foundation
|
||||
import ZIPFoundation
|
||||
import UniformTypeIdentifiers
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class BundleImporter: NSObject, ObservableObject {
|
||||
@Published var bundleIndex: BundleIndex?
|
||||
@Published var progressMessage: String? = nil
|
||||
@Published var errorMessage: String? = nil
|
||||
@Published var lastImportedURL: URL? = nil
|
||||
|
||||
var index: BundleIndex? { bundleIndex }
|
||||
private let fm = FileManager.default
|
||||
|
||||
// 公开导入方法供CLI使用
|
||||
func importZipFile(at url: URL) async throws -> BundleIndex {
|
||||
let ts = Int(Date().timeIntervalSince1970)
|
||||
let baseDir = try bundleBaseDirectory()
|
||||
let target = baseDir.appendingPathComponent("bundle_\(ts)")
|
||||
try fm.createDirectory(at: target, withIntermediateDirectories: true)
|
||||
try fm.unzipItem(at: url, to: target)
|
||||
let idx = try parseBundle(at: target)
|
||||
await MainActor.run {
|
||||
self.bundleIndex = idx
|
||||
self.lastImportedURL = url
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// iOS 选择器
|
||||
func presentPicker() {
|
||||
#if canImport(UIKit)
|
||||
let types: [UTType] = [UTType.filenameExtension("zip")!]
|
||||
let picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
|
||||
picker.delegate = self
|
||||
picker.allowsMultipleSelection = false
|
||||
UIApplication.shared.firstKeyWindow?.rootViewController?.present(picker, animated: true)
|
||||
#else
|
||||
print("[BundleImporter] presentPicker() unsupported on this platform")
|
||||
#endif
|
||||
}
|
||||
|
||||
// 存储目录
|
||||
private func bundleBaseDirectory() throws -> URL {
|
||||
let appSup = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let dir = appSup.appendingPathComponent("ImportedBundle")
|
||||
if !fm.fileExists(atPath: dir.path) { try fm.createDirectory(at: dir, withIntermediateDirectories: true) }
|
||||
return dir
|
||||
}
|
||||
|
||||
@MainActor private func update(message: String?) { self.progressMessage = message }
|
||||
|
||||
// 解析内容
|
||||
private func parseBundle(at dir: URL) throws -> BundleIndex {
|
||||
func load<T:Decodable>(_ name: String, _ type: T.Type) throws -> T {
|
||||
let data = try Data(contentsOf: dir.appendingPathComponent(name))
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
let books: [BookMeta] = try load("books_meta.json", [BookMeta].self)
|
||||
let stats: StatsPayload = try load("stats.json", StatsPayload.self)
|
||||
let introData = try? Data(contentsOf: dir.appendingPathComponent("bookintro.json"))
|
||||
let bookIntro = (introData.flatMap { try? JSONDecoder().decode([String:String].self, from: $0) }) ?? [:]
|
||||
|
||||
let annotationsRaw = try Data(contentsOf: dir.appendingPathComponent("annotations.json"))
|
||||
let anyMap = (try? JSONSerialization.jsonObject(with: annotationsRaw)) as? [String:Any] ?? [:]
|
||||
var annMap: [String:[AnnotationRow]] = [:]
|
||||
for (k,v) in anyMap {
|
||||
guard let arr = v as? [[String:Any]] else { continue }
|
||||
let rows: [AnnotationRow] = arr.compactMap { d in
|
||||
guard let uuid = d["uuid"] as? String else { return nil }
|
||||
return AnnotationRow(
|
||||
uuid: uuid,
|
||||
creationdate: d["creationdate"] as? String,
|
||||
idref: d["idref"] as? String,
|
||||
filepos: d["filepos"] as? String,
|
||||
selected: d["selected"] as? String,
|
||||
note: d["note"] as? String
|
||||
)
|
||||
}
|
||||
annMap[k] = rows
|
||||
}
|
||||
return BundleIndex(books: books, stats: stats, bookIntro: bookIntro, annotationsMap: annMap)
|
||||
}
|
||||
|
||||
// 导入执行
|
||||
private func importZip(at url: URL) async {
|
||||
await update(message: "解压中...")
|
||||
do {
|
||||
let ts = Int(Date().timeIntervalSince1970)
|
||||
let baseDir = try bundleBaseDirectory()
|
||||
let target = baseDir.appendingPathComponent("bundle_\(ts)")
|
||||
try fm.createDirectory(at: target, withIntermediateDirectories: true)
|
||||
try fm.unzipItem(at: url, to: target)
|
||||
await update(message: "解析 JSON...")
|
||||
let idx = try parseBundle(at: target)
|
||||
await MainActor.run {
|
||||
self.bundleIndex = idx
|
||||
self.lastImportedURL = url
|
||||
}
|
||||
await update(message: nil)
|
||||
} catch {
|
||||
await MainActor.run { self.errorMessage = error.localizedDescription }
|
||||
await update(message: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
extension BundleImporter: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else { return }
|
||||
lastImportedURL = url
|
||||
Task { await importZip(at: url) }
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
var firstKeyWindow: UIWindow? {
|
||||
self.connectedScenes.compactMap { $0 as? UIWindowScene }
|
||||
.flatMap { $0.windows }
|
||||
.first { $0.isKeyWindow }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user