Render Add Source buttons inline on macOS where toolbar items aren't shown

Pre-macOS 26 doesn't reliably surface a detail-pane NavigationStack's
toolbar confirmation items in the window title bar, leaving the
"Add Source" actions invisible. Gate the toolbar placement behind
macOS 26+ and fall back to an inline form/header button on older
macOS across the add-source views and the sources list.
This commit is contained in:
Arkadiusz Fal
2026-05-15 07:55:30 +02:00
parent bc81b03821
commit 37d11b50a8
5 changed files with 172 additions and 40 deletions

View File

@@ -31,6 +31,21 @@ struct AddLocalFolderView: View {
!name.isEmpty && selectedFolderURL != nil !name.isEmpty && selectedFolderURL != nil
} }
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@@ -99,16 +114,22 @@ struct AddLocalFolderView: View {
if let result = testResult { if let result = testResult {
SourceTestResultSection(result: result) SourceTestResultSection(result: result)
} }
if !usesToolbarActionButton {
actionSection
}
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(String(localized: "sources.addLocalFolder")) .navigationTitle(String(localized: "sources.addLocalFolder"))
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { if usesToolbarActionButton {
Button(String(localized: "sources.addSource")) { ToolbarItem(placement: .confirmationAction) {
addSource() Button(String(localized: "sources.addSource")) {
addSource()
}
.disabled(!canAdd)
.keyboardShortcut(.defaultAction)
} }
.disabled(!canAdd)
.keyboardShortcut(.defaultAction)
} }
} }
} }

View File

@@ -87,6 +87,22 @@ struct AddRemoteServerView: View {
return false return false
} }
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS (and on iOS/tvOS) we
/// render the "Add Source" action button inline in the form instead, so it
/// is always visible.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
private var canAdd: Bool { private var canAdd: Bool {
guard !urlString.isEmpty else { return false } guard !urlString.isEmpty else { return false }
@@ -172,9 +188,9 @@ struct AddRemoteServerView: View {
if isFieldsRevealed { if isFieldsRevealed {
serverConfigurationFields serverConfigurationFields
#if !os(macOS) if !usesToolbarActionButton {
actionSection actionSection
#endif }
} }
} }
#if os(iOS) #if os(iOS)
@@ -183,8 +199,8 @@ struct AddRemoteServerView: View {
#if os(macOS) #if os(macOS)
.formStyle(.grouped) .formStyle(.grouped)
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { if isFieldsRevealed, usesToolbarActionButton {
if isFieldsRevealed { ToolbarItem(placement: .confirmationAction) {
Button { Button {
addSource() addSource()
} label: { } label: {

View File

@@ -36,6 +36,21 @@ struct AddSMBView: View {
!name.isEmpty && !server.isEmpty !name.isEmpty && !server.isEmpty
} }
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@@ -133,25 +148,31 @@ struct AddSMBView: View {
if let result = testResult { if let result = testResult {
SourceTestResultSection(result: result) SourceTestResultSection(result: result)
} }
if !usesToolbarActionButton {
actionSection
}
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(String(localized: "sources.addSMB")) .navigationTitle(String(localized: "sources.addSMB"))
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { if usesToolbarActionButton {
Button { ToolbarItem(placement: .confirmationAction) {
addSource() Button {
} label: { addSource()
if isTesting { } label: {
HStack(spacing: 6) { if isTesting {
ProgressView().controlSize(.small) HStack(spacing: 6) {
Text(testProgress ?? String(localized: "sources.testing")) ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
} }
} else {
Text(String(localized: "sources.addSource"))
} }
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
} }
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
} }
} }
.onAppear { .onAppear {

View File

@@ -37,6 +37,21 @@ struct AddWebDAVView: View {
!name.isEmpty && !urlString.isEmpty && URL(string: urlString) != nil !name.isEmpty && !urlString.isEmpty && URL(string: urlString) != nil
} }
/// macOS surfaces toolbar confirmation items from a pushed-in-sheet view
/// reliably only from macOS 26 onward. On older macOS we render the
/// "Add Source" action button inline in the form instead.
private var usesToolbarActionButton: Bool {
#if os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@@ -134,25 +149,31 @@ struct AddWebDAVView: View {
if let result = testResult { if let result = testResult {
SourceTestResultSection(result: result) SourceTestResultSection(result: result)
} }
if !usesToolbarActionButton {
actionSection
}
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(String(localized: "sources.addWebDAV")) .navigationTitle(String(localized: "sources.addWebDAV"))
.toolbar { .toolbar {
ToolbarItem(placement: .confirmationAction) { if usesToolbarActionButton {
Button { ToolbarItem(placement: .confirmationAction) {
addSource() Button {
} label: { addSource()
if isTesting { } label: {
HStack(spacing: 6) { if isTesting {
ProgressView().controlSize(.small) HStack(spacing: 6) {
Text(testProgress ?? String(localized: "sources.testing")) ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
} }
} else {
Text(String(localized: "sources.addSource"))
} }
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
} }
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
} }
} }
.onAppear { .onAppear {

View File

@@ -64,6 +64,16 @@ struct SourcesListView: View {
.focusSection() .focusSection()
} }
} }
#elseif os(macOS)
VStack(spacing: 0) {
// macOS 15 does not surface a detail-pane NavigationStack's
// toolbar items to the window title bar, so the toolbar
// "Add Source" button is invisible there. Show an inline
// header button as a fallback on older macOS. On macOS 26+
// the toolbar button works, so this header is omitted.
macOSLegacyAddHeader
sourcesInner
}
#else #else
sourcesInner sourcesInner
#endif #endif
@@ -76,13 +86,15 @@ struct SourcesListView: View {
#endif #endif
#if os(iOS) || os(macOS) #if os(iOS) || os(macOS)
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { if showAddButtonInToolbar {
Button { ToolbarItem(placement: .primaryAction) {
showingAddSheet = true Button {
} label: { showingAddSheet = true
Label(String(localized: "sources.addSource"), systemImage: "plus") } label: {
Label(String(localized: "sources.addSource"), systemImage: "plus")
}
.accessibilityIdentifier("sources.addButton")
} }
.accessibilityIdentifier("sources.addButton")
} }
} }
#endif #endif
@@ -122,6 +134,47 @@ struct SourcesListView: View {
#endif #endif
} }
/// Whether the "Add Source" button should be placed in the window/navigation
/// toolbar. On macOS this only works reliably from macOS 26 onward (see
/// `macOSLegacyAddHeader`); iOS always uses the toolbar.
private var showAddButtonInToolbar: Bool {
#if os(iOS)
return true
#elseif os(macOS)
if #available(macOS 26, *) {
return true
} else {
return false
}
#else
return false
#endif
}
#if os(macOS)
// Inline fallback "Add Source" button for macOS versions where the
// detail-pane toolbar item is not shown (pre-macOS 26). Only needed when
// there are existing sources the empty state already offers its own
// add button.
@ViewBuilder
private var macOSLegacyAddHeader: some View {
if !showAddButtonInToolbar, !isEmpty {
HStack {
Spacer()
Button {
showingAddSheet = true
} label: {
Label(String(localized: "sources.addSource"), systemImage: "plus")
}
.accessibilityIdentifier("sources.addButton")
}
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 4)
}
}
#endif
@ViewBuilder @ViewBuilder
private var sourcesInner: some View { private var sourcesInner: some View {
if isEmpty { if isEmpty {