Files
yattee/Yattee/Views/Settings/AddSource/AddWebDAVView.swift
Arkadiusz Fal 508069cecf 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.
2026-04-20 20:51:24 +02:00

325 lines
10 KiB
Swift

//
// AddWebDAVView.swift
// Yattee
//
// View for adding a WebDAV share as a media source.
//
import SwiftUI
struct AddWebDAVView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.appEnvironment) private var appEnvironment
// MARK: - State
@State private var name = ""
@State private var urlString = ""
@State private var username = ""
@State private var password = ""
@State private var allowInvalidCertificates = false
@State private var isTesting = false
@State private var testResult: SourceTestResult?
@State private var testProgress: String?
// Pre-filled from network discovery
var prefillURL: URL?
var prefillName: String?
var prefillAllowInvalidCertificates: Bool = false
// Closure to dismiss the parent sheet
var dismissSheet: DismissAction?
// MARK: - Computed Properties
private var canAdd: Bool {
!name.isEmpty && !urlString.isEmpty && URL(string: urlString) != nil
}
// MARK: - Body
var body: some View {
#if os(macOS)
macOSBody
#else
Form {
nameSection
serverSection
authSection
securitySection
if let result = testResult {
SourceTestResultSection(result: result)
}
actionSection
}
#if os(iOS)
.scrollDismissesKeyboard(.interactively)
#endif
#if !os(tvOS)
.navigationTitle(String(localized: "sources.addWebDAV"))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
#endif
.onAppear {
if let url = prefillURL {
urlString = url.absoluteString
}
if let prefillName, name.isEmpty {
name = prefillName
}
if prefillAllowInvalidCertificates {
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 {
Section {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.name"), text: $name)
#else
TextField(String(localized: "sources.field.name"), text: $name)
#endif
} footer: {
Text(String(localized: "sources.footer.displayName"))
}
}
private var serverSection: some View {
Section {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.placeholder.webdavUrl"), text: $urlString)
#else
TextField(String(localized: "sources.placeholder.webdavUrl"), text: $urlString)
.textContentType(.URL)
#if os(iOS)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled()
#endif
} footer: {
Text(String(localized: "sources.footer.webdav"))
}
}
private var authSection: some View {
Section {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.usernameOptional"), text: $username)
TVSettingsTextField(title: String(localized: "sources.field.passwordOptional"), text: $password, isSecure: true)
#else
TextField(String(localized: "sources.field.usernameOptional"), text: $username)
.textContentType(.username)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled()
SecureField(String(localized: "sources.field.passwordOptional"), text: $password)
.textContentType(.password)
#endif
} header: {
Text(String(localized: "sources.header.auth"))
} footer: {
Text(String(localized: "sources.footer.auth"))
}
}
private var securitySection: some View {
Section {
#if os(tvOS)
TVSettingsToggle(
title: String(localized: "sources.field.allowInvalidCertificates"),
isOn: $allowInvalidCertificates
)
#else
Toggle(String(localized: "sources.field.allowInvalidCertificates"), isOn: $allowInvalidCertificates)
#endif
} header: {
Text(String(localized: "sources.header.security"))
} footer: {
Text(String(localized: "sources.footer.allowInvalidCertificates"))
}
}
private var actionSection: some View {
Section {
Button {
addSource()
} label: {
if isTesting {
HStack {
ProgressView()
.controlSize(.small)
Text(testProgress ?? String(localized: "sources.testing"))
}
} else {
Text(String(localized: "sources.addSource"))
}
}
.disabled(!canAdd || isTesting)
#if os(tvOS)
.buttonStyle(TVSettingsButtonStyle())
#endif
}
}
// MARK: - Actions
private func addSource() {
guard let appEnvironment,
let url = URL(string: urlString) else { return }
isTesting = true
testResult = nil
testProgress = String(localized: "sources.testing.connecting")
let source = MediaSource.webdav(
name: name,
url: url,
username: username.isEmpty ? nil : username,
allowInvalidCertificates: allowInvalidCertificates
)
let webDAVClient = appEnvironment.webDAVClientFactory.createClient(for: source)
Task {
do {
_ = try await webDAVClient.testConnection(
source: source,
password: password.isEmpty ? nil : password
)
await MainActor.run {
if !password.isEmpty {
appEnvironment.mediaSourcesManager.setPassword(password, for: source)
}
appEnvironment.mediaSourcesManager.add(source)
isTesting = false
testProgress = nil
if let dismissSheet {
dismissSheet()
} else {
dismiss()
}
}
} catch {
await MainActor.run {
isTesting = false
testProgress = nil
testResult = .failure(error.localizedDescription)
}
}
}
}
}
// MARK: - Preview
#Preview {
NavigationStack {
AddWebDAVView()
.appEnvironment(.preview)
}
}