Extract source restoration helpers
This commit is contained in:
parent
3a30b94369
commit
78ab078674
@ -892,7 +892,10 @@ final class SourceLibrary: ObservableObject {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if sourceNeedsReconcile(refreshedSource) {
|
if SourceRestoration.needsReconcile(
|
||||||
|
refreshedSource,
|
||||||
|
currentCollectionSnapshots: currentCollectionSnapshots(for:)
|
||||||
|
) {
|
||||||
queueAutomaticSync(
|
queueAutomaticSync(
|
||||||
for: sourceID,
|
for: sourceID,
|
||||||
reason: refreshedSource.hasCachedContent
|
reason: refreshedSource.hasCachedContent
|
||||||
@ -1145,44 +1148,9 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
var source = MinecraftSource(
|
let source = SourceRestoration.restoredSource(from: record) { [connectedDeviceSourceFactory] device, container in
|
||||||
sourceID: record.sourceID,
|
connectedDeviceSourceFactory.displayName(for: device, container: container)
|
||||||
folderURL: record.folderURL,
|
|
||||||
bookmarkData: record.bookmarkData,
|
|
||||||
origin: record.origin,
|
|
||||||
accessDescriptor: record.accessDescriptor,
|
|
||||||
availability: record.availability
|
|
||||||
)
|
|
||||||
if case .connectedDevice(let device, let container) = source.origin {
|
|
||||||
var repairedDevice = device
|
|
||||||
repairedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
|
|
||||||
currentName: device.name,
|
|
||||||
fallbackDeviceName: "",
|
|
||||||
fallbackDisplayName: record.displayName
|
|
||||||
)
|
|
||||||
source.origin = .connectedDevice(device: repairedDevice, container: container)
|
|
||||||
let persistedDeviceName = record.displayName.components(separatedBy: " • ").first ?? record.displayName
|
|
||||||
if ConnectedDeviceSourcePolicy.sanitizedDeviceName(persistedDeviceName) == nil {
|
|
||||||
source.displayName = connectedDeviceSourceFactory.displayName(
|
|
||||||
for: repairedDevice,
|
|
||||||
container: container
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
source.displayName = record.displayName
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
source.displayName = record.displayName
|
|
||||||
}
|
|
||||||
source.rawItems = record.rawItems
|
|
||||||
source.indexedItemCount = record.rawItems.count
|
|
||||||
source.indexedDetailCount = record.rawItems.filter(\.metadataLoaded).count
|
|
||||||
source.previewLoadedCount = record.rawItems.filter(\.previewLoaded).count
|
|
||||||
source.sizeLoadedCount = record.rawItems.filter(\.sizeLoaded).count
|
|
||||||
source.lastScanDate = record.lastScanDate
|
|
||||||
source.snapshot = record.snapshot
|
|
||||||
source.scanStatus = source.indexedItemCount == 0
|
|
||||||
? "No Minecraft items found."
|
|
||||||
: "Loaded \(source.indexedDetailCount) items."
|
|
||||||
|
|
||||||
sources.append(source)
|
sources.append(source)
|
||||||
rebuildNormalizedIndex(for: source.id)
|
rebuildNormalizedIndex(for: source.id)
|
||||||
@ -1198,15 +1166,14 @@ final class SourceLibrary: ObservableObject {
|
|||||||
await Task.yield()
|
await Task.yield()
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
let restoredItems = await restoreCachedImages(in: record.rawItems)
|
let restoredItems = await SourceRestoration.restoreCachedImages(in: record.rawItems)
|
||||||
updateSource(record.sourceID) { source in
|
updateSource(record.sourceID) { source in
|
||||||
source.rawItems = restoredItems
|
SourceRestoration.applyRestoredItemState(
|
||||||
source.indexedItemCount = restoredItems.count
|
restoredItems,
|
||||||
source.indexedDetailCount = restoredItems.filter(\.metadataLoaded).count
|
lastScanDate: record.lastScanDate,
|
||||||
source.previewLoadedCount = restoredItems.filter(\.previewLoaded).count
|
snapshot: record.snapshot,
|
||||||
source.sizeLoadedCount = restoredItems.filter(\.sizeLoaded).count
|
to: &source
|
||||||
source.lastScanDate = record.lastScanDate
|
)
|
||||||
source.snapshot = record.snapshot
|
|
||||||
}
|
}
|
||||||
rebuildNormalizedIndex(for: record.sourceID)
|
rebuildNormalizedIndex(for: record.sourceID)
|
||||||
}
|
}
|
||||||
@ -1216,131 +1183,16 @@ final class SourceLibrary: ObservableObject {
|
|||||||
scheduleRestoredSourceRefreshes(records: records)
|
scheduleRestoredSourceRefreshes(records: records)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restoreCachedImages(in items: [MinecraftContentItem]) async -> [MinecraftContentItem] {
|
|
||||||
var restoredItems: [MinecraftContentItem] = []
|
|
||||||
restoredItems.reserveCapacity(items.count)
|
|
||||||
|
|
||||||
for var item in items {
|
|
||||||
item.iconURL = await ImageCacheStore.shared.cachedImageURL(for: item.iconURL)
|
|
||||||
item.packReferences = await restoreCachedImages(in: item.packReferences)
|
|
||||||
restoredItems.append(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return restoredItems
|
|
||||||
}
|
|
||||||
|
|
||||||
private func restoreCachedImages(in references: [ContentPackReference]) async -> [ContentPackReference] {
|
|
||||||
var restoredReferences: [ContentPackReference] = []
|
|
||||||
restoredReferences.reserveCapacity(references.count)
|
|
||||||
|
|
||||||
for reference in references {
|
|
||||||
let cachedIconURL = await ImageCacheStore.shared.cachedImageURL(for: reference.iconURL)
|
|
||||||
restoredReferences.append(
|
|
||||||
ContentPackReference(
|
|
||||||
name: reference.name,
|
|
||||||
type: reference.type,
|
|
||||||
iconURL: cachedIconURL,
|
|
||||||
uuid: reference.uuid,
|
|
||||||
version: reference.version,
|
|
||||||
source: reference.source
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return restoredReferences
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sourceNeedsRescan(_ record: PersistedSourceRecord) -> Bool {
|
|
||||||
guard record.accessDescriptor.refreshStrategy == .eagerFullScan else {
|
|
||||||
return record.rawItems.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let snapshot = record.snapshot else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceURL = record.folderURL
|
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: sourceURL.path) else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentCollections = Dictionary(uniqueKeysWithValues: currentCollectionSnapshots(for: sourceURL).map { ($0.folderName, $0) })
|
|
||||||
let persistedCollections = Dictionary(uniqueKeysWithValues: snapshot.collectionSnapshots.map { ($0.folderName, $0) })
|
|
||||||
|
|
||||||
if currentCollections.count != persistedCollections.count {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for (folderName, persistedCollection) in persistedCollections {
|
|
||||||
guard let currentCollection = currentCollections[folderName],
|
|
||||||
currentCollection.fingerprint == persistedCollection.fingerprint else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sourceNeedsReconcile(_ source: MinecraftSource) -> Bool {
|
|
||||||
guard source.accessDescriptor.refreshStrategy == .eagerFullScan else {
|
|
||||||
return source.rawItems.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let snapshot = source.snapshot else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceURL = source.folderURL
|
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: sourceURL.path) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentCollections = Dictionary(uniqueKeysWithValues: currentCollectionSnapshots(for: sourceURL).map { ($0.folderName, $0) })
|
|
||||||
let persistedCollections = Dictionary(uniqueKeysWithValues: snapshot.collectionSnapshots.map { ($0.folderName, $0) })
|
|
||||||
|
|
||||||
if currentCollections.count != persistedCollections.count {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for (folderName, persistedCollection) in persistedCollections {
|
|
||||||
guard let currentCollection = currentCollections[folderName],
|
|
||||||
currentCollection.fingerprint == persistedCollection.fingerprint else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleRestoredSourceRefreshes(records: [PersistedSourceRecord]) {
|
private func scheduleRestoredSourceRefreshes(records: [PersistedSourceRecord]) {
|
||||||
let persistedRecordsByID = Dictionary(uniqueKeysWithValues: records.map { ($0.sourceID, $0) })
|
let persistedRecordsByID = Dictionary(uniqueKeysWithValues: records.map { ($0.sourceID, $0) })
|
||||||
|
|
||||||
for source in sources {
|
for source in sources {
|
||||||
guard source.availability == .available else {
|
if let refreshReason = SourceRestoration.startupRefreshReason(
|
||||||
continue
|
for: source,
|
||||||
}
|
persistedRecord: persistedRecordsByID[source.id],
|
||||||
|
currentCollectionSnapshots: currentCollectionSnapshots(for:)
|
||||||
switch source.origin.kind {
|
) {
|
||||||
case .localFolder:
|
queueAutomaticSync(for: source.id, reason: refreshReason)
|
||||||
if let record = persistedRecordsByID[source.id], sourceNeedsRescan(record) || sourceNeedsReconcile(source) {
|
|
||||||
queueAutomaticSync(
|
|
||||||
for: source.id,
|
|
||||||
reason: source.hasCachedContent
|
|
||||||
? "Refreshing cached library..."
|
|
||||||
: "Scanning Minecraft library..."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case .connectedDevice:
|
|
||||||
if source.rawItems.isEmpty || source.lastScanDate == nil {
|
|
||||||
queueAutomaticSync(
|
|
||||||
for: source.id,
|
|
||||||
reason: source.hasCachedContent
|
|
||||||
? "Refreshing cached library..."
|
|
||||||
: "Scanning Minecraft library..."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
221
World Manager for Minecraft/Services/SourceRestoration.swift
Normal file
221
World Manager for Minecraft/Services/SourceRestoration.swift
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
//
|
||||||
|
// SourceRestoration.swift
|
||||||
|
// World Manager for Minecraft
|
||||||
|
//
|
||||||
|
// Created by OpenAI Codex on 2026-05-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum SourceRestoration {
|
||||||
|
static func restoredSource(
|
||||||
|
from record: PersistedSourceRecord,
|
||||||
|
connectedDeviceDisplayName: (ConnectedDevice, DeviceAppContainer) -> String
|
||||||
|
) -> MinecraftSource {
|
||||||
|
var source = MinecraftSource(
|
||||||
|
sourceID: record.sourceID,
|
||||||
|
folderURL: record.folderURL,
|
||||||
|
bookmarkData: record.bookmarkData,
|
||||||
|
origin: record.origin,
|
||||||
|
accessDescriptor: record.accessDescriptor,
|
||||||
|
availability: record.availability
|
||||||
|
)
|
||||||
|
|
||||||
|
if case .connectedDevice(let device, let container) = source.origin {
|
||||||
|
var repairedDevice = device
|
||||||
|
repairedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
|
||||||
|
currentName: device.name,
|
||||||
|
fallbackDeviceName: "",
|
||||||
|
fallbackDisplayName: record.displayName
|
||||||
|
)
|
||||||
|
source.origin = .connectedDevice(device: repairedDevice, container: container)
|
||||||
|
|
||||||
|
let persistedDeviceName = record.displayName.components(separatedBy: " • ").first ?? record.displayName
|
||||||
|
if ConnectedDeviceSourcePolicy.sanitizedDeviceName(persistedDeviceName) == nil {
|
||||||
|
source.displayName = connectedDeviceDisplayName(repairedDevice, container)
|
||||||
|
} else {
|
||||||
|
source.displayName = record.displayName
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source.displayName = record.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRestoredItemState(
|
||||||
|
record.rawItems,
|
||||||
|
lastScanDate: record.lastScanDate,
|
||||||
|
snapshot: record.snapshot,
|
||||||
|
to: &source
|
||||||
|
)
|
||||||
|
source.scanStatus = restoredStatus(for: source)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
static func applyRestoredItemState(
|
||||||
|
_ items: [MinecraftContentItem],
|
||||||
|
lastScanDate: Date?,
|
||||||
|
snapshot: SourceSnapshot?,
|
||||||
|
to source: inout MinecraftSource
|
||||||
|
) {
|
||||||
|
source.rawItems = items
|
||||||
|
source.indexedItemCount = items.count
|
||||||
|
source.indexedDetailCount = items.filter(\.metadataLoaded).count
|
||||||
|
source.previewLoadedCount = items.filter(\.previewLoaded).count
|
||||||
|
source.sizeLoadedCount = items.filter(\.sizeLoaded).count
|
||||||
|
source.lastScanDate = lastScanDate
|
||||||
|
source.snapshot = snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
static func restoreCachedImages(in items: [MinecraftContentItem]) async -> [MinecraftContentItem] {
|
||||||
|
var restoredItems: [MinecraftContentItem] = []
|
||||||
|
restoredItems.reserveCapacity(items.count)
|
||||||
|
|
||||||
|
for var item in items {
|
||||||
|
item.iconURL = await ImageCacheStore.shared.cachedImageURL(for: item.iconURL)
|
||||||
|
item.packReferences = await restoreCachedImages(in: item.packReferences)
|
||||||
|
restoredItems.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoredItems
|
||||||
|
}
|
||||||
|
|
||||||
|
static func restoreCachedImages(in references: [ContentPackReference]) async -> [ContentPackReference] {
|
||||||
|
var restoredReferences: [ContentPackReference] = []
|
||||||
|
restoredReferences.reserveCapacity(references.count)
|
||||||
|
|
||||||
|
for reference in references {
|
||||||
|
let cachedIconURL = await ImageCacheStore.shared.cachedImageURL(for: reference.iconURL)
|
||||||
|
restoredReferences.append(
|
||||||
|
ContentPackReference(
|
||||||
|
name: reference.name,
|
||||||
|
type: reference.type,
|
||||||
|
iconURL: cachedIconURL,
|
||||||
|
uuid: reference.uuid,
|
||||||
|
version: reference.version,
|
||||||
|
source: reference.source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoredReferences
|
||||||
|
}
|
||||||
|
|
||||||
|
static func startupRefreshReason(
|
||||||
|
for source: MinecraftSource,
|
||||||
|
persistedRecord: PersistedSourceRecord?,
|
||||||
|
currentCollectionSnapshots: (URL) -> [CollectionSnapshot]
|
||||||
|
) -> String? {
|
||||||
|
guard source.availability == .available else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch source.origin.kind {
|
||||||
|
case .localFolder:
|
||||||
|
guard let persistedRecord else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsRescan(persistedRecord, currentCollectionSnapshots: currentCollectionSnapshots)
|
||||||
|
|| reconcileIsNeeded(source, currentCollectionSnapshots: currentCollectionSnapshots) {
|
||||||
|
return defaultRefreshReason(for: source)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case .connectedDevice:
|
||||||
|
guard source.rawItems.isEmpty || source.lastScanDate == nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultRefreshReason(for: source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func needsReconcile(
|
||||||
|
_ source: MinecraftSource,
|
||||||
|
currentCollectionSnapshots: (URL) -> [CollectionSnapshot]
|
||||||
|
) -> Bool {
|
||||||
|
reconcileIsNeeded(source, currentCollectionSnapshots: currentCollectionSnapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func restoredStatus(for source: MinecraftSource) -> String {
|
||||||
|
source.indexedItemCount == 0
|
||||||
|
? "No Minecraft items found."
|
||||||
|
: "Loaded \(source.indexedDetailCount) items."
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func defaultRefreshReason(for source: MinecraftSource) -> String {
|
||||||
|
source.hasCachedContent
|
||||||
|
? "Refreshing cached library..."
|
||||||
|
: "Scanning Minecraft library..."
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func needsRescan(
|
||||||
|
_ record: PersistedSourceRecord,
|
||||||
|
currentCollectionSnapshots: (URL) -> [CollectionSnapshot]
|
||||||
|
) -> Bool {
|
||||||
|
guard record.accessDescriptor.refreshStrategy == .eagerFullScan else {
|
||||||
|
return record.rawItems.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let snapshot = record.snapshot else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceURL = record.folderURL
|
||||||
|
guard FileManager.default.fileExists(atPath: sourceURL.path) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsDiffer(
|
||||||
|
currentCollectionSnapshots(sourceURL),
|
||||||
|
persistedCollections: snapshot.collectionSnapshots
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func reconcileIsNeeded(
|
||||||
|
_ source: MinecraftSource,
|
||||||
|
currentCollectionSnapshots: (URL) -> [CollectionSnapshot]
|
||||||
|
) -> Bool {
|
||||||
|
guard source.accessDescriptor.refreshStrategy == .eagerFullScan else {
|
||||||
|
return source.rawItems.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let snapshot = source.snapshot else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceURL = source.folderURL
|
||||||
|
guard FileManager.default.fileExists(atPath: sourceURL.path) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionsDiffer(
|
||||||
|
currentCollectionSnapshots(sourceURL),
|
||||||
|
persistedCollections: snapshot.collectionSnapshots
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func collectionsDiffer(
|
||||||
|
_ currentCollections: [CollectionSnapshot],
|
||||||
|
persistedCollections: [CollectionSnapshot]
|
||||||
|
) -> Bool {
|
||||||
|
let currentCollectionsByName = Dictionary(
|
||||||
|
uniqueKeysWithValues: currentCollections.map { ($0.folderName, $0) }
|
||||||
|
)
|
||||||
|
let persistedCollectionsByName = Dictionary(
|
||||||
|
uniqueKeysWithValues: persistedCollections.map { ($0.folderName, $0) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if currentCollectionsByName.count != persistedCollectionsByName.count {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (folderName, persistedCollection) in persistedCollectionsByName {
|
||||||
|
guard let currentCollection = currentCollectionsByName[folderName],
|
||||||
|
currentCollection.fingerprint == persistedCollection.fingerprint else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user