Make source add/edit forms feel native on macOS

Use grouped Form style with LabeledContent rows and move primary
actions into the sheet toolbar for SMB, WebDAV, Local Folder, Remote
Server and the Edit sheet. iOS and tvOS branches unchanged.
This commit is contained in:
Arkadiusz Fal
2026-04-20 20:51:24 +02:00
parent e0e1e8cbd7
commit 508069cecf
6 changed files with 370 additions and 1 deletions

View File

@@ -34,6 +34,9 @@ struct AddLocalFolderView: View {
// MARK: - Body
var body: some View {
#if os(macOS)
macOSBody
#else
Form {
nameSection
folderSection
@@ -53,8 +56,64 @@ struct AddLocalFolderView: View {
}
}
#endif
#endif
}
#if os(macOS)
private var macOSBody: some View {
Form {
Section {
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name)
}
} footer: {
Text(String(localized: "sources.footer.displayName"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
LabeledContent(String(localized: "sources.header.folder")) {
HStack {
if let url = selectedFolderURL {
Text(url.path)
.lineLimit(1)
.truncationMode(.middle)
.foregroundStyle(.secondary)
.font(.system(.body, design: .monospaced))
} else {
Text(String(localized: "sources.selectFolder"))
.foregroundStyle(.secondary)
}
Button(String(localized: "sources.selectFolder")) {
selectFolderMacOS()
}
}
}
} footer: {
Text(String(localized: "sources.footer.folder"))
.font(.callout)
.foregroundStyle(.secondary)
}
if let result = testResult {
SourceTestResultSection(result: result)
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addLocalFolder"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "sources.addSource")) {
addSource()
}
.disabled(!canAdd)
.keyboardShortcut(.defaultAction)
}
}
}
#endif
// MARK: - Sections
private var nameSection: some View {

View File

@@ -172,12 +172,38 @@ struct AddRemoteServerView: View {
if isFieldsRevealed {
serverConfigurationFields
#if !os(macOS)
actionSection
#endif
}
}
#if os(iOS)
.scrollDismissesKeyboard(.interactively)
#endif
#if os(macOS)
.formStyle(.grouped)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if isFieldsRevealed {
Button {
addSource()
} label: {
if isValidatingCredentials {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(String(localized: "sources.validatingCredentials"))
}
} else {
Text(String(localized: "sources.addSource"))
}
}
.disabled(!canAdd || isValidatingCredentials)
.keyboardShortcut(.defaultAction)
.accessibilityIdentifier("addRemoteServer.actionButton")
}
}
}
#endif
}
// MARK: - URL Entry Section
@@ -207,6 +233,35 @@ struct AddRemoteServerView: View {
.buttonStyle(TVSettingsButtonStyle())
.disabled(urlString.isEmpty || uiState == .detecting)
}
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.url")) {
TextField("", text: $urlString, prompt: Text(String(localized: "sources.placeholder.urlOrAddress")))
.textContentType(.URL)
.autocorrectionDisabled()
.accessibilityIdentifier("addRemoteServer.urlField")
.onChange(of: urlString) { _, _ in
handleURLChange()
}
}
if !isFieldsRevealed && !isAwaitingBasicAuth {
HStack {
Spacer()
Button {
startDetection()
} label: {
HStack(spacing: 6) {
if case .detecting = uiState {
ProgressView()
.controlSize(.small)
}
Text(String(localized: "sources.detect"))
}
}
.disabled(urlString.isEmpty || uiState == .detecting)
.accessibilityIdentifier("addRemoteServer.detectButton")
}
}
#else
TextField(String(localized: "sources.placeholder.urlOrAddress"), text: $urlString)
.textContentType(.URL)
@@ -277,6 +332,16 @@ struct AddRemoteServerView: View {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
TVSettingsTextField(title: String(localized: "sources.field.password"), text: $basicAuthPassword, isSecure: true)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.username")) {
TextField("", text: $basicAuthUsername)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.password")) {
SecureField("", text: $basicAuthPassword)
.textContentType(.password)
}
#else
TextField(String(localized: "sources.field.username"), text: $basicAuthUsername)
.textContentType(.username)
@@ -328,6 +393,10 @@ struct AddRemoteServerView: View {
Section {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.nameOptional"), text: $name)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name, prompt: Text(String(localized: "sources.field.nameOptional")))
}
#else
TextField(String(localized: "sources.field.nameOptional"), text: $name)
#endif
@@ -381,6 +450,16 @@ struct AddRemoteServerView: View {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
TVSettingsTextField(title: String(localized: "sources.field.password"), text: $basicAuthPassword, isSecure: true)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.username")) {
TextField("", text: $basicAuthUsername)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.password")) {
SecureField("", text: $basicAuthPassword)
.textContentType(.password)
}
#else
TextField(String(localized: "sources.field.username"), text: $basicAuthUsername)
.textContentType(.username)

View File

@@ -39,6 +39,9 @@ struct AddSMBView: View {
// MARK: - Body
var body: some View {
#if os(macOS)
macOSBody
#else
Form {
nameSection
serverSection
@@ -68,8 +71,100 @@ struct AddSMBView: View {
name = prefillName
}
}
#endif
}
#if os(macOS)
private var macOSBody: some View {
Form {
Section {
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name)
}
} footer: {
Text(String(localized: "sources.footer.displayName"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
LabeledContent(String(localized: "sources.placeholder.smbServer")) {
TextField("", text: $server)
.autocorrectionDisabled()
}
} footer: {
Text(String(localized: "sources.footer.smb"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
LabeledContent(String(localized: "sources.field.usernameOptional")) {
TextField("", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.passwordOptional")) {
SecureField("", text: $password)
.textContentType(.password)
}
} header: {
Text(String(localized: "sources.header.auth"))
} footer: {
Text(String(localized: "sources.footer.auth"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
Picker(String(localized: "sources.field.smbProtocol"), selection: $protocolVersion) {
ForEach(SMBProtocol.allCases, id: \.self) { proto in
Text(proto.displayName).tag(proto)
}
}
} header: {
Text(String(localized: "sources.header.advanced"))
} footer: {
Text(String(localized: "sources.footer.smbProtocol"))
.font(.callout)
.foregroundStyle(.secondary)
}
if let result = testResult {
SourceTestResultSection(result: result)
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addSMB"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
}
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
}
.onAppear {
if let prefillServer {
server = prefillServer
}
if let prefillName, name.isEmpty {
name = prefillName
}
}
}
#endif
// MARK: - Sections
private var nameSection: some View {

View File

@@ -40,6 +40,9 @@ struct AddWebDAVView: View {
// MARK: - Body
var body: some View {
#if os(macOS)
macOSBody
#else
Form {
nameSection
serverSection
@@ -72,8 +75,100 @@ struct AddWebDAVView: View {
allowInvalidCertificates = true
}
}
#endif
}
#if os(macOS)
private var macOSBody: some View {
Form {
Section {
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name)
}
} footer: {
Text(String(localized: "sources.footer.displayName"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
LabeledContent(String(localized: "sources.placeholder.webdavUrl")) {
TextField("", text: $urlString)
.textContentType(.URL)
.autocorrectionDisabled()
}
} footer: {
Text(String(localized: "sources.footer.webdav"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
LabeledContent(String(localized: "sources.field.usernameOptional")) {
TextField("", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.passwordOptional")) {
SecureField("", text: $password)
.textContentType(.password)
}
} header: {
Text(String(localized: "sources.header.auth"))
} footer: {
Text(String(localized: "sources.footer.auth"))
.font(.callout)
.foregroundStyle(.secondary)
}
Section {
Toggle(String(localized: "sources.field.allowInvalidCertificates"), isOn: $allowInvalidCertificates)
} header: {
Text(String(localized: "sources.header.security"))
} footer: {
Text(String(localized: "sources.footer.allowInvalidCertificates"))
.font(.callout)
.foregroundStyle(.secondary)
}
if let result = testResult {
SourceTestResultSection(result: result)
}
}
.formStyle(.grouped)
.navigationTitle(String(localized: "sources.addWebDAV"))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
addSource()
} label: {
if isTesting {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
}
}
.disabled(!canAdd || isTesting)
.keyboardShortcut(.defaultAction)
}
}
.onAppear {
if let url = prefillURL {
urlString = url.absoluteString
}
if let prefillName, name.isEmpty {
name = prefillName
}
if prefillAllowInvalidCertificates {
allowInvalidCertificates = true
}
}
}
#endif
// MARK: - Sections
private var nameSection: some View {

View File

@@ -89,7 +89,7 @@ struct AddSourceView: View {
}
}
#if os(macOS)
.frame(minWidth: 500, minHeight: 450)
.frame(minWidth: 560, minHeight: 560)
#endif
.sheet(isPresented: $showingNetworkDiscovery) {
NetworkShareDiscoverySheet(filterType: selectedShareType) { share in

View File

@@ -118,6 +118,11 @@ private struct EditRemoteServerContent: View {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.name"), text: $name)
TVSettingsToggle(title: String(localized: "sources.field.enabled"), isOn: $isEnabled)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name)
}
Toggle(String(localized: "sources.field.enabled"), isOn: $isEnabled)
#else
TextField(String(localized: "sources.field.name"), text: $name)
Toggle(String(localized: "sources.field.enabled"), isOn: $isEnabled)
@@ -128,6 +133,16 @@ private struct EditRemoteServerContent: View {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
TVSettingsTextField(title: String(localized: "sources.field.password"), text: $basicAuthPassword, isSecure: true)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.username")) {
TextField("", text: $basicAuthUsername)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.password")) {
SecureField("", text: $basicAuthPassword)
.textContentType(.password)
}
#else
TextField(String(localized: "sources.field.username"), text: $basicAuthUsername)
.textContentType(.username)
@@ -321,6 +336,9 @@ private struct EditRemoteServerContent: View {
#if os(iOS)
.scrollDismissesKeyboard(.interactively)
#endif
#if os(macOS)
.formStyle(.grouped)
#endif
.confirmationDialog(
String(localized: "sources.delete.confirmation.single \(instance.displayName)"),
isPresented: $showingDeleteConfirmation,
@@ -564,6 +582,11 @@ private struct EditFileSourceContent: View {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.name"), text: $name)
TVSettingsToggle(title: String(localized: "sources.field.enabled"), isOn: $isEnabled)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.name")) {
TextField("", text: $name)
}
Toggle(String(localized: "sources.field.enabled"), isOn: $isEnabled)
#else
TextField(String(localized: "sources.field.name"), text: $name)
Toggle(String(localized: "sources.field.enabled"), isOn: $isEnabled)
@@ -615,6 +638,21 @@ private struct EditFileSourceContent: View {
text: $password,
isSecure: true
)
#elseif os(macOS)
LabeledContent(String(localized: "sources.field.username")) {
TextField("", text: $username)
.textContentType(.username)
.autocorrectionDisabled()
}
LabeledContent(String(localized: "sources.field.password")) {
SecureField(
hasExistingPassword
? String(localized: "sources.field.passwordKeep")
: String(localized: "sources.field.passwordRequired"),
text: $password
)
.textContentType(.password)
}
#else
TextField(String(localized: "sources.field.username"), text: $username)
.textContentType(.username)
@@ -718,6 +756,9 @@ private struct EditFileSourceContent: View {
#if os(iOS)
.scrollDismissesKeyboard(.interactively)
#endif
#if os(macOS)
.formStyle(.grouped)
#endif
.confirmationDialog(
String(localized: "sources.delete.confirmation.single \(source.name)"),
isPresented: $showingDeleteConfirmation,