From 78ab07867493f4c5365126e202e2fe0ca6350f39 Mon Sep 17 00:00:00 2001 From: John Burwell Date: Thu, 28 May 2026 22:44:21 -0500 Subject: [PATCH] Extract source restoration helpers --- .../Services/SourceLibrary.swift | 186 ++------------- .../Services/SourceRestoration.swift | 221 ++++++++++++++++++ 2 files changed, 240 insertions(+), 167 deletions(-) create mode 100644 World Manager for Minecraft/Services/SourceRestoration.swift diff --git a/World Manager for Minecraft/Services/SourceLibrary.swift b/World Manager for Minecraft/Services/SourceLibrary.swift index d8cb14b..74eb7b6 100644 --- a/World Manager for Minecraft/Services/SourceLibrary.swift +++ b/World Manager for Minecraft/Services/SourceLibrary.swift @@ -892,7 +892,10 @@ final class SourceLibrary: ObservableObject { continue } - if sourceNeedsReconcile(refreshedSource) { + if SourceRestoration.needsReconcile( + refreshedSource, + currentCollectionSnapshots: currentCollectionSnapshots(for:) + ) { queueAutomaticSync( for: sourceID, reason: refreshedSource.hasCachedContent @@ -1145,44 +1148,9 @@ final class SourceLibrary: ObservableObject { } for record in records { - 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 = connectedDeviceSourceFactory.displayName( - for: repairedDevice, - container: container - ) - } else { - source.displayName = record.displayName - } - } else { - source.displayName = record.displayName + let source = SourceRestoration.restoredSource(from: record) { [connectedDeviceSourceFactory] device, container in + connectedDeviceSourceFactory.displayName(for: device, container: container) } - 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) rebuildNormalizedIndex(for: source.id) @@ -1198,15 +1166,14 @@ final class SourceLibrary: ObservableObject { await Task.yield() for record in records { - let restoredItems = await restoreCachedImages(in: record.rawItems) + let restoredItems = await SourceRestoration.restoreCachedImages(in: record.rawItems) updateSource(record.sourceID) { source in - source.rawItems = restoredItems - source.indexedItemCount = restoredItems.count - source.indexedDetailCount = restoredItems.filter(\.metadataLoaded).count - source.previewLoadedCount = restoredItems.filter(\.previewLoaded).count - source.sizeLoadedCount = restoredItems.filter(\.sizeLoaded).count - source.lastScanDate = record.lastScanDate - source.snapshot = record.snapshot + SourceRestoration.applyRestoredItemState( + restoredItems, + lastScanDate: record.lastScanDate, + snapshot: record.snapshot, + to: &source + ) } rebuildNormalizedIndex(for: record.sourceID) } @@ -1216,131 +1183,16 @@ final class SourceLibrary: ObservableObject { 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]) { let persistedRecordsByID = Dictionary(uniqueKeysWithValues: records.map { ($0.sourceID, $0) }) for source in sources { - guard source.availability == .available else { - continue - } - - switch source.origin.kind { - case .localFolder: - 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..." - ) - } + if let refreshReason = SourceRestoration.startupRefreshReason( + for: source, + persistedRecord: persistedRecordsByID[source.id], + currentCollectionSnapshots: currentCollectionSnapshots(for:) + ) { + queueAutomaticSync(for: source.id, reason: refreshReason) } } } diff --git a/World Manager for Minecraft/Services/SourceRestoration.swift b/World Manager for Minecraft/Services/SourceRestoration.swift new file mode 100644 index 0000000..89414ff --- /dev/null +++ b/World Manager for Minecraft/Services/SourceRestoration.swift @@ -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 + } +}