Remove abandoned sidebar footer
This commit is contained in:
parent
b7ea9ce89d
commit
4a3b336643
@ -45,7 +45,6 @@ struct ContentView: View {
|
|||||||
sources: library.sidebarSources,
|
sources: library.sidebarSources,
|
||||||
connectedDevices: library.connectedDevices,
|
connectedDevices: library.connectedDevices,
|
||||||
selection: $selectedSidebarSelection,
|
selection: $selectedSidebarSelection,
|
||||||
footerState: library.sidebarFooterState,
|
|
||||||
addSourceAction: pickFolder,
|
addSourceAction: pickFolder,
|
||||||
addDeviceSourceAction: { isShowingDeviceSourceSheet = true },
|
addDeviceSourceAction: { isShowingDeviceSourceSheet = true },
|
||||||
addConnectedDeviceAction: addConnectedDeviceSource(from:),
|
addConnectedDeviceAction: addConnectedDeviceSource(from:),
|
||||||
@ -57,7 +56,6 @@ struct ContentView: View {
|
|||||||
removeSourceAction: { source in
|
removeSourceAction: { source in
|
||||||
removeSource(source.id)
|
removeSource(source.id)
|
||||||
},
|
},
|
||||||
revealFooterURLAction: revealURLInFinder(_:),
|
|
||||||
filters: sidebarFilters(for:)
|
filters: sidebarFilters(for:)
|
||||||
)
|
)
|
||||||
.navigationSplitViewColumnWidth(min: 280, ideal: 320, max: 380)
|
.navigationSplitViewColumnWidth(min: 280, ideal: 320, max: 380)
|
||||||
@ -681,7 +679,6 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPerformingItemAction = true
|
isPerformingItemAction = true
|
||||||
library.setItemActionInProgress("Creating \(item.contentType.archiveExtension) file...")
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@ -695,16 +692,11 @@ struct ContentView: View {
|
|||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPerformingItemAction = false
|
isPerformingItemAction = false
|
||||||
library.setItemActionSuccess(
|
_ = finalURL
|
||||||
title: "Created \(finalURL.lastPathComponent)",
|
|
||||||
subtitle: "Ready to move to another device",
|
|
||||||
revealURL: finalURL
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPerformingItemAction = false
|
isPerformingItemAction = false
|
||||||
library.setItemActionFailure(error.localizedDescription)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -717,7 +709,6 @@ struct ContentView: View {
|
|||||||
let source = currentSource
|
let source = currentSource
|
||||||
|
|
||||||
isPerformingItemAction = true
|
isPerformingItemAction = true
|
||||||
library.setItemActionInProgress("Preparing \(item.contentType.archiveExtension) file...")
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@ -733,16 +724,9 @@ struct ContentView: View {
|
|||||||
|
|
||||||
let presentationView = anchorView ?? NSApp.keyWindow?.contentView
|
let presentationView = anchorView ?? NSApp.keyWindow?.contentView
|
||||||
guard let presentationView else {
|
guard let presentationView else {
|
||||||
library.setItemActionFailure("Could not present the share menu.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
library.setItemActionSuccess(
|
|
||||||
title: "Share ready",
|
|
||||||
subtitle: shareURL.lastPathComponent,
|
|
||||||
revealURL: shareURL
|
|
||||||
)
|
|
||||||
|
|
||||||
let picker = NSSharingServicePicker(items: [shareURL])
|
let picker = NSSharingServicePicker(items: [shareURL])
|
||||||
let targetRect = anchorView?.bounds ?? presentationView.bounds.insetBy(
|
let targetRect = anchorView?.bounds ?? presentationView.bounds.insetBy(
|
||||||
dx: presentationView.bounds.width / 2,
|
dx: presentationView.bounds.width / 2,
|
||||||
@ -753,7 +737,6 @@ struct ContentView: View {
|
|||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPerformingItemAction = false
|
isPerformingItemAction = false
|
||||||
library.setItemActionFailure(error.localizedDescription)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -774,7 +757,6 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPerformingItemAction = true
|
isPerformingItemAction = true
|
||||||
library.setItemActionInProgress("Preparing item for Finder...")
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@ -783,25 +765,15 @@ struct ContentView: View {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPerformingItemAction = false
|
isPerformingItemAction = false
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([revealURL])
|
NSWorkspace.shared.activateFileViewerSelecting([revealURL])
|
||||||
library.setItemActionSuccess(
|
|
||||||
title: "Prepared for Finder",
|
|
||||||
subtitle: item.displayName,
|
|
||||||
revealURL: revealURL
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPerformingItemAction = false
|
isPerformingItemAction = false
|
||||||
library.setItemActionFailure(error.localizedDescription)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func revealURLInFinder(_ url: URL) {
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
||||||
}
|
|
||||||
|
|
||||||
private func archiveType(for item: MinecraftContentItem) -> UTType {
|
private func archiveType(for item: MinecraftContentItem) -> UTType {
|
||||||
UTType(filenameExtension: item.contentType.archiveExtension) ?? .data
|
UTType(filenameExtension: item.contentType.archiveExtension) ?? .data
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,14 +227,6 @@ enum PreviewFixtures {
|
|||||||
|
|
||||||
static let allSources = [primarySource, secondarySource]
|
static let allSources = [primarySource, secondarySource]
|
||||||
|
|
||||||
static let sidebarFooter = SidebarFooterState(
|
|
||||||
style: .success,
|
|
||||||
title: "Export Complete",
|
|
||||||
subtitle: "Saved a preview copy of \(featuredWorld.displayName)",
|
|
||||||
detail: nil,
|
|
||||||
revealURL: featuredWorld.folderURL
|
|
||||||
)
|
|
||||||
|
|
||||||
static let directoryEntries = [
|
static let directoryEntries = [
|
||||||
DirectoryPreviewEntry(name: "db", isDirectory: true),
|
DirectoryPreviewEntry(name: "db", isDirectory: true),
|
||||||
DirectoryPreviewEntry(name: "level.dat", isDirectory: false),
|
DirectoryPreviewEntry(name: "level.dat", isDirectory: false),
|
||||||
@ -242,48 +234,9 @@ enum PreviewFixtures {
|
|||||||
DirectoryPreviewEntry(name: "world_icon.jpeg", isDirectory: false),
|
DirectoryPreviewEntry(name: "world_icon.jpeg", isDirectory: false),
|
||||||
DirectoryPreviewEntry(name: "resource_packs", isDirectory: true)
|
DirectoryPreviewEntry(name: "resource_packs", isDirectory: true)
|
||||||
]
|
]
|
||||||
|
|
||||||
nonisolated static func sidebarFilters(for source: MinecraftSource) -> [SidebarFilter] {
|
|
||||||
let allFilter = SidebarFilter(
|
|
||||||
title: "All Content",
|
|
||||||
iconName: "square.stack.3d.up",
|
|
||||||
count: source.items.count,
|
|
||||||
selection: .allContent(sourceID: source.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
let groupedFilters = MinecraftContentType.allCases.compactMap { contentType -> SidebarFilter? in
|
|
||||||
let count = source.items.filter { $0.contentType == contentType }.count
|
|
||||||
guard count > 0 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return SidebarFilter(
|
|
||||||
title: contentType.rawValue,
|
|
||||||
iconName: iconName(for: contentType),
|
|
||||||
count: count,
|
|
||||||
selection: .contentType(sourceID: source.id, contentType: contentType)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [allFilter] + groupedFilters
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated static func iconName(for contentType: MinecraftContentType) -> String {
|
|
||||||
switch contentType {
|
|
||||||
case .world:
|
|
||||||
return "globe.europe.africa"
|
|
||||||
case .behaviorPack:
|
|
||||||
return "shippingbox"
|
|
||||||
case .resourcePack:
|
|
||||||
return "paintpalette"
|
|
||||||
case .skinPack:
|
|
||||||
return "person.crop.square"
|
|
||||||
case .worldTemplate:
|
|
||||||
return "doc.on.doc"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct SidebarColumnPreviewContainer: View {
|
struct SidebarColumnPreviewContainer: View {
|
||||||
@State private var selection: SidebarSelection? = .allContent(sourceID: PreviewFixtures.primarySource.id)
|
@State private var selection: SidebarSelection? = .allContent(sourceID: PreviewFixtures.primarySource.id)
|
||||||
|
|
||||||
@ -293,14 +246,49 @@ struct SidebarColumnPreviewContainer: View {
|
|||||||
sources: PreviewFixtures.allSources,
|
sources: PreviewFixtures.allSources,
|
||||||
connectedDevices: [],
|
connectedDevices: [],
|
||||||
selection: $selection,
|
selection: $selection,
|
||||||
footerState: PreviewFixtures.sidebarFooter,
|
|
||||||
addSourceAction: {},
|
addSourceAction: {},
|
||||||
addDeviceSourceAction: {},
|
addDeviceSourceAction: {},
|
||||||
addConnectedDeviceAction: { _ in },
|
addConnectedDeviceAction: { _ in },
|
||||||
rescanSourceAction: { _ in },
|
rescanSourceAction: { _ in },
|
||||||
removeSourceAction: { _ in },
|
removeSourceAction: { _ in },
|
||||||
revealFooterURLAction: { _ in },
|
filters: { source in
|
||||||
filters: PreviewFixtures.sidebarFilters(for:)
|
let allFilter = SidebarFilter(
|
||||||
|
title: "All Content",
|
||||||
|
iconName: "square.stack.3d.up",
|
||||||
|
count: source.displayItems.count,
|
||||||
|
selection: .allContent(sourceID: source.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
let groupedFilters = MinecraftContentType.allCases.compactMap { contentType -> SidebarFilter? in
|
||||||
|
let count = source.displayItems.filter { $0.contentType == contentType }.count
|
||||||
|
guard count > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconName: String
|
||||||
|
switch contentType {
|
||||||
|
case .world:
|
||||||
|
iconName = "globe.europe.africa"
|
||||||
|
case .behaviorPack:
|
||||||
|
iconName = "shippingbox"
|
||||||
|
case .resourcePack:
|
||||||
|
iconName = "paintpalette"
|
||||||
|
case .skinPack:
|
||||||
|
iconName = "person.crop.square"
|
||||||
|
case .worldTemplate:
|
||||||
|
iconName = "doc.on.doc"
|
||||||
|
}
|
||||||
|
|
||||||
|
return SidebarFilter(
|
||||||
|
title: contentType.rawValue,
|
||||||
|
iconName: iconName,
|
||||||
|
count: count,
|
||||||
|
selection: .contentType(sourceID: source.id, contentType: contentType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [allFilter] + groupedFilters
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,21 +9,6 @@ import Combine
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
struct SidebarFooterState {
|
|
||||||
enum Style {
|
|
||||||
case idle
|
|
||||||
case inProgress
|
|
||||||
case failure
|
|
||||||
case success
|
|
||||||
}
|
|
||||||
|
|
||||||
let style: Style
|
|
||||||
let title: String
|
|
||||||
let subtitle: String?
|
|
||||||
let detail: String?
|
|
||||||
let revealURL: URL?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
|
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
|
||||||
let device: ConnectedDevice
|
let device: ConnectedDevice
|
||||||
let containers: [DeviceAppContainer]
|
let containers: [DeviceAppContainer]
|
||||||
@ -58,7 +43,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
private static let localSourceRefreshInterval: TimeInterval = 4.0
|
private static let localSourceRefreshInterval: TimeInterval = 4.0
|
||||||
private static let connectedDeviceRefreshInterval: TimeInterval = 2.0
|
private static let connectedDeviceRefreshInterval: TimeInterval = 2.0
|
||||||
private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0
|
private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0
|
||||||
private static let footerRefreshDebounce: TimeInterval = 0.15
|
|
||||||
private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0
|
private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0
|
||||||
private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0
|
private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0
|
||||||
private static let usbConnectedDeviceDiscoveryCacheTTL: TimeInterval = 60.0
|
private static let usbConnectedDeviceDiscoveryCacheTTL: TimeInterval = 60.0
|
||||||
@ -70,21 +54,12 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
@Published var sources: [MinecraftSource] = []
|
@Published var sources: [MinecraftSource] = []
|
||||||
@Published private(set) var connectedDevices: [ConnectedDeviceSidebarEntry] = []
|
@Published private(set) var connectedDevices: [ConnectedDeviceSidebarEntry] = []
|
||||||
@Published private(set) var sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .idle,
|
|
||||||
title: "",
|
|
||||||
subtitle: nil,
|
|
||||||
detail: nil,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
@Published private(set) var isRestoringPersistedSources = true
|
@Published private(set) var isRestoringPersistedSources = true
|
||||||
|
|
||||||
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
||||||
private var automaticSyncTasks: [URL: Task<Void, Never>] = [:]
|
private var automaticSyncTasks: [URL: Task<Void, Never>] = [:]
|
||||||
private var connectedDeviceRefreshTask: Task<Void, Never>?
|
private var connectedDeviceRefreshTask: Task<Void, Never>?
|
||||||
private var localSourceRefreshTask: Task<Void, Never>?
|
private var localSourceRefreshTask: Task<Void, Never>?
|
||||||
private var footerResetTask: Task<Void, Never>?
|
|
||||||
private var footerRefreshTask: Task<Void, Never>?
|
|
||||||
private let persistenceStore: SourcePersistenceStore
|
private let persistenceStore: SourcePersistenceStore
|
||||||
private let sourceAccessMethod: SourceAccessMethod
|
private let sourceAccessMethod: SourceAccessMethod
|
||||||
private let connectedDeviceAccessMethod: ConnectedDeviceSourceAccessMethod?
|
private let connectedDeviceAccessMethod: ConnectedDeviceSourceAccessMethod?
|
||||||
@ -123,8 +98,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
deinit {
|
deinit {
|
||||||
connectedDeviceRefreshTask?.cancel()
|
connectedDeviceRefreshTask?.cancel()
|
||||||
localSourceRefreshTask?.cancel()
|
localSourceRefreshTask?.cancel()
|
||||||
footerResetTask?.cancel()
|
|
||||||
footerRefreshTask?.cancel()
|
|
||||||
automaticSyncTasks.values.forEach { $0.cancel() }
|
automaticSyncTasks.values.forEach { $0.cancel() }
|
||||||
scanTasks.values.forEach { $0.cancel() }
|
scanTasks.values.forEach { $0.cancel() }
|
||||||
}
|
}
|
||||||
@ -147,10 +120,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
connectedDeviceRefreshTask = nil
|
connectedDeviceRefreshTask = nil
|
||||||
localSourceRefreshTask?.cancel()
|
localSourceRefreshTask?.cancel()
|
||||||
localSourceRefreshTask = nil
|
localSourceRefreshTask = nil
|
||||||
footerResetTask?.cancel()
|
|
||||||
footerResetTask = nil
|
|
||||||
footerRefreshTask?.cancel()
|
|
||||||
footerRefreshTask = nil
|
|
||||||
|
|
||||||
for task in automaticSyncTasks.values {
|
for task in automaticSyncTasks.values {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@ -257,53 +226,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
if let removedSource {
|
if let removedSource {
|
||||||
purgeCachedArtifacts(for: removedSource)
|
purgeCachedArtifacts(for: removedSource)
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setItemActionInProgress(_ description: String) {
|
|
||||||
cancelFooterReset()
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .inProgress,
|
|
||||||
title: description,
|
|
||||||
subtitle: nil,
|
|
||||||
detail: nil,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setItemActionFailure(_ message: String) {
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .failure,
|
|
||||||
title: "Action Failed",
|
|
||||||
subtitle: message,
|
|
||||||
detail: nil,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
scheduleFooterReset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setItemActionSuccess(title: String, subtitle: String, revealURL: URL?) {
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .success,
|
|
||||||
title: title,
|
|
||||||
subtitle: subtitle,
|
|
||||||
detail: nil,
|
|
||||||
revealURL: revealURL
|
|
||||||
)
|
|
||||||
scheduleFooterReset()
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeScanSummary: String? {
|
|
||||||
let scanningSources = sources.filter(\.isScanning)
|
|
||||||
guard !scanningSources.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if scanningSources.count == 1, let source = scanningSources.first {
|
|
||||||
return "\(source.displayName): \(source.scanStatus)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Scanning \(scanningSources.count) sources..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startScan(for sourceID: URL, mode: SourceDiscoveryMode) {
|
private func startScan(for sourceID: URL, mode: SourceDiscoveryMode) {
|
||||||
@ -361,7 +283,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
source.previewLoadedCount = 0
|
source.previewLoadedCount = 0
|
||||||
source.sizeLoadedCount = 0
|
source.sizeLoadedCount = 0
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source)
|
source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source)
|
||||||
@ -383,7 +304,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
source.availability = .available
|
source.availability = .available
|
||||||
source.scanStatus = scanningLibraryStatus(for: source, mode: mode)
|
source.scanStatus = scanningLibraryStatus(for: source, mode: mode)
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let index = SourceIndexActor(sourceID: sourceID, folderURL: scanContextURL)
|
let index = SourceIndexActor(sourceID: sourceID, folderURL: scanContextURL)
|
||||||
@ -405,7 +325,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
if let snapshot = await index.applyEnrichedItem(enrichedItem) {
|
if let snapshot = await index.applyEnrichedItem(enrichedItem) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
library.applySnapshot(snapshot, to: sourceID)
|
library.applySnapshot(snapshot, to: sourceID)
|
||||||
library.scheduleSidebarFooterRefresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,8 +385,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
) {
|
) {
|
||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
}
|
}
|
||||||
scheduleSidebarFooterRefresh()
|
|
||||||
|
|
||||||
if itemForIndex.id == item.id, itemForIndex.metadataLoaded == false {
|
if itemForIndex.id == item.id, itemForIndex.metadataLoaded == false {
|
||||||
await enrichmentQueue.enqueue(item)
|
await enrichmentQueue.enqueue(item)
|
||||||
}
|
}
|
||||||
@ -509,8 +426,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
if let snapshot = await index.markDiscoveryFinished() {
|
if let snapshot = await index.markDiscoveryFinished() {
|
||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
await enrichmentQueue.finish()
|
await enrichmentQueue.finish()
|
||||||
let enrichmentStartTime = Date()
|
let enrichmentStartTime = Date()
|
||||||
|
|
||||||
@ -529,7 +444,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
}
|
}
|
||||||
persistSourceIfAvailable(withID: sourceID)
|
persistSourceIfAvailable(withID: sourceID)
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
let previewStageStartTime = Date()
|
let previewStageStartTime = Date()
|
||||||
let previewSeedItems = await index.currentItems()
|
let previewSeedItems = await index.currentItems()
|
||||||
@ -540,7 +454,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
for previewItem in previewItems {
|
for previewItem in previewItems {
|
||||||
if let snapshot = await index.applyPreviewItem(previewItem) {
|
if let snapshot = await index.applyPreviewItem(previewItem) {
|
||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
scheduleSidebarFooterRefresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,7 +468,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
}
|
}
|
||||||
persistSourceIfAvailable(withID: sourceID)
|
persistSourceIfAvailable(withID: sourceID)
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
if source.origin.kind == .connectedDevice {
|
if source.origin.kind == .connectedDevice {
|
||||||
let sizeStageStartTime = Date()
|
let sizeStageStartTime = Date()
|
||||||
@ -567,7 +479,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
for sizedItem in sizedItems {
|
for sizedItem in sizedItems {
|
||||||
if let snapshot = await index.applySizedItem(sizedItem) {
|
if let snapshot = await index.applySizedItem(sizedItem) {
|
||||||
applySnapshot(snapshot, to: sourceID)
|
applySnapshot(snapshot, to: sourceID)
|
||||||
scheduleSidebarFooterRefresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,7 +507,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
persistSourceIfAvailable(withID: sourceID)
|
persistSourceIfAvailable(withID: sourceID)
|
||||||
refreshSidebarFooterState()
|
|
||||||
logScanStage(
|
logScanStage(
|
||||||
"Total",
|
"Total",
|
||||||
elapsed: Date().timeIntervalSince(scanStartTime),
|
elapsed: Date().timeIntervalSince(scanStartTime),
|
||||||
@ -629,7 +539,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
if let snapshot = await index.applySizedItem(sizedItem) {
|
if let snapshot = await index.applySizedItem(sizedItem) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
library.applySnapshot(snapshot, to: sourceID)
|
library.applySnapshot(snapshot, to: sourceID)
|
||||||
library.scheduleSidebarFooterRefresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -670,7 +579,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
persistSourceIfAvailable(withID: sourceID)
|
persistSourceIfAvailable(withID: sourceID)
|
||||||
refreshSidebarFooterState()
|
|
||||||
logScanStage(
|
logScanStage(
|
||||||
"Total",
|
"Total",
|
||||||
elapsed: Date().timeIntervalSince(scanStartTime),
|
elapsed: Date().timeIntervalSince(scanStartTime),
|
||||||
@ -719,7 +627,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
source.isScanning = false
|
source.isScanning = false
|
||||||
}
|
}
|
||||||
persistSourceIfAvailable(withID: sourceID)
|
persistSourceIfAvailable(withID: sourceID)
|
||||||
refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -746,7 +653,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
sourceID: sourceID
|
sourceID: sourceID
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSizedItem(_ sizedItem: MinecraftContentItem, for sourceID: URL) {
|
private func handleSizedItem(_ sizedItem: MinecraftContentItem, for sourceID: URL) {
|
||||||
@ -762,7 +668,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
source.scanStatus = "Calculating sizes for \(source.rawItems.filter(\.sizeLoaded).count) of \(source.indexedItemCount) items..."
|
source.scanStatus = "Calculating sizes for \(source.rawItems.filter(\.sizeLoaded).count) of \(source.indexedItemCount) items..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rebuildNormalizedIndex(for sourceID: URL) {
|
private func rebuildNormalizedIndex(for sourceID: URL) {
|
||||||
@ -1647,7 +1552,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
private func restorePersistedSources() async {
|
private func restorePersistedSources() async {
|
||||||
defer {
|
defer {
|
||||||
isRestoringPersistedSources = false
|
isRestoringPersistedSources = false
|
||||||
refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let records: [PersistedSourceRecord]
|
let records: [PersistedSourceRecord]
|
||||||
@ -1708,7 +1612,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
||||||
refreshSidebarFooterState()
|
|
||||||
await Task.yield()
|
await Task.yield()
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
@ -1993,91 +1896,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
contentType == .behaviorPack || contentType == .resourcePack
|
contentType == .behaviorPack || contentType == .resourcePack
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshSidebarFooterState() {
|
|
||||||
footerRefreshTask?.cancel()
|
|
||||||
footerRefreshTask = nil
|
|
||||||
|
|
||||||
if isRestoringPersistedSources {
|
|
||||||
cancelFooterReset()
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .inProgress,
|
|
||||||
title: "Restoring library...",
|
|
||||||
subtitle: "Loading saved sources and cached metadata",
|
|
||||||
detail: nil,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let scanningSources = sources.filter(\.isScanning)
|
|
||||||
if let source = scanningSources.first {
|
|
||||||
cancelFooterReset()
|
|
||||||
let title = source.liveScanStatusTitle.isEmpty
|
|
||||||
? "Scanning Minecraft library..."
|
|
||||||
: source.liveScanStatusTitle
|
|
||||||
let subtitle: String
|
|
||||||
let detail: String?
|
|
||||||
if source.indexedItemCount > 0 {
|
|
||||||
subtitle = source.displayName
|
|
||||||
switch source.scanPhase {
|
|
||||||
case .discovering:
|
|
||||||
detail = "\(source.indexedItemCount) items found"
|
|
||||||
case .metadata:
|
|
||||||
detail = "\(source.indexedDetailCount) of \(source.indexedItemCount) indexed"
|
|
||||||
case .previews:
|
|
||||||
detail = "\(source.previewLoadedCount) of \(source.indexedItemCount) previews loaded"
|
|
||||||
case .sizing:
|
|
||||||
detail = "\(source.sizeLoadedCount) of \(source.indexedItemCount) sizes calculated"
|
|
||||||
case .completed, .idle:
|
|
||||||
detail = "\(source.indexedDetailCount) of \(source.indexedItemCount) indexed"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
subtitle = "Searching \(source.displayName)"
|
|
||||||
detail = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .inProgress,
|
|
||||||
title: title,
|
|
||||||
subtitle: subtitle,
|
|
||||||
detail: detail,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let source = sources.first(where: { $0.scanError != nil }) {
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
|
||||||
style: .failure,
|
|
||||||
title: "Scan failed",
|
|
||||||
subtitle: source.scanError,
|
|
||||||
detail: nil,
|
|
||||||
revealURL: nil
|
|
||||||
)
|
|
||||||
scheduleFooterReset()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cancelFooterReset()
|
|
||||||
sidebarFooterState = SidebarFooterState(style: .idle, title: "", subtitle: nil, detail: nil, revealURL: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleSidebarFooterRefresh() {
|
|
||||||
guard !isRestoringPersistedSources else {
|
|
||||||
refreshSidebarFooterState()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
footerRefreshTask?.cancel()
|
|
||||||
footerRefreshTask = Task { @MainActor [weak self] in
|
|
||||||
try? await Task.sleep(for: .seconds(Self.footerRefreshDebounce))
|
|
||||||
guard let self, !Task.isCancelled else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refreshSidebarFooterState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
private func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||||
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
||||||
@ -2165,11 +1983,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cancelFooterReset() {
|
|
||||||
footerResetTask?.cancel()
|
|
||||||
footerResetTask = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func initialScanStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
private func initialScanStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
||||||
switch (source.origin, mode) {
|
switch (source.origin, mode) {
|
||||||
case (.localFolder, .fullScan):
|
case (.localFolder, .fullScan):
|
||||||
@ -2231,18 +2044,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scheduleFooterReset(after seconds: Double = 5) {
|
|
||||||
cancelFooterReset()
|
|
||||||
footerResetTask = Task { @MainActor [weak self] in
|
|
||||||
try? await Task.sleep(for: .seconds(seconds))
|
|
||||||
guard let self, !Task.isCancelled else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refreshSidebarFooterState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildSnapshot(
|
private func buildSnapshot(
|
||||||
for source: MinecraftSource,
|
for source: MinecraftSource,
|
||||||
scanRootURL: URL,
|
scanRootURL: URL,
|
||||||
|
|||||||
@ -25,13 +25,11 @@ struct SourcesSidebarView: View {
|
|||||||
let sources: [MinecraftSource]
|
let sources: [MinecraftSource]
|
||||||
let connectedDevices: [ConnectedDeviceSidebarEntry]
|
let connectedDevices: [ConnectedDeviceSidebarEntry]
|
||||||
@Binding var selection: SidebarSelection?
|
@Binding var selection: SidebarSelection?
|
||||||
let footerState: SidebarFooterState
|
|
||||||
let addSourceAction: () -> Void
|
let addSourceAction: () -> Void
|
||||||
let addDeviceSourceAction: () -> Void
|
let addDeviceSourceAction: () -> Void
|
||||||
let addConnectedDeviceAction: (ConnectedDeviceSidebarEntry) -> Void
|
let addConnectedDeviceAction: (ConnectedDeviceSidebarEntry) -> Void
|
||||||
let rescanSourceAction: (MinecraftSource) -> Void
|
let rescanSourceAction: (MinecraftSource) -> Void
|
||||||
let removeSourceAction: (MinecraftSource) -> Void
|
let removeSourceAction: (MinecraftSource) -> Void
|
||||||
let revealFooterURLAction: (URL) -> Void
|
|
||||||
let filters: (MinecraftSource) -> [SidebarFilter]
|
let filters: (MinecraftSource) -> [SidebarFilter]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -466,93 +464,6 @@ private struct ConnectedDeviceTransportIcon: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SidebarFooterView: View {
|
|
||||||
let state: SidebarFooterState
|
|
||||||
let revealAction: (URL) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
if state.style == .inProgress {
|
|
||||||
ProgressView()
|
|
||||||
.controlSize(.small)
|
|
||||||
.tint(.appAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(state.title)
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
.foregroundStyle(primaryColor)
|
|
||||||
.lineLimit(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let subtitle = state.subtitle {
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let detail = state.detail {
|
|
||||||
Text(detail)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let revealURL = state.revealURL {
|
|
||||||
Button("Reveal in Finder") {
|
|
||||||
revealAction(revealURL)
|
|
||||||
}
|
|
||||||
.buttonStyle(.link)
|
|
||||||
.font(.footnote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.background(cardBackground, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
||||||
.overlay {
|
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
||||||
.strokeBorder(cardStroke)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var primaryColor: Color {
|
|
||||||
switch state.style {
|
|
||||||
case .idle:
|
|
||||||
return .primary
|
|
||||||
case .inProgress:
|
|
||||||
return .appAccent
|
|
||||||
case .failure:
|
|
||||||
return .red
|
|
||||||
case .success:
|
|
||||||
return .appAccent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cardBackground: AnyShapeStyle {
|
|
||||||
switch state.style {
|
|
||||||
case .inProgress:
|
|
||||||
return AnyShapeStyle(Color.appAccent.opacity(0.08))
|
|
||||||
default:
|
|
||||||
return AnyShapeStyle(.regularMaterial)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cardStroke: Color {
|
|
||||||
switch state.style {
|
|
||||||
case .inProgress:
|
|
||||||
return Color.appAccent.opacity(0.18)
|
|
||||||
case .failure:
|
|
||||||
return .red.opacity(0.18)
|
|
||||||
case .success:
|
|
||||||
return Color.appAccent.opacity(0.16)
|
|
||||||
case .idle:
|
|
||||||
return .white.opacity(0.08)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SidebarColumnViews_Previews: PreviewProvider {
|
struct SidebarColumnViews_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SidebarColumnPreviewContainer()
|
SidebarColumnPreviewContainer()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user