Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public struct ActionableInfoBar<Action: View>: View {
if let systemImage {
Image(systemName: systemImage)
.foregroundStyle(iconColor)
.accessibilityHidden(true)
}

Text(message)
Expand Down
5 changes: 5 additions & 0 deletions MarkEditMac/Sources/Extension/ExtensionsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ final class ExtensionsModel {

var mode: Mode = .discover
var phase: Phase = .loading
var hasLoadedIndex = false
var pendingRelaunch = false
var isBusy = false

Expand Down Expand Up @@ -348,6 +349,10 @@ private extension ExtensionsModel {

/// Rebuilds both lists; `installed` is injectable so the mapping can be tested in isolation.
func rebuildItems(index: ExtensionIndex?, installed: [ExtensionConfig.Installed] = ExtensionConfig.installed) {
if index != nil {
hasLoadedIndex = true
}

let entries = index?.extensions ?? []
let entryByID = Dictionary(entries.map { ($0.id, $0) }) { lhs, _ in lhs }

Expand Down
54 changes: 36 additions & 18 deletions MarkEditMac/Sources/Extension/ExtensionsOverlays.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ struct ExtensionsStateView: View {
Double($0.completed) / Double($0.total)
}
)
} else if model.phase == .loading {
} else if model.phase == .loading && !model.hasLoadedIndex {
// Only spin on a cold start with no catalog yet
ProgressView()
} else if model.items.isEmpty {
// Gate on the live item count so a mode switch doesn't flash a stale empty message
emptyState
EmptyStateView(isRegistryError: isRegistryError, emptyMessage: emptyMessage) {
Task {
await model.load(forceRefresh: true)
}
}
}
}
}
Expand All @@ -51,28 +56,41 @@ struct ExtensionsInfoBar: View {
// MARK: - Private

private extension ExtensionsStateView {
var isRegistryError: Bool {
model.mode == .discover && model.phase == .failed
}
struct EmptyStateView: View {
@Environment(\.accessibilityReduceMotion)
private var reduceMotion
@State private var visible = false

let isRegistryError: Bool
let emptyMessage: String
let retryAction: () -> Void

var emptyState: some View {
VStack(spacing: 10) {
Image(systemName: isRegistryError ? Icons.wifiSlash : Icons.puzzlepieceExtension)
.font(.largeTitle)
.foregroundStyle(.secondary)
var body: some View {
VStack(spacing: 10) {
Image(systemName: isRegistryError ? Icons.wifiSlash : Icons.puzzlepieceExtension)
.font(.largeTitle)
.foregroundStyle(.secondary)
Comment thread
Copilot marked this conversation as resolved.
.accessibilityHidden(true)

Text(emptyMessage)
.foregroundStyle(.secondary)
Text(emptyMessage)
.foregroundStyle(.secondary)

if isRegistryError {
Button(Localized.Extension.retry) {
Task {
await model.load(forceRefresh: true)
}
if isRegistryError {
Button(Localized.Extension.retry, action: retryAction)
}
}
.padding()
.opacity(visible ? 1 : 0)
.onAppear {
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.25)) {
visible = true
}
}
}
.padding()
}

var isRegistryError: Bool {
model.mode == .discover && model.phase == .failed
}

var emptyMessage: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ private extension ExtensionsViewController {
_ = model.mode
_ = model.items
_ = model.phase
_ = model.hasLoadedIndex
_ = model.pendingRelaunch
} onChange: { [weak self] in
Task { @MainActor in
Expand Down