Move search back to toolbar. still don't love the UI but gettin gthere
This commit is contained in:
parent
711ac54f00
commit
2932ac2f48
@ -33,15 +33,18 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
.navigationTitle("Sources")
|
|
||||||
|
|
||||||
Divider()
|
if library.sidebarFooterState.style != .idle {
|
||||||
|
SidebarFooterView(
|
||||||
SidebarFooterView(
|
state: library.sidebarFooterState,
|
||||||
state: library.sidebarFooterState,
|
revealAction: revealURLInFinder(_:)
|
||||||
revealAction: revealURLInFinder(_:)
|
)
|
||||||
)
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: library.sidebarFooterState.style)
|
||||||
} content: {
|
} content: {
|
||||||
if library.sources.isEmpty {
|
if library.sources.isEmpty {
|
||||||
EmptySourcesView(
|
EmptySourcesView(
|
||||||
@ -53,9 +56,7 @@ struct ContentView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ContentCollectionHeaderView(
|
ContentCollectionHeaderView(
|
||||||
title: collectionHeaderTitle,
|
title: collectionHeaderTitle,
|
||||||
subtitle: collectionHeaderSubtitle,
|
subtitle: collectionHeaderSubtitle
|
||||||
prompt: searchPrompt,
|
|
||||||
searchText: $searchText
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@ -93,8 +94,8 @@ struct ContentView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Minecraft World Manager")
|
|
||||||
.tint(.minecraftAccent)
|
.tint(.minecraftAccent)
|
||||||
|
.searchable(text: $searchText, placement: .toolbar, prompt: searchPrompt)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .primaryAction) {
|
ToolbarItemGroup(placement: .primaryAction) {
|
||||||
if let selectedExportableItem = selectedExportableItem {
|
if let selectedExportableItem = selectedExportableItem {
|
||||||
@ -205,6 +206,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var collectionHeaderTitle: String {
|
private var collectionHeaderTitle: String {
|
||||||
|
if isSearching {
|
||||||
|
return "Searching “\(searchScopeTitle)”"
|
||||||
|
}
|
||||||
|
|
||||||
guard let selectedSidebarSelection else {
|
guard let selectedSidebarSelection else {
|
||||||
return "Minecraft Content"
|
return "Minecraft Content"
|
||||||
}
|
}
|
||||||
@ -222,13 +227,28 @@ struct ContentView: View {
|
|||||||
let filteredCount = filteredItems.count
|
let filteredCount = filteredItems.count
|
||||||
let noun = collectionCountNoun
|
let noun = collectionCountNoun
|
||||||
|
|
||||||
if searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if !isSearching {
|
||||||
return "\(totalCount.formatted(.number)) \(noun)"
|
return "\(totalCount.formatted(.number)) \(noun)"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "\(filteredCount.formatted(.number)) of \(totalCount.formatted(.number)) \(noun)"
|
return "\(filteredCount.formatted(.number)) of \(totalCount.formatted(.number)) \(noun)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var searchScopeTitle: String {
|
||||||
|
switch selectedSidebarSelection {
|
||||||
|
case .some(.allContent):
|
||||||
|
return "All"
|
||||||
|
case .some(.contentType(_, let contentType)):
|
||||||
|
return sidebarTitle(for: contentType)
|
||||||
|
case .none:
|
||||||
|
return "Content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isSearching: Bool {
|
||||||
|
!searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
private var collectionCountNoun: String {
|
private var collectionCountNoun: String {
|
||||||
guard let selectedSidebarSelection else {
|
guard let selectedSidebarSelection else {
|
||||||
return "items"
|
return "items"
|
||||||
@ -658,7 +678,7 @@ private struct SidebarFooterView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.background(.bar)
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var primaryColor: Color {
|
private var primaryColor: Color {
|
||||||
@ -676,22 +696,18 @@ private struct SidebarFooterView: View {
|
|||||||
private struct ContentCollectionHeaderView: View {
|
private struct ContentCollectionHeaderView: View {
|
||||||
let title: String
|
let title: String
|
||||||
let subtitle: String
|
let subtitle: String
|
||||||
let prompt: String
|
|
||||||
@Binding var searchText: String
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.title2.weight(.semibold))
|
.font(.title2.weight(.semibold))
|
||||||
|
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
TextField(prompt, text: $searchText)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 14)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(.background)
|
.background(.background)
|
||||||
}
|
}
|
||||||
@ -752,9 +768,10 @@ private struct ItemDetailView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 24) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
LargeItemThumbnailView(iconURL: item.iconURL)
|
LargeItemThumbnailView(iconURL: item.iconURL, contentType: item.contentType)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(item.displayName)
|
Text(item.displayName)
|
||||||
@ -764,49 +781,60 @@ private struct ItemDetailView: View {
|
|||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 18) {
|
|
||||||
metadataChip(title: "Size", value: sizeText)
|
|
||||||
metadataChip(title: item.displayDateLabel, value: displayDateText)
|
|
||||||
|
|
||||||
if item.lastPlayedDate == nil, item.contentType == .world {
|
|
||||||
metadataChip(title: "Last Played", value: "Not available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
detailCard {
|
||||||
Text("Actions")
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
.font(.headline)
|
Text("Actions")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
Button(primaryActionTitle) {
|
Button(primaryActionTitle) {
|
||||||
primaryAction()
|
primaryAction()
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.disabled(isPerformingItemAction)
|
|
||||||
|
|
||||||
Text(primaryActionSubtitle)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
SharingPickerButton(
|
|
||||||
title: "Share...",
|
|
||||||
systemImage: "square.and.arrow.up",
|
|
||||||
isEnabled: !isPerformingItemAction
|
|
||||||
) { anchorView in
|
|
||||||
shareAction(anchorView)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Reveal in Finder") {
|
|
||||||
revealAction()
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.large)
|
||||||
.disabled(isPerformingItemAction)
|
.disabled(isPerformingItemAction)
|
||||||
|
|
||||||
|
Text(primaryActionSubtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
SharingPickerButton(
|
||||||
|
title: "Share...",
|
||||||
|
systemImage: "square.and.arrow.up",
|
||||||
|
isEnabled: !isPerformingItemAction
|
||||||
|
) { anchorView in
|
||||||
|
shareAction(anchorView)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Reveal in Finder") {
|
||||||
|
revealAction()
|
||||||
|
}
|
||||||
|
.disabled(isPerformingItemAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.contentType == .world {
|
detailCard {
|
||||||
if !behaviorPacks.isEmpty || !resourcePacks.isEmpty {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
Text("Details")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
detailValueRow(title: "Size", value: sizeText)
|
||||||
|
detailValueRow(title: item.displayDateLabel, value: displayDateText)
|
||||||
|
|
||||||
|
if item.contentType == .world {
|
||||||
|
detailValueRow(
|
||||||
|
title: "Last Played",
|
||||||
|
value: item.lastPlayedDate?.formatted(date: .abbreviated, time: .omitted) ?? "Not available"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.contentType == .world, !behaviorPacks.isEmpty || !resourcePacks.isEmpty {
|
||||||
|
detailCard {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
Text("Packs Used")
|
Text("Packs Used")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@ -822,55 +850,55 @@ private struct ItemDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisclosureGroup("Technical Details") {
|
detailCard {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
DisclosureGroup("Technical Details") {
|
||||||
detailRow(title: "Folder ID", value: item.folderID)
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
detailRow(title: "Folder Path", value: item.folderURL.path)
|
detailRow(title: "Folder ID", value: item.folderID)
|
||||||
detailRow(title: "Collection Root", value: item.collectionRootURL.path)
|
detailRow(title: "Folder Path", value: item.folderURL.path)
|
||||||
|
detailRow(title: "Collection Root", value: item.collectionRootURL.path)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Contents")
|
Text("Contents")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
if contents.isEmpty {
|
|
||||||
Text("No visible files or folders")
|
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
} else {
|
|
||||||
ForEach(contents) { entry in
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
Image(systemName: entry.isDirectory ? "folder" : "doc")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text(entry.name)
|
|
||||||
.lineLimit(1)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if contents.count == directoryPreviewLimit {
|
if contents.isEmpty {
|
||||||
Text("Showing the first \(directoryPreviewLimit) items")
|
Text("No visible files or folders")
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
ForEach(contents) { entry in
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: entry.isDirectory ? "folder" : "doc")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(entry.name)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if contents.count == directoryPreviewLimit {
|
||||||
|
Text("Showing the first \(directoryPreviewLimit) items")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(28)
|
||||||
|
.frame(maxWidth: 760, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func metadataChip(title: String, value: String) -> some View {
|
private func detailCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
content()
|
||||||
Text(title)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.font(.caption)
|
.padding(18)
|
||||||
.foregroundStyle(.secondary)
|
.background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||||
Text(value)
|
|
||||||
.font(.body.weight(.medium))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -905,6 +933,18 @@ private struct ItemDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func detailValueRow(title: String, value: String) -> some View {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||||
|
Text(title)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var sizeText: String {
|
private var sizeText: String {
|
||||||
item.sizeBytes.map { ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) } ?? "Unknown"
|
item.sizeBytes.map { ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) } ?? "Unknown"
|
||||||
}
|
}
|
||||||
@ -1034,25 +1074,41 @@ private struct ItemThumbnailView: View {
|
|||||||
|
|
||||||
private struct LargeItemThumbnailView: View {
|
private struct LargeItemThumbnailView: View {
|
||||||
let iconURL: URL?
|
let iconURL: URL?
|
||||||
|
let contentType: MinecraftContentType
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let image = loadImage(from: iconURL) {
|
if let image = loadImage(from: iconURL) {
|
||||||
Image(nsImage: image)
|
Image(nsImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 180, height: 180)
|
.frame(maxWidth: 420, minHeight: 260, maxHeight: 340)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 28)
|
||||||
.fill(.quaternary)
|
.fill(.quaternary)
|
||||||
.frame(width: 180, height: 180)
|
.frame(maxWidth: 420, minHeight: 260, maxHeight: 340)
|
||||||
.overlay(
|
.overlay(
|
||||||
Image(systemName: "shippingbox")
|
Image(systemName: fallbackIconName)
|
||||||
.font(.largeTitle)
|
.font(.system(size: 56))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var fallbackIconName: 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage(from url: URL?) -> NSImage? {
|
private func loadImage(from url: URL?) -> NSImage? {
|
||||||
|
|||||||
@ -27,12 +27,13 @@ final class SourceLibrary: ObservableObject {
|
|||||||
@Published var sources: [MinecraftSource] = []
|
@Published var sources: [MinecraftSource] = []
|
||||||
@Published private(set) var sidebarFooterState = SidebarFooterState(
|
@Published private(set) var sidebarFooterState = SidebarFooterState(
|
||||||
style: .idle,
|
style: .idle,
|
||||||
title: "Ready",
|
title: "",
|
||||||
subtitle: nil,
|
subtitle: nil,
|
||||||
revealURL: nil
|
revealURL: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
||||||
|
private var footerResetTask: Task<Void, Never>?
|
||||||
|
|
||||||
func addSource(at url: URL) -> URL {
|
func addSource(at url: URL) -> URL {
|
||||||
let normalizedURL = url.standardizedFileURL
|
let normalizedURL = url.standardizedFileURL
|
||||||
@ -64,6 +65,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setItemActionInProgress(_ description: String) {
|
func setItemActionInProgress(_ description: String) {
|
||||||
|
cancelFooterReset()
|
||||||
sidebarFooterState = SidebarFooterState(
|
sidebarFooterState = SidebarFooterState(
|
||||||
style: .inProgress,
|
style: .inProgress,
|
||||||
title: description,
|
title: description,
|
||||||
@ -79,6 +81,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
subtitle: message,
|
subtitle: message,
|
||||||
revealURL: nil
|
revealURL: nil
|
||||||
)
|
)
|
||||||
|
scheduleFooterReset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setItemActionSuccess(title: String, subtitle: String, revealURL: URL?) {
|
func setItemActionSuccess(title: String, subtitle: String, revealURL: URL?) {
|
||||||
@ -88,6 +91,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
revealURL: revealURL
|
revealURL: revealURL
|
||||||
)
|
)
|
||||||
|
scheduleFooterReset()
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeScanSummary: String? {
|
var activeScanSummary: String? {
|
||||||
@ -216,7 +220,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
private func refreshSidebarFooterState() {
|
private func refreshSidebarFooterState() {
|
||||||
let scanningSources = sources.filter(\.isScanning)
|
let scanningSources = sources.filter(\.isScanning)
|
||||||
if let source = scanningSources.first {
|
if let source = scanningSources.first {
|
||||||
let title = source.itemCount == 0 ? "Scanning worlds..." : "Scanning worlds..."
|
cancelFooterReset()
|
||||||
let subtitle: String
|
let subtitle: String
|
||||||
if source.indexedItemCount > 0 {
|
if source.indexedItemCount > 0 {
|
||||||
subtitle = "\(source.indexedDetailCount) of \(source.indexedItemCount) indexed"
|
subtitle = "\(source.indexedDetailCount) of \(source.indexedItemCount) indexed"
|
||||||
@ -226,7 +230,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
sidebarFooterState = SidebarFooterState(
|
||||||
style: .inProgress,
|
style: .inProgress,
|
||||||
title: title,
|
title: "Scanning...",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
revealURL: nil
|
revealURL: nil
|
||||||
)
|
)
|
||||||
@ -240,21 +244,27 @@ final class SourceLibrary: ObservableObject {
|
|||||||
subtitle: source.scanError,
|
subtitle: source.scanError,
|
||||||
revealURL: nil
|
revealURL: nil
|
||||||
)
|
)
|
||||||
|
scheduleFooterReset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cancelFooterReset()
|
||||||
|
sidebarFooterState = SidebarFooterState(style: .idle, title: "", subtitle: nil, revealURL: nil)
|
||||||
|
}
|
||||||
|
|
||||||
let totalItems = sources.reduce(0) { $0 + $1.itemCount }
|
private func cancelFooterReset() {
|
||||||
let subtitle = totalItems == 0 ? "No content indexed" : "\(totalItems.formatted(.number)) items indexed"
|
footerResetTask?.cancel()
|
||||||
let lastUpdatedDate = sources.compactMap(\.lastScanDate).max()
|
footerResetTask = nil
|
||||||
let secondaryText = lastUpdatedDate.map {
|
}
|
||||||
"\(subtitle) \u{2022} Last updated \($0.formatted(.relative(presentation: .named)))"
|
|
||||||
} ?? subtitle
|
|
||||||
|
|
||||||
sidebarFooterState = SidebarFooterState(
|
private func scheduleFooterReset(after seconds: Double = 5) {
|
||||||
style: .idle,
|
cancelFooterReset()
|
||||||
title: "Ready",
|
footerResetTask = Task { @MainActor [weak self] in
|
||||||
subtitle: secondaryText,
|
try? await Task.sleep(for: .seconds(seconds))
|
||||||
revealURL: nil
|
guard let self, !Task.isCancelled else {
|
||||||
)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshSidebarFooterState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user