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")
|
.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
|
import Foundation
|
||||||
|
|
||||||
enum SourceScanPhase {
|
|
||||||
case discovering
|
|
||||||
case metadata
|
|
||||||
case previews
|
|
||||||
case sizing
|
|
||||||
case completed
|
|
||||||
case idle
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MinecraftSource: Identifiable, Hashable, Sendable {
|
struct MinecraftSource: Identifiable, Hashable, Sendable {
|
||||||
let id: URL
|
let id: URL
|
||||||
let folderURL: URL
|
let folderURL: URL
|
||||||
@ -102,139 +93,10 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
|
|||||||
availability != .available && hasCachedContent
|
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] {
|
var items: [MinecraftContentItem] {
|
||||||
displayItems
|
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? {
|
func rawItem(withID itemID: URL) -> MinecraftContentItem? {
|
||||||
rawItems.first(where: { $0.id == itemID })
|
rawItems.first(where: { $0.id == itemID })
|
||||||
}
|
}
|
||||||
@ -256,7 +118,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
|
|||||||
.filter { $0.logicalPackID == logicalPackID }
|
.filter { $0.logicalPackID == logicalPackID }
|
||||||
.compactMap { rawItem(withID: $0.worldItemID) }
|
.compactMap { rawItem(withID: $0.worldItemID) }
|
||||||
.uniqued(by: \.id)
|
.uniqued(by: \.id)
|
||||||
.sorted(by: WorldScanner.sortItems)
|
.sorted(by: MinecraftContentItem.displaySort)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvedPackReferences(for worldItemID: URL, type: MinecraftContentType) -> [ContentPackReference] {
|
func resolvedPackReferences(for worldItemID: URL, type: MinecraftContentType) -> [ContentPackReference] {
|
||||||
|
|||||||
@ -186,16 +186,7 @@ enum WorldScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonisolated static func sortItems(_ lhs: MinecraftContentItem, _ rhs: MinecraftContentItem) -> Bool {
|
nonisolated static func sortItems(_ lhs: MinecraftContentItem, _ rhs: MinecraftContentItem) -> Bool {
|
||||||
if lhs.contentType != rhs.contentType {
|
MinecraftContentItem.displaySort(lhs, rhs)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated private static func contentType(forCollectionFolderName folderName: String) -> MinecraftContentType? {
|
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 {
|
} else {
|
||||||
source.scanError = nil
|
source.scanError = nil
|
||||||
source.scanProgress = nil
|
source.scanProgress = nil
|
||||||
source.scanStatus = source.availabilityDisplayText
|
source.scanStatus = SourcePresentation.availabilityDisplayText(for: source)
|
||||||
source.scanDiagnostic = source.cachedAvailabilityDetailText
|
source.scanDiagnostic = SourcePresentation.cachedAvailabilityDetailText(for: source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -78,7 +78,7 @@ struct SourceDetailView: View {
|
|||||||
|
|
||||||
private var statusTitle: String {
|
private var statusTitle: String {
|
||||||
if !source.isScanning, source.availability != .available {
|
if !source.isScanning, source.availability != .available {
|
||||||
return source.availabilityDisplayText
|
return SourcePresentation.availabilityDisplayText(for: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let scanError = source.scanError, !scanError.isEmpty {
|
if let scanError = source.scanError, !scanError.isEmpty {
|
||||||
@ -86,7 +86,7 @@ struct SourceDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if source.isScanning {
|
if source.isScanning {
|
||||||
return source.liveScanStatusTitle
|
return SourcePresentation.liveScanStatusTitle(for: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !source.scanStatus.isEmpty {
|
if !source.scanStatus.isEmpty {
|
||||||
@ -102,7 +102,7 @@ struct SourceDetailView: View {
|
|||||||
return scanDiagnostic
|
return scanDiagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
if let cachedAvailabilityDetailText = source.cachedAvailabilityDetailText {
|
if let cachedAvailabilityDetailText = SourcePresentation.cachedAvailabilityDetailText(for: source) {
|
||||||
return cachedAvailabilityDetailText
|
return cachedAvailabilityDetailText
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ struct SourceDetailView: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack(alignment: .center, spacing: 10) {
|
HStack(alignment: .center, spacing: 10) {
|
||||||
if source.showsIndeterminateScanActivityIndicator {
|
if SourcePresentation.showsIndeterminateScanActivityIndicator(for: source) {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
} else if !source.isScanning {
|
} else if !source.isScanning {
|
||||||
@ -292,7 +292,8 @@ struct SourceDetailView: View {
|
|||||||
&& (source.scanProgress ?? 0) < 0.65
|
&& (source.scanProgress ?? 0) < 0.65
|
||||||
|
|
||||||
let status: StageStatus
|
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
|
status = .completed
|
||||||
} else if hasPreviewWorkStarted {
|
} else if hasPreviewWorkStarted {
|
||||||
status = .inProgress
|
status = .inProgress
|
||||||
@ -331,7 +332,7 @@ struct SourceDetailView: View {
|
|||||||
let previewsAreFullyLoaded = total > 0 && source.previewLoadedCount >= total
|
let previewsAreFullyLoaded = total > 0 && source.previewLoadedCount >= total
|
||||||
|
|
||||||
let status: StageStatus
|
let status: StageStatus
|
||||||
switch source.scanPhase {
|
switch SourcePresentation.scanPhase(for: source) {
|
||||||
case .sizing:
|
case .sizing:
|
||||||
status = .inProgress
|
status = .inProgress
|
||||||
case .completed:
|
case .completed:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user