Extract source restoration helpers
This commit is contained in:
parent
3a30b94369
commit
78ab078674
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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