Separate source presentation from source models
This commit is contained in:
parent
dce5af8a89
commit
2d84823826
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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] {
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -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..."
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user