diff --git a/ApplicationLibrary/Views/Abstract/TVToolbarButton.swift b/ApplicationLibrary/Views/Abstract/TVToolbarButton.swift index a0a55cc..b280201 100644 --- a/ApplicationLibrary/Views/Abstract/TVToolbarButton.swift +++ b/ApplicationLibrary/Views/Abstract/TVToolbarButton.swift @@ -6,11 +6,13 @@ /// Using UIButton via UIViewRepresentable bypasses this issue. struct TVToolbarButton: UIViewRepresentable { let title: String + var isEnabled: Bool = true let action: () -> Void func makeUIView(context: Context) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) + button.isEnabled = isEnabled button.setContentHuggingPriority(.required, for: .horizontal) button.setContentCompressionResistancePriority(.required, for: .horizontal) button.addTarget(context.coordinator, action: #selector(Coordinator.buttonTapped), for: .primaryActionTriggered) @@ -19,6 +21,7 @@ func updateUIView(_ uiView: UIButton, context: Context) { uiView.setTitle(title, for: .normal) + uiView.isEnabled = isEnabled uiView.invalidateIntrinsicContentSize() uiView.sizeToFit() context.coordinator.action = action diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift index 9b41da4..da85386 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift @@ -1,109 +1,129 @@ -#if os(iOS) || os(macOS) - import Foundation - import Library - import SwiftUI +import Foundation +import Library +import SwiftUI - @MainActor - public struct EditProfileContentView: View { - public struct Context: Codable, Hashable { - public let profileID: Int64 - public let readOnly: Bool +@MainActor +public struct EditProfileContentView: View { + public struct Context: Codable, Hashable { + public let profileID: Int64 + public let readOnly: Bool + } + + private let readOnly: Bool + @StateObject private var viewModel: EditProfileContentViewModel + + public init(_ context: Context?) { + readOnly = context?.readOnly == true + _viewModel = StateObject(wrappedValue: EditProfileContentViewModel(profileID: context?.profileID)) + } + + @Environment(\.dismiss) private var dismiss + @Environment(\.profileEditor) private var profileEditor + + public var body: some View { + Group { + if viewModel.isLoading { + ProgressView().onAppear { + Task { + await viewModel.loadContent() + } + } + } else { + editorView + .onChangeCompat(of: viewModel.profileContent) { + viewModel.markAsChanged() + } + } } - - private let readOnly: Bool - @StateObject private var viewModel: EditProfileContentViewModel - - public init(_ context: Context?) { - readOnly = context?.readOnly == true - _viewModel = StateObject(wrappedValue: EditProfileContentViewModel(profileID: context?.profileID)) - } - - @Environment(\.dismiss) private var dismiss - @Environment(\.profileEditor) private var profileEditor - - public var body: some View { - Group { - if viewModel.isLoading { - ProgressView().onAppear { - Task { - await viewModel.loadContent() + .alert($viewModel.alert) + .navigationTitle(navigationTitle) + #if os(macOS) + .toolbar { + ToolbarItemGroup(placement: .navigation) { + if !readOnly { + Button { + Task { + await viewModel.saveContent() + } + } label: { + Label("Save", image: "save") + } + .disabled(!viewModel.isChanged) + } else { + Button { + NSPasteboard.general.setString(viewModel.profileContent, forType: .fileContents) + } label: { + Label("Copy", systemImage: "clipboard.fill") } } - } else { - editorView - .onChangeCompat(of: viewModel.profileContent) { - viewModel.markAsChanged() - } } } - .alert($viewModel.alert) - .navigationTitle(navigationTitle) + #elseif os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if !readOnly { + Button("Save") { + Task { + await viewModel.saveContent() + } + }.disabled(!viewModel.isChanged) + } else { + Button("Copy") { + UIPasteboard.general.string = viewModel.profileContent + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + #elseif os(tvOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if !readOnly { + TVToolbarButton(title: String(localized: "Save"), isEnabled: viewModel.isChanged) { + Task { + await viewModel.saveContent() + } + } + } + } + } + #endif + } + + private var navigationTitle: String { + if readOnly { + return String(localized: "View Content") + } else { + return String(localized: "Edit Content") + } + } + + @ViewBuilder + private var editorView: some View { + if let profileEditor { + profileEditor( + readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, + !readOnly + ) #if os(macOS) - .toolbar { - ToolbarItemGroup(placement: .navigation) { - if !readOnly { - Button { - Task { - await viewModel.saveContent() - } - } label: { - Label("Save", image: "save") - } - .disabled(!viewModel.isChanged) - } else { - Button { - NSPasteboard.general.setString(viewModel.profileContent, forType: .fileContents) - } label: { - Label("Copy", systemImage: "clipboard.fill") - } - } - } - } - #elseif os(iOS) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - if !readOnly { - Button("Save") { - Task { - await viewModel.saveContent() - } - }.disabled(!viewModel.isChanged) - } else { - Button("Copy") { - UIPasteboard.general.string = viewModel.profileContent - } - } - } - } - .navigationBarTitleDisplayMode(.inline) + .frame(maxWidth: .infinity, maxHeight: .infinity) #endif + } else { + defaultEditorView } + } - private var navigationTitle: String { - if readOnly { - return String(localized: "View Content") - } else { - return String(localized: "Edit Content") + @ViewBuilder + private var defaultEditorView: some View { + #if os(tvOS) + ScrollView { + TextField("", text: readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, axis: .vertical) + .lineLimit(1000) + .font(Font.system(.caption2, design: .monospaced)) + .autocorrectionDisabled(true) + .disabled(readOnly) } - } - - @ViewBuilder - private var editorView: some View { - if let profileEditor { - profileEditor( - readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, - !readOnly - ) - #if os(macOS) - .frame(maxWidth: .infinity, maxHeight: .infinity) - #endif - } else { - defaultEditorView - } - } - - @ViewBuilder - private var defaultEditorView: some View { + #else Group { if readOnly { TextEditor(text: .constant(viewModel.profileContent)) @@ -117,7 +137,6 @@ .textContentType(.init(rawValue: "")) .padding() #endif - } + #endif } - -#endif +} diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift index 1bcbfd4..887091a 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift @@ -1,74 +1,71 @@ -#if os(iOS) || os(macOS) - import Foundation - import Library - import SwiftUI +import Foundation +import Library +import SwiftUI - @MainActor - public final class EditProfileContentViewModel: BaseViewModel { - @Published public var profile: Profile? - @Published public var profileContent = "" - @Published public var isChanged = false +@MainActor +public final class EditProfileContentViewModel: BaseViewModel { + @Published public var profile: Profile? + @Published public var profileContent = "" + @Published public var isChanged = false - private let profileID: Int64? + private let profileID: Int64? - public init(profileID: Int64?) { - self.profileID = profileID - super.init() - isLoading = true + public init(profileID: Int64?) { + self.profileID = profileID + super.init() + isLoading = true + } + + public func markAsChanged() { + isChanged = true + } + + public func reset() { + isLoading = true + profile = nil + profileContent = "" + isChanged = false + alert = nil + } + + public func loadContent() async { + do { + try await loadContentBackground() + } catch { + alert = AlertState(error: error) } + isLoading = false + } - public func markAsChanged() { - isChanged = true + private nonisolated func loadContentBackground() async throws { + guard let profileID else { + throw NSError(domain: "Context destroyed", code: 0) } - - public func reset() { - isLoading = true - profile = nil - profileContent = "" - isChanged = false - alert = nil + guard let profile = try await ProfileManager.get(profileID) else { + throw NSError(domain: "Profile missing", code: 0) } - - public func loadContent() async { - do { - try await loadContentBackground() - } catch { - alert = AlertState(error: error) - } - isLoading = false - } - - private nonisolated func loadContentBackground() async throws { - guard let profileID else { - throw NSError(domain: "Context destroyed", code: 0) - } - guard let profile = try await ProfileManager.get(profileID) else { - throw NSError(domain: "Profile missing", code: 0) - } - let profileContent = try profile.read() - await MainActor.run { - self.profile = profile - self.profileContent = profileContent - } - } - - public func saveContent() async { - guard let profile else { - return - } - do { - try await saveContentBackground(profile) - } catch { - alert = AlertState(error: error) - return - } - isChanged = false - } - - private nonisolated func saveContentBackground(_ profile: Profile) async throws { - let profileContent = await profileContent - try profile.write(profileContent) + let profileContent = try profile.read() + await MainActor.run { + self.profile = profile + self.profileContent = profileContent } } -#endif + public func saveContent() async { + guard let profile else { + return + } + do { + try await saveContentBackground(profile) + } catch { + alert = AlertState(error: error) + return + } + isChanged = false + } + + private nonisolated func saveContentBackground(_ profile: Profile) async throws { + let profileContent = await profileContent + try profile.write(profileContent) + } +} diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index 641c037..528e127 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -51,8 +51,8 @@ public struct NewProfileView: View { .multilineTextAlignment(.trailing) } Picker(selection: $viewModel.profileType) { + Text("Local").tag(ProfileType.local) #if !os(tvOS) - Text("Local").tag(ProfileType.local) Text("iCloud").tag(ProfileType.icloud) #endif Text("Remote").tag(ProfileType.remote) @@ -190,6 +190,11 @@ public struct NewProfileView: View { .navigationTitle("New Profile") .disabled(viewModel.isSaving) .alert($viewModel.alert) + .onChangeCompat(of: viewModel.createSucceeded) { newValue in + if newValue { + dismiss() + } + } #if os(iOS) .fileImporter( isPresented: $viewModel.pickerPresented, diff --git a/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift b/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift index 3742a82..c47cba2 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift @@ -6,6 +6,7 @@ import SwiftUI @MainActor public final class NewProfileViewModel: BaseViewModel { @Published public var isSaving = false + @Published public var createSucceeded = false @Published public var profileName = "" #if !os(tvOS) @Published public var profileType = ProfileType.local @@ -81,6 +82,7 @@ public final class NewProfileViewModel: BaseViewModel { if sendUpdateNotification { environments.profileUpdate.send() } + createSucceeded = true dismiss?() #if os(macOS) diff --git a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift index b537329..a2aea89 100644 --- a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift +++ b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift @@ -18,16 +18,14 @@ public struct ProfileActionToolbar: View { } public var body: some View { - #if os(iOS) + #if os(iOS) || os(tvOS) iosBody - #elseif os(tvOS) - tvOSBody #elseif os(macOS) macOSBody #endif } - #if os(iOS) + #if os(iOS) || os(tvOS) private var iosBody: some View { Section("Action") { if profile.type != .remote { @@ -49,12 +47,6 @@ public struct ProfileActionToolbar: View { } #endif - #if os(tvOS) - private var tvOSBody: some View { - EmptyView() - } - #endif - #if os(macOS) private var macOSBody: some View { VStack(spacing: 0) { diff --git a/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift b/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift index fbd3e23..cb23002 100644 --- a/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift +++ b/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift @@ -1,14 +1,12 @@ -#if os(iOS) || os(macOS) - import SwiftUI +import SwiftUI - public struct ProfileEditorKey: EnvironmentKey { - public static let defaultValue: ((Binding, Bool) -> AnyView)? = nil - } +public struct ProfileEditorKey: EnvironmentKey { + public static let defaultValue: ((Binding, Bool) -> AnyView)? = nil +} - public extension EnvironmentValues { - var profileEditor: ((Binding, Bool) -> AnyView)? { - get { self[ProfileEditorKey.self] } - set { self[ProfileEditorKey.self] = newValue } - } +public extension EnvironmentValues { + var profileEditor: ((Binding, Bool) -> AnyView)? { + get { self[ProfileEditorKey.self] } + set { self[ProfileEditorKey.self] = newValue } } -#endif +}