diff --git a/Yattee/Views/Settings/AddSource/AddLocalFolderView.swift b/Yattee/Views/Settings/AddSource/AddLocalFolderView.swift index 64b7fd28..5f8d6102 100644 --- a/Yattee/Views/Settings/AddSource/AddLocalFolderView.swift +++ b/Yattee/Views/Settings/AddSource/AddLocalFolderView.swift @@ -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 { diff --git a/Yattee/Views/Settings/AddSource/AddRemoteServerView.swift b/Yattee/Views/Settings/AddSource/AddRemoteServerView.swift index 8d31e80c..187a22f6 100644 --- a/Yattee/Views/Settings/AddSource/AddRemoteServerView.swift +++ b/Yattee/Views/Settings/AddSource/AddRemoteServerView.swift @@ -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) diff --git a/Yattee/Views/Settings/AddSource/AddSMBView.swift b/Yattee/Views/Settings/AddSource/AddSMBView.swift index 15d0b30a..3cd5ffb7 100644 --- a/Yattee/Views/Settings/AddSource/AddSMBView.swift +++ b/Yattee/Views/Settings/AddSource/AddSMBView.swift @@ -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 { diff --git a/Yattee/Views/Settings/AddSource/AddWebDAVView.swift b/Yattee/Views/Settings/AddSource/AddWebDAVView.swift index e137e543..3211a568 100644 --- a/Yattee/Views/Settings/AddSource/AddWebDAVView.swift +++ b/Yattee/Views/Settings/AddSource/AddWebDAVView.swift @@ -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 { diff --git a/Yattee/Views/Settings/AddSourceView.swift b/Yattee/Views/Settings/AddSourceView.swift index 221ef84b..57cb7d6c 100644 --- a/Yattee/Views/Settings/AddSourceView.swift +++ b/Yattee/Views/Settings/AddSourceView.swift @@ -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 diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index e2d20c7c..3b2d2478 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -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,