diff --git a/World Manager for Minecraft/Models/Content/MinecraftContentItem.swift b/World Manager for Minecraft/Models/Content/MinecraftContentItem.swift index 02fdc25..79c24e3 100644 --- a/World Manager for Minecraft/Models/Content/MinecraftContentItem.swift +++ b/World Manager for Minecraft/Models/Content/MinecraftContentItem.swift @@ -211,4 +211,16 @@ nonisolated struct MinecraftContentItem: Identifiable, Hashable, Sendable, Codab .joined(separator: "\n") } + nonisolated static func displaySort(_ lhs: MinecraftContentItem, _ rhs: MinecraftContentItem) -> Bool { + if lhs.contentType != rhs.contentType { + return lhs.contentType.rawValue.localizedStandardCompare(rhs.contentType.rawValue) == .orderedAscending + } + + let displayNameOrder = lhs.displayName.localizedStandardCompare(rhs.displayName) + if displayNameOrder != .orderedSame { + return displayNameOrder == .orderedAscending + } + + return lhs.folderName.localizedStandardCompare(rhs.folderName) == .orderedAscending + } } diff --git a/World Manager for Minecraft/Models/Sources/MinecraftSource.swift b/World Manager for Minecraft/Models/Sources/MinecraftSource.swift index c1c1366..0c9997c 100644 --- a/World Manager for Minecraft/Models/Sources/MinecraftSource.swift +++ b/World Manager for Minecraft/Models/Sources/MinecraftSource.swift @@ -7,15 +7,6 @@ import Foundation -enum SourceScanPhase { - case discovering - case metadata - case previews - case sizing - case completed - case idle -} - struct MinecraftSource: Identifiable, Hashable, Sendable { let id: URL let folderURL: URL @@ -102,139 +93,10 @@ struct MinecraftSource: Identifiable, Hashable, Sendable { availability != .available && hasCachedContent } - var availabilityDisplayText: String { - switch availability { - case .available: - return "Available" - case .unknown: - return "Checking availability" - case .disconnected: - return origin.kind == .connectedDevice ? "Device offline" : "Folder offline" - case .limited: - return origin.kind == .connectedDevice ? "Device access limited" : "Limited access" - case .unavailable: - return origin.kind == .connectedDevice ? "Device unavailable" : "Folder unavailable" - } - } - - var cachedAvailabilityDetailText: String? { - guard isOfflineCached else { - return nil - } - - switch availability { - case .disconnected: - return origin.kind == .connectedDevice - ? "Showing cached results until this device reconnects." - : "Showing cached results until this folder becomes reachable again." - case .limited: - return origin.kind == .connectedDevice - ? "Showing cached results until the device is unlocked and trusted." - : "Showing cached results until full access is restored." - case .unavailable, .unknown: - return "Showing cached results while the source is unavailable." - case .available: - return nil - } - } - var items: [MinecraftContentItem] { displayItems } - var scanPhase: SourceScanPhase { - guard isScanning else { - if scanStatus.hasPrefix("Loaded ") || scanStatus == "No Minecraft items found." { - return .completed - } - return .idle - } - - if sizeLoadedCount > 0 { - return .sizing - } - - if previewLoadedCount > 0 { - return .previews - } - - if let scanProgress { - if scanProgress >= 0.75 { - return .sizing - } - if scanProgress >= 0.65 { - return .previews - } - if scanProgress >= 0.1 { - return .metadata - } - } - - if scanStatus.contains("Calculating sizes") { - return .sizing - } - if scanStatus.contains("Loading previews") { - return .previews - } - if scanStatus.contains("metadata") { - return .metadata - } - - return .discovering - } - - var liveScanStatusTitle: String { - guard isScanning else { - return scanStatus - } - - if indexedItemCount == 0 { - return "Scanning Minecraft library..." - } - - let discoveryIsComplete = (scanProgress ?? 0) >= 0.65 - - if !discoveryIsComplete { - return "Discovering items..." - } - - if indexedItemCount > 0, previewLoadedCount >= indexedItemCount, sizeLoadedCount == 0 { - return "Preparing size calculations..." - } - - if scanStatus == "Preparing previews..." || scanStatus == "Preparing size calculations..." { - return scanStatus - } - - switch scanPhase { - case .discovering, .metadata, .previews: - return "Loading previews for \(previewLoadedCount) of \(indexedItemCount) items..." - case .sizing: - return "Calculating sizes for \(sizeLoadedCount) of \(indexedItemCount) items..." - case .completed: - return indexedItemCount == 0 ? "No Minecraft items found." : "Loaded \(indexedDetailCount) items." - case .idle: - return scanStatus - } - } - - var showsIndeterminateScanActivityIndicator: Bool { - guard isScanning else { - return false - } - - let discoveryIsComplete = (scanProgress ?? 0) >= 0.65 - if !discoveryIsComplete { - return true - } - - if indexedItemCount > 0, previewLoadedCount >= indexedItemCount, sizeLoadedCount == 0 { - return true - } - - return scanStatus == "Preparing previews..." || scanStatus == "Preparing size calculations..." - } - func rawItem(withID itemID: URL) -> MinecraftContentItem? { rawItems.first(where: { $0.id == itemID }) } @@ -256,7 +118,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable { .filter { $0.logicalPackID == logicalPackID } .compactMap { rawItem(withID: $0.worldItemID) } .uniqued(by: \.id) - .sorted(by: WorldScanner.sortItems) + .sorted(by: MinecraftContentItem.displaySort) } func resolvedPackReferences(for worldItemID: URL, type: MinecraftContentType) -> [ContentPackReference] { diff --git a/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift b/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift index 0de472a..24ae1d3 100644 --- a/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift +++ b/World Manager for Minecraft/Services/AppSupport/Scanning/WorldScanner.swift @@ -186,16 +186,7 @@ enum WorldScanner { } nonisolated static func sortItems(_ lhs: MinecraftContentItem, _ rhs: MinecraftContentItem) -> Bool { - if lhs.contentType != rhs.contentType { - return lhs.contentType.rawValue.localizedStandardCompare(rhs.contentType.rawValue) == .orderedAscending - } - - let displayNameOrder = lhs.displayName.localizedStandardCompare(rhs.displayName) - if displayNameOrder != .orderedSame { - return displayNameOrder == .orderedAscending - } - - return lhs.folderName.localizedStandardCompare(rhs.folderName) == .orderedAscending + MinecraftContentItem.displaySort(lhs, rhs) } nonisolated private static func contentType(forCollectionFolderName folderName: String) -> MinecraftContentType? { diff --git a/World Manager for Minecraft/Services/Sources/Core/SourcePresentation.swift b/World Manager for Minecraft/Services/Sources/Core/SourcePresentation.swift new file mode 100644 index 0000000..b8bc47b --- /dev/null +++ b/World Manager for Minecraft/Services/Sources/Core/SourcePresentation.swift @@ -0,0 +1,146 @@ +import Foundation + +enum SourceScanPhase { + case discovering + case metadata + case previews + case sizing + case completed + case idle +} + +enum SourcePresentation { + nonisolated static func availabilityDisplayText(for source: MinecraftSource) -> String { + switch source.availability { + case .available: + return "Available" + case .unknown: + return "Checking availability" + case .disconnected: + return source.origin.kind == .connectedDevice ? "Device offline" : "Folder offline" + case .limited: + return source.origin.kind == .connectedDevice ? "Device access limited" : "Limited access" + case .unavailable: + return source.origin.kind == .connectedDevice ? "Device unavailable" : "Folder unavailable" + } + } + + nonisolated static func cachedAvailabilityDetailText(for source: MinecraftSource) -> String? { + let hasCachedContent = !source.displayItems.isEmpty || !source.rawItems.isEmpty || source.snapshot != nil + guard source.availability != .available && hasCachedContent else { + return nil + } + + switch source.availability { + case .disconnected: + return source.origin.kind == .connectedDevice + ? "Showing cached results until this device reconnects." + : "Showing cached results until this folder becomes reachable again." + case .limited: + return source.origin.kind == .connectedDevice + ? "Showing cached results until the device is unlocked and trusted." + : "Showing cached results until full access is restored." + case .unavailable, .unknown: + return "Showing cached results while the source is unavailable." + case .available: + return nil + } + } + + nonisolated static func scanPhase(for source: MinecraftSource) -> SourceScanPhase { + guard source.isScanning else { + if source.scanStatus.hasPrefix("Loaded ") || source.scanStatus == "No Minecraft items found." { + return .completed + } + return .idle + } + + if source.sizeLoadedCount > 0 { + return .sizing + } + + if source.previewLoadedCount > 0 { + return .previews + } + + if let scanProgress = source.scanProgress { + if scanProgress >= 0.75 { + return .sizing + } + if scanProgress >= 0.65 { + return .previews + } + if scanProgress >= 0.1 { + return .metadata + } + } + + if source.scanStatus.contains("Calculating sizes") { + return .sizing + } + if source.scanStatus.contains("Loading previews") { + return .previews + } + if source.scanStatus.contains("metadata") { + return .metadata + } + + return .discovering + } + + nonisolated static func liveScanStatusTitle(for source: MinecraftSource) -> String { + guard source.isScanning else { + return source.scanStatus + } + + if source.indexedItemCount == 0 { + return "Scanning Minecraft library..." + } + + let discoveryIsComplete = (source.scanProgress ?? 0) >= 0.65 + + if !discoveryIsComplete { + return "Discovering items..." + } + + if source.indexedItemCount > 0, + source.previewLoadedCount >= source.indexedItemCount, + source.sizeLoadedCount == 0 { + return "Preparing size calculations..." + } + + if source.scanStatus == "Preparing previews..." || source.scanStatus == "Preparing size calculations..." { + return source.scanStatus + } + + switch scanPhase(for: source) { + case .discovering, .metadata, .previews: + return "Loading previews for \(source.previewLoadedCount) of \(source.indexedItemCount) items..." + case .sizing: + return "Calculating sizes for \(source.sizeLoadedCount) of \(source.indexedItemCount) items..." + case .completed: + return source.indexedItemCount == 0 ? "No Minecraft items found." : "Loaded \(source.indexedDetailCount) items." + case .idle: + return source.scanStatus + } + } + + nonisolated static func showsIndeterminateScanActivityIndicator(for source: MinecraftSource) -> Bool { + guard source.isScanning else { + return false + } + + let discoveryIsComplete = (source.scanProgress ?? 0) >= 0.65 + if !discoveryIsComplete { + return true + } + + if source.indexedItemCount > 0, + source.previewLoadedCount >= source.indexedItemCount, + source.sizeLoadedCount == 0 { + return true + } + + return source.scanStatus == "Preparing previews..." || source.scanStatus == "Preparing size calculations..." + } +} diff --git a/World Manager for Minecraft/Services/Sources/Scanning/SourceSyncRuntime.swift b/World Manager for Minecraft/Services/Sources/Scanning/SourceSyncRuntime.swift index 3aafe6e..45490fb 100644 --- a/World Manager for Minecraft/Services/Sources/Scanning/SourceSyncRuntime.swift +++ b/World Manager for Minecraft/Services/Sources/Scanning/SourceSyncRuntime.swift @@ -103,8 +103,8 @@ enum SourceSyncRuntime { } else { source.scanError = nil source.scanProgress = nil - source.scanStatus = source.availabilityDisplayText - source.scanDiagnostic = source.cachedAvailabilityDetailText + source.scanStatus = SourcePresentation.availabilityDisplayText(for: source) + source.scanDiagnostic = SourcePresentation.cachedAvailabilityDetailText(for: source) } } diff --git a/World Manager for Minecraft/UI/Detail/SourceDetailView.swift b/World Manager for Minecraft/UI/Detail/SourceDetailView.swift index f00ad5c..34ba936 100644 --- a/World Manager for Minecraft/UI/Detail/SourceDetailView.swift +++ b/World Manager for Minecraft/UI/Detail/SourceDetailView.swift @@ -78,7 +78,7 @@ struct SourceDetailView: View { private var statusTitle: String { if !source.isScanning, source.availability != .available { - return source.availabilityDisplayText + return SourcePresentation.availabilityDisplayText(for: source) } if let scanError = source.scanError, !scanError.isEmpty { @@ -86,7 +86,7 @@ struct SourceDetailView: View { } if source.isScanning { - return source.liveScanStatusTitle + return SourcePresentation.liveScanStatusTitle(for: source) } if !source.scanStatus.isEmpty { @@ -102,7 +102,7 @@ struct SourceDetailView: View { return scanDiagnostic } - if let cachedAvailabilityDetailText = source.cachedAvailabilityDetailText { + if let cachedAvailabilityDetailText = SourcePresentation.cachedAvailabilityDetailText(for: source) { return cachedAvailabilityDetailText } @@ -136,7 +136,7 @@ struct SourceDetailView: View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .center, spacing: 10) { - if source.showsIndeterminateScanActivityIndicator { + if SourcePresentation.showsIndeterminateScanActivityIndicator(for: source) { ProgressView() .controlSize(.small) } else if !source.isScanning { @@ -292,7 +292,8 @@ struct SourceDetailView: View { && (source.scanProgress ?? 0) < 0.65 let status: StageStatus - if previewsAreFullyLoaded || source.scanPhase == .sizing || source.scanPhase == .completed { + let scanPhase = SourcePresentation.scanPhase(for: source) + if previewsAreFullyLoaded || scanPhase == .sizing || scanPhase == .completed { status = .completed } else if hasPreviewWorkStarted { status = .inProgress @@ -331,7 +332,7 @@ struct SourceDetailView: View { let previewsAreFullyLoaded = total > 0 && source.previewLoadedCount >= total let status: StageStatus - switch source.scanPhase { + switch SourcePresentation.scanPhase(for: source) { case .sizing: status = .inProgress case .completed: