131 lines
5.1 KiB
Swift
131 lines
5.1 KiB
Swift
// 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
|