From 1b7c6fb7ce9440e657d1156de40af2dbfe8d4036 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Wed, 27 Nov 2024 09:09:14 +0100 Subject: [PATCH] refactor: proper placement of toolbar and other improvements (#33) --- Sources/ContentView.swift | 12 +- Sources/Library/Modifiers/ErrorAlert.swift | 42 ------- Sources/Library/Modifiers/ErrorSheet.swift | 12 +- Sources/Library/Modifiers/WarningSheet.swift | 12 +- .../Screens/Account/AccountEditEmail.swift | 71 +++++------ .../Screens/Account/AccountEditFullName.swift | 67 +++++----- .../Screens/Account/AccountEditPassword.swift | 13 +- Sources/Screens/Account/AccountSettings.swift | 3 +- Sources/Screens/Account/AccountStore.swift | 3 +- Sources/Screens/Browser/BrowserStore.swift | 2 +- Sources/Screens/File/FileRename.swift | 13 +- Sources/Screens/File/FileStore.swift | 12 +- Sources/Screens/File/FolderCreate.swift | 5 +- Sources/Screens/Group/GroupCreate.swift | 5 +- Sources/Screens/Group/GroupEditName.swift | 55 +++++---- Sources/Screens/Group/GroupList.swift | 96 +++++++-------- Sources/Screens/Group/GroupMemberAdd.swift | 12 +- Sources/Screens/Group/GroupMemberList.swift | 24 ++-- Sources/Screens/Group/GroupSettings.swift | 4 +- Sources/Screens/Group/GroupStore.swift | 20 +-- Sources/Screens/Insights/InsightsChart.swift | 33 +++-- Sources/Screens/Insights/InsightsCreate.swift | 92 +++++++------- .../Screens/Insights/InsightsEntityList.swift | 64 +++++----- .../Screens/Insights/InsightsSettings.swift | 8 +- Sources/Screens/Insights/InsightsStore.swift | 20 +-- .../Screens/Invitation/InvitationCreate.swift | 13 +- .../Invitation/InvitationOutgoingList.swift | 6 +- .../Invitation/InvitationOverview.swift | 9 +- .../Screens/Invitation/InvitationStore.swift | 32 ++--- Sources/Screens/Mosaic/MosaicCreate.swift | 5 +- Sources/Screens/Mosaic/MosaicSettings.swift | 8 +- Sources/Screens/Mosaic/MosaicStore.swift | 12 +- .../Organization/OrganizationCreate.swift | 6 +- .../Organization/OrganizationEditName.swift | 55 +++++---- .../Organization/OrganizationList.swift | 99 +++++++-------- .../Organization/OrganizationSelector.swift | 66 +++++----- .../Organization/OrganizationSettings.swift | 4 +- .../Organization/OrganizationStore.swift | 20 +-- Sources/Screens/Server/ServerCreate.swift | 14 +-- Sources/Screens/Server/ServerOverview.swift | 12 +- .../Sharing/SharingGroupPermission.swift | 6 +- .../Sharing/SharingUserPermission.swift | 8 +- Sources/Screens/SignIn/SignIn.swift | 62 +++++----- Sources/Screens/Snapshot/SnapshotList.swift | 70 +++++------ .../Screens/Snapshot/SnapshotOverview.swift | 6 +- Sources/Screens/Snapshot/SnapshotStore.swift | 20 +-- Sources/Screens/Task/TaskList.swift | 90 +++++++------- Sources/Screens/Task/TaskStore.swift | 20 +-- Sources/Screens/User/UserSelector.swift | 14 +-- Sources/Screens/User/UserStore.swift | 20 +-- .../Screens/Workspace/WorkspaceCreate.swift | 5 +- .../Screens/Workspace/WorkspaceEditName.swift | 55 +++++---- .../WorkspaceEditStorageCapacity.swift | 56 ++++----- Sources/Screens/Workspace/WorkspaceList.swift | 114 +++++++++--------- .../Screens/Workspace/WorkspaceSettings.swift | 4 +- .../Screens/Workspace/WorkspaceStore.swift | 45 ++++--- 56 files changed, 849 insertions(+), 807 deletions(-) delete mode 100644 Sources/Library/Modifiers/ErrorAlert.swift diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 14b1e4b..8d617a3 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -18,7 +18,7 @@ struct ContentView: View { @Environment(\.modelContext) private var context @Query private var servers: [Server] @State private var timer: Timer? - @State private var showSignIn = false + @State private var signInIsPresented = false var body: some View { MainView() @@ -27,12 +27,12 @@ struct ContentView: View { if token.isExpired { tokenStore.token = nil tokenStore.deleteFromKeychain() - showSignIn = true + signInIsPresented = true } else { tokenStore.token = token } } else { - showSignIn = true + signInIsPresented = true } startTokenTimer() @@ -52,15 +52,15 @@ struct ContentView: View { // itself unexpectedly without user interaction or a direct code-triggered dismissal. Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in DispatchQueue.main.async { - showSignIn = true + signInIsPresented = true } } } } - .fullScreenCover(isPresented: $showSignIn) { + .fullScreenCover(isPresented: $signInIsPresented) { SignIn { startTokenTimer() - showSignIn = false + signInIsPresented = false } } } diff --git a/Sources/Library/Modifiers/ErrorAlert.swift b/Sources/Library/Modifiers/ErrorAlert.swift deleted file mode 100644 index 6e5600c..0000000 --- a/Sources/Library/Modifiers/ErrorAlert.swift +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2024 Anass Bouassaba. -// -// Use of this software is governed by the Business Source License -// included in the file LICENSE in the root of this repository. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the GNU Affero General Public License v3.0 only, included in the file -// AGPL-3.0-only in the root of this repository. - -import SwiftUI - -struct VOErrorAlert: ViewModifier { - @EnvironmentObject private var tokenStore: TokenStore - private let isPresented: Binding - private let title: String? - private let message: String? - - init(isPresented: Binding, title: String? = nil, message: String? = nil) { - self.isPresented = isPresented - self.title = title - self.message = message - } - - func body(content: Content) -> some View { - content - .alert(title ?? "Error", isPresented: isPresented) { - Button("Sign Out", role: .destructive) { - tokenStore.token = nil - tokenStore.deleteFromKeychain() - } - } message: { - Text(message ?? "Unexpected error occurred.") - } - } -} - -extension View { - func voErrorAlert(isPresented: Binding, title: String? = nil, message: String? = nil) -> some View { - modifier(VOErrorAlert(isPresented: isPresented, title: title, message: message)) - } -} diff --git a/Sources/Library/Modifiers/ErrorSheet.swift b/Sources/Library/Modifiers/ErrorSheet.swift index 836a5b7..d452823 100644 --- a/Sources/Library/Modifiers/ErrorSheet.swift +++ b/Sources/Library/Modifiers/ErrorSheet.swift @@ -45,23 +45,23 @@ extension View { } #Preview { - @Previewable @State var showError = false - @Previewable @State var showLongError = false + @Previewable @State var errorIsPresented = false + @Previewable @State var longErrorIsPresented = false VStack(spacing: VOMetrics.spacing) { Button("Show Error") { - showError = true + errorIsPresented = true } Button("Show Long Error") { - showLongError = true + longErrorIsPresented = true } } .voErrorSheet( - isPresented: $showError, + isPresented: $errorIsPresented, message: "Lorem ipsum dolor sit amet." ) .voErrorSheet( - isPresented: $showLongError, + isPresented: $longErrorIsPresented, message: // swiftlint:disable:next line_length "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." diff --git a/Sources/Library/Modifiers/WarningSheet.swift b/Sources/Library/Modifiers/WarningSheet.swift index a5873a6..0487d71 100644 --- a/Sources/Library/Modifiers/WarningSheet.swift +++ b/Sources/Library/Modifiers/WarningSheet.swift @@ -45,23 +45,23 @@ extension View { } #Preview { - @Previewable @State var showWarning = false - @Previewable @State var showLongWarning = false + @Previewable @State var warningIsPresented = false + @Previewable @State var longWarningIsPresented = false VStack(spacing: VOMetrics.spacing) { Button("Show Warning") { - showWarning = true + warningIsPresented = true } Button("Show Long Warning") { - showLongWarning = true + longWarningIsPresented = true } } .voWarningSheet( - isPresented: $showWarning, + isPresented: $warningIsPresented, message: "Lorem ipsum dolor sit amet." ) .voWarningSheet( - isPresented: $showLongWarning, + isPresented: $longWarningIsPresented, message: // swiftlint:disable:next line_length "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." diff --git a/Sources/Screens/Account/AccountEditEmail.swift b/Sources/Screens/Account/AccountEditEmail.swift index 5f78c96..4f43bf4 100644 --- a/Sources/Screens/Account/AccountEditEmail.swift +++ b/Sources/Screens/Account/AccountEditEmail.swift @@ -15,50 +15,52 @@ struct AccountEditEmail: View, LoadStateProvider, FormValidatable, ErrorPresenta @ObservedObject private var accountStore: AccountStore @Environment(\.dismiss) private var dismiss @State private var value = "" - @State private var isSaving = false + @State private var isProcessing = false init(accountStore: AccountStore) { self.accountStore = accountStore } var body: some View { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let identityUser = accountStore.identityUser { - Form { - TextField("Email", text: $value) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .disabled(isSaving) - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Email") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let identityUser = accountStore.identityUser { + Form { + TextField("Email", text: $value) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .disabled(isProcessing) + } + .onAppear { + value = identityUser.pendingEmail ?? identityUser.email } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = identityUser.pendingEmail ?? identityUser.email - } - .onChange(of: accountStore.identityUser) { _, newUser in - if let newUser { - value = newUser.pendingEmail ?? newUser.email + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Email") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } } + .onChange(of: accountStore.identityUser) { _, newUser in + if let newUser { + value = newUser.pendingEmail ?? newUser.email + } + } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedValue: String { @@ -66,17 +68,18 @@ struct AccountEditEmail: View, LoadStateProvider, FormValidatable, ErrorPresenta } private func performSave() { - isSaving = true withErrorHandling { _ = try await accountStore.updateEmail(normalizedValue) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Account/AccountEditFullName.swift b/Sources/Screens/Account/AccountEditFullName.swift index e56a6e9..149701a 100644 --- a/Sources/Screens/Account/AccountEditFullName.swift +++ b/Sources/Screens/Account/AccountEditFullName.swift @@ -15,48 +15,50 @@ struct AccountEditFullName: View, LoadStateProvider, FormValidatable, ErrorPrese @ObservedObject private var accountStore: AccountStore @Environment(\.dismiss) private var dismiss @State private var value = "" - @State private var isSaving = false + @State private var isProcessing = false init(accountStore: AccountStore) { self.accountStore = accountStore } var body: some View { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let identityUser = accountStore.identityUser { - Form { - TextField("Full Name", text: $value) - .disabled(isSaving) - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Full Name") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let identityUser = accountStore.identityUser { + Form { + TextField("Full Name", text: $value) + .disabled(isProcessing) + } + .onAppear { + value = identityUser.fullName } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = identityUser.fullName - } - .onChange(of: accountStore.identityUser) { _, newUser in - if let newUser { - value = newUser.fullName + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Full Name") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } } + .onChange(of: accountStore.identityUser) { _, newUser in + if let newUser { + value = newUser.fullName + } + } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedValue: String { @@ -64,17 +66,18 @@ struct AccountEditFullName: View, LoadStateProvider, FormValidatable, ErrorPrese } private func performSave() { - isSaving = true withErrorHandling { _ = try await accountStore.updateFullName(normalizedValue) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Account/AccountEditPassword.swift b/Sources/Screens/Account/AccountEditPassword.swift index f9031e1..ab4b7df 100644 --- a/Sources/Screens/Account/AccountEditPassword.swift +++ b/Sources/Screens/Account/AccountEditPassword.swift @@ -16,7 +16,7 @@ struct AccountEditPassword: View, FormValidatable, ErrorPresentable { @Environment(\.dismiss) private var dismiss @State private var currentValue = "" @State private var newValue = "" - @State private var isSaving = false + @State private var isProcessing = false init(accountStore: AccountStore) { self.accountStore = accountStore @@ -25,15 +25,15 @@ struct AccountEditPassword: View, FormValidatable, ErrorPresentable { var body: some View { Form { SecureField("Current Password", text: $currentValue) - .disabled(isSaving) + .disabled(isProcessing) SecureField("New Password", text: $newValue) - .disabled(isSaving) + .disabled(isProcessing) } .navigationBarTitleDisplayMode(.inline) .navigationTitle("Change Password") .toolbar { ToolbarItem(placement: .topBarTrailing) { - if isSaving { + if isProcessing { ProgressView() } else { Button("Save") { @@ -47,17 +47,18 @@ struct AccountEditPassword: View, FormValidatable, ErrorPresentable { } private func performSave() { - isSaving = true withErrorHandling { _ = try await accountStore.updatePassword(current: currentValue, new: newValue) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Account/AccountSettings.swift b/Sources/Screens/Account/AccountSettings.swift index 20ddcb9..f40dffa 100644 --- a/Sources/Screens/Account/AccountSettings.swift +++ b/Sources/Screens/Account/AccountSettings.swift @@ -125,10 +125,11 @@ struct AccountSettings: View, ViewDataProvider, LoadStateProvider, ErrorPresenta } private func performDelete() { - isDeleting = true withErrorHandling { try await accountStore.deleteAccount(password: password) return true + } before: { + isDeleting = true } success: { dismiss() onDelete?() diff --git a/Sources/Screens/Account/AccountStore.swift b/Sources/Screens/Account/AccountStore.swift index 5eb196f..869a018 100644 --- a/Sources/Screens/Account/AccountStore.swift +++ b/Sources/Screens/Account/AccountStore.swift @@ -19,7 +19,7 @@ class AccountStore: ObservableObject { @Published var storageUsageError: String? @Published var storageUsageIsLoading: Bool = false private var timer: Timer? - private var accountClient: VOAccount? + private var accountClient: VOAccount = .init(baseURL: Config.production.idpURL) private var identityUserClient: VOIdentityUser? private var storageClient: VOStorage? var tokenStore: TokenStore? @@ -27,7 +27,6 @@ class AccountStore: ObservableObject { var token: VOToken.Value? { didSet { if let token { - accountClient = .init(baseURL: Config.production.idpURL) identityUserClient = .init( baseURL: Config.production.idpURL, accessToken: token.accessToken diff --git a/Sources/Screens/Browser/BrowserStore.swift b/Sources/Screens/Browser/BrowserStore.swift index 0f201e0..3f91834 100644 --- a/Sources/Screens/Browser/BrowserStore.swift +++ b/Sources/Screens/Browser/BrowserStore.swift @@ -176,7 +176,7 @@ class BrowserStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - if let current = self.folder { + if let current = self.folder, self.entities != nil { Task { var size = Constants.pageSize if let list = self.list { diff --git a/Sources/Screens/File/FileRename.swift b/Sources/Screens/File/FileRename.swift index d17051b..18e994a 100644 --- a/Sources/Screens/File/FileRename.swift +++ b/Sources/Screens/File/FileRename.swift @@ -17,7 +17,7 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To @EnvironmentObject private var tokenStore: TokenStore @StateObject private var fileStore = FileStore() @Environment(\.dismiss) private var dismiss - @State private var isSaving = false + @State private var isProcessing = false @State private var value = "" private let file: VOFile.Entity private let onCompletion: ((VOFile.Entity) -> Void)? @@ -38,7 +38,7 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To if !value.isEmpty { Form { TextField("Name", text: $value) - .disabled(isSaving) + .disabled(isProcessing) } } } @@ -52,7 +52,7 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To } } ToolbarItem(placement: .topBarTrailing) { - if isSaving { + if isProcessing { ProgressView() } else { Button("Save") { @@ -63,7 +63,6 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { fileStore.file = file if let token = tokenStore.token { @@ -86,6 +85,7 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To value = file.name } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedValue: String { @@ -94,12 +94,13 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To private func performRename() { guard let file = fileStore.file else { return } - isSaving = true var updatedFile: VOFile.Entity? withErrorHandling { updatedFile = try await fileStore.patchName(file.id, name: normalizedValue) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let updatedFile { @@ -109,7 +110,7 @@ struct FileRename: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, To errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/File/FileStore.swift b/Sources/Screens/File/FileStore.swift index f16f3d2..b52388a 100644 --- a/Sources/Screens/File/FileStore.swift +++ b/Sources/Screens/File/FileStore.swift @@ -354,7 +354,7 @@ class FileStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - if let current = self.file { + if let current = self.file, self.entities != nil { Task { var size = Constants.pageSize if let list = self.list { @@ -378,10 +378,12 @@ class FileStore: ObservableObject { } } } - Task { - let taskCount = try await self.fetchTaskCount() - DispatchQueue.main.async { - self.taskCount = taskCount + if self.taskCount != nil { + Task { + let taskCount = try await self.fetchTaskCount() + DispatchQueue.main.async { + self.taskCount = taskCount + } } } } diff --git a/Sources/Screens/File/FolderCreate.swift b/Sources/Screens/File/FolderCreate.swift index f74c4ae..1b13d81 100644 --- a/Sources/Screens/File/FolderCreate.swift +++ b/Sources/Screens/File/FolderCreate.swift @@ -50,8 +50,8 @@ struct FolderCreate: View, ErrorPresentable, FormValidatable { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedName: String { @@ -59,7 +59,6 @@ struct FolderCreate: View, ErrorPresentable, FormValidatable { } private func performCreate() { - isProcessing = true withErrorHandling { _ = try await fileStore.createFolder( name: normalizedName, @@ -70,6 +69,8 @@ struct FolderCreate: View, ErrorPresentable, FormValidatable { fileStore.fetchNextPage() } return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Group/GroupCreate.swift b/Sources/Screens/Group/GroupCreate.swift index 4a41aac..ca14610 100644 --- a/Sources/Screens/Group/GroupCreate.swift +++ b/Sources/Screens/Group/GroupCreate.swift @@ -66,8 +66,8 @@ struct GroupCreate: View, FormValidatable, ErrorPresentable { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedName: String { @@ -76,12 +76,13 @@ struct GroupCreate: View, FormValidatable, ErrorPresentable { private func performCreate() { guard let organization else { return } - isProcessing = true var group: VOGroup.Entity? withErrorHandling { group = try await groupStore.create(name: normalizedName, organization: organization) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let group { diff --git a/Sources/Screens/Group/GroupEditName.swift b/Sources/Screens/Group/GroupEditName.swift index 09e872b..1948fad 100644 --- a/Sources/Screens/Group/GroupEditName.swift +++ b/Sources/Screens/Group/GroupEditName.swift @@ -15,7 +15,7 @@ struct GroupEditName: View, FormValidatable, ErrorPresentable { @ObservedObject private var groupStore: GroupStore @Environment(\.dismiss) private var dismiss @State private var value = "" - @State private var isSaving = false + @State private var isProcessing = false private let onCompletion: ((VOGroup.Entity) -> Void)? init(groupStore: GroupStore, onCompletion: ((VOGroup.Entity) -> Void)? = nil) { @@ -24,35 +24,37 @@ struct GroupEditName: View, FormValidatable, ErrorPresentable { } var body: some View { - if let current = groupStore.current { - Form { - TextField("Name", text: $value) - .disabled(isSaving) + VStack { + if let current = groupStore.current { + Form { + TextField("Name", text: $value) + .disabled(isProcessing) + } + .onAppear { + value = current.name + } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Name") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Name") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = current.name - } - .onChange(of: groupStore.current) { _, newCurrent in - if let newCurrent { - value = newCurrent.name - } + } + .onChange(of: groupStore.current) { _, newCurrent in + if let newCurrent { + value = newCurrent.name } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedValue: String { @@ -61,12 +63,13 @@ struct GroupEditName: View, FormValidatable, ErrorPresentable { private func performSave() { guard let current = groupStore.current else { return } - isSaving = true var updatedGroup: VOGroup.Entity? withErrorHandling { updatedGroup = try await groupStore.patchName(current.id, name: value) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let updatedGroup { @@ -76,7 +79,7 @@ struct GroupEditName: View, FormValidatable, ErrorPresentable { errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Group/GroupList.swift b/Sources/Screens/Group/GroupList.swift index 3d1f7a6..b7aa846 100644 --- a/Sources/Screens/Group/GroupList.swift +++ b/Sources/Screens/Group/GroupList.swift @@ -22,65 +22,67 @@ struct GroupList: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, Tok var body: some View { NavigationStack { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = groupStore.entities { - Group { - if entities.count == 0 { - Text("There are no groups.") - } else { - List { - ForEach(entities, id: \.id) { group in - NavigationLink { - GroupOverview(group, groupStore: groupStore) - } label: { - GroupRow(group) - .onAppear { - onListItemAppear(group.id) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = groupStore.entities { + Group { + if entities.count == 0 { + Text("There are no groups.") + } else { + List { + ForEach(entities, id: \.id) { group in + NavigationLink { + GroupOverview(group, groupStore: groupStore) + } label: { + GroupRow(group) + .onAppear { + onListItemAppear(group.id) + } + } } } } } - } - .navigationTitle("Groups") - .refreshable { - groupStore.fetchNextPage(replace: true) - } - .searchable(text: $searchText) - .onChange(of: searchText) { - groupStore.searchPublisher.send($1) - } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - createIsPresented = true - } label: { - Image(systemName: "plus") - } + .refreshable { + groupStore.fetchNextPage(replace: true) + } + .searchable(text: $searchText) + .onChange(of: searchText) { + groupStore.searchPublisher.send($1) } - ToolbarItem(placement: .topBarLeading) { - if groupStore.entitiesIsLoading { - ProgressView() + .navigationDestination(isPresented: $overviewIsPresented) { + if let newGroup { + GroupOverview(newGroup, groupStore: groupStore) } } } - .sheet(isPresented: $createIsPresented) { - GroupCreate(groupStore: groupStore) { newGroup in - self.newGroup = newGroup - overviewIsPresented = true - } + } + } + .navigationTitle("Groups") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + createIsPresented = true + } label: { + Image(systemName: "plus") } - .navigationDestination(isPresented: $overviewIsPresented) { - if let newGroup { - GroupOverview(newGroup, groupStore: groupStore) - } + } + ToolbarItem(placement: .topBarLeading) { + if groupStore.entitiesIsLoading { + ProgressView() } } } + .sheet(isPresented: $createIsPresented) { + GroupCreate(groupStore: groupStore) { newGroup in + self.newGroup = newGroup + overviewIsPresented = true + } + } } .onAppear { if let token = tokenStore.token { diff --git a/Sources/Screens/Group/GroupMemberAdd.swift b/Sources/Screens/Group/GroupMemberAdd.swift index d98275a..557e825 100644 --- a/Sources/Screens/Group/GroupMemberAdd.swift +++ b/Sources/Screens/Group/GroupMemberAdd.swift @@ -15,7 +15,7 @@ struct GroupMemberAdd: View, FormValidatable, ErrorPresentable { @ObservedObject private var groupStore: GroupStore @Environment(\.dismiss) private var dismiss @State private var user: VOUser.Entity? - @State private var isSaving = false + @State private var isProcessing = false init(groupStore: GroupStore) { self.groupStore = groupStore @@ -54,7 +54,7 @@ struct GroupMemberAdd: View, FormValidatable, ErrorPresentable { } } ToolbarItem(placement: .topBarTrailing) { - if isSaving { + if isProcessing { ProgressView() } else { Button("Add") { @@ -64,24 +64,24 @@ struct GroupMemberAdd: View, FormValidatable, ErrorPresentable { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func performAdd() { guard let user else { return } - isSaving = true - withErrorHandling { try await groupStore.addMember(userID: user.id) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Group/GroupMemberList.swift b/Sources/Screens/Group/GroupMemberList.swift index b57d7c5..e5443de 100644 --- a/Sources/Screens/Group/GroupMemberList.swift +++ b/Sources/Screens/Group/GroupMemberList.swift @@ -59,23 +59,23 @@ struct GroupMemberList: View, ViewDataProvider, LoadStateProvider, TimerLifecycl .onChange(of: searchText) { userStore.searchPublisher.send($1) } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - addMemberIsPresentable = true - } label: { - Image(systemName: "plus") - } - } - } - .sheet(isPresented: $addMemberIsPresentable) { - GroupMemberAdd(groupStore: groupStore) - } } } } .navigationBarTitleDisplayMode(.inline) .navigationTitle("Members") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + addMemberIsPresentable = true + } label: { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $addMemberIsPresentable) { + GroupMemberAdd(groupStore: groupStore) + } .onAppear { if let group = groupStore.current { userStore.groupID = group.id diff --git a/Sources/Screens/Group/GroupSettings.swift b/Sources/Screens/Group/GroupSettings.swift index 2130c3a..eaf84f9 100644 --- a/Sources/Screens/Group/GroupSettings.swift +++ b/Sources/Screens/Group/GroupSettings.swift @@ -71,12 +71,12 @@ struct GroupSettings: View, ErrorPresentable { } private func performDelete() { - isDeleting = true let current = groupStore.current - withErrorHandling { try await groupStore.delete() return true + } before: { + isDeleting = true } success: { dismiss() if let current { diff --git a/Sources/Screens/Group/GroupStore.swift b/Sources/Screens/Group/GroupStore.swift index e46e6a8..ebb1918 100644 --- a/Sources/Screens/Group/GroupStore.swift +++ b/Sources/Screens/Group/GroupStore.swift @@ -200,15 +200,17 @@ class GroupStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/Insights/InsightsChart.swift b/Sources/Screens/Insights/InsightsChart.swift index da358d5..8d6adf9 100644 --- a/Sources/Screens/Insights/InsightsChart.swift +++ b/Sources/Screens/Insights/InsightsChart.swift @@ -25,13 +25,13 @@ struct InsightsChart: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, var body: some View { NavigationView { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = insightsStore.entities { - Group { + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = insightsStore.entities { if entities.count < 5 { Text("Not enough data to render the chart.") } else { @@ -63,17 +63,14 @@ struct InsightsChart: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, .frame(maxWidth: 300, maxHeight: 300) } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Insights") - .refreshable { - insightsStore.fetchEntityNextPage(replace: true) - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { - dismiss() - } - } + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Insights") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() } } } diff --git a/Sources/Screens/Insights/InsightsCreate.swift b/Sources/Screens/Insights/InsightsCreate.swift index fb2a247..007f8d0 100644 --- a/Sources/Screens/Insights/InsightsCreate.swift +++ b/Sources/Screens/Insights/InsightsCreate.swift @@ -26,53 +26,58 @@ struct InsightsCreate: View, ViewDataProvider, LoadStateProvider, TokenDistribut var body: some View { NavigationStack { - if let languages = insightsStore.languages { - VStack { - VStack { - ScrollView { - // swift-format-ignore - // swiftlint:disable:next line_length - Text("Select the language to use for collecting insights. During the process, text will be extracted using OCR (optical character recognition), and entities will be scanned using NER (named entity recognition).") - } - Picker("Language", selection: $language) { - ForEach(languages, id: \.id) { language in - Text(language.name) - .tag(language) + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let languages = insightsStore.languages { + VStack { + VStack { + ScrollView { + // swift-format-ignore + // swiftlint:disable:next line_length + Text("Select the language to use for collecting insights. During the process, text will be extracted using OCR (optical character recognition), and entities will be scanned using NER (named entity recognition).") + } + Picker("Language", selection: $language) { + ForEach(languages, id: \.id) { language in + Text(language.name) + .tag(language) + } + } + .disabled(isCreating) + Button { + performCreate() + } label: { + VOButtonLabel("Collect Insights", isLoading: isCreating) + } + .voPrimaryButton(isDisabled: isCreating || !isValid()) } + .padding() + } + .overlay { + RoundedRectangle(cornerRadius: VOMetrics.borderRadius) + .stroke(Color.borderColor(colorScheme: colorScheme), lineWidth: 1) } - .disabled(isCreating) - Button { - performCreate() - } label: { - VOButtonLabel("Collect Insights", isLoading: isCreating) + .padding(.horizontal) + .modifierIfPad { + $0.padding(.bottom) } - .voPrimaryButton(isDisabled: isCreating || !isValid()) } - .padding() - } - .overlay { - RoundedRectangle(cornerRadius: VOMetrics.borderRadius) - .stroke(Color.borderColor(colorScheme: colorScheme), lineWidth: 1) } - .padding(.horizontal) - .modifierIfPad { - $0.padding(.bottom) - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Insights") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { - dismiss() - } - .disabled(isCreating) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Insights") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() } + .disabled(isCreating) } - } else { - ProgressView() } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { insightsStore.fileID = fileID if let token = tokenStore.token { @@ -92,6 +97,7 @@ struct InsightsCreate: View, ViewDataProvider, LoadStateProvider, TokenDistribut } } .presentationDetents([.fraction(0.45)]) + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func performCreate() { @@ -111,11 +117,6 @@ struct InsightsCreate: View, ViewDataProvider, LoadStateProvider, TokenDistribut } } - // MARK: - ErrorPresentable - - @State var errorIsPresented: Bool = false - @State var errorMessage: String? - // MARK: - LoadStateProvider var isLoading: Bool { @@ -126,6 +127,11 @@ struct InsightsCreate: View, ViewDataProvider, LoadStateProvider, TokenDistribut insightsStore.languagesError } + // MARK: - ErrorPresentable + + @State var errorIsPresented: Bool = false + @State var errorMessage: String? + // MARK: - ViewDataProvider func onAppearOrChange() { diff --git a/Sources/Screens/Insights/InsightsEntityList.swift b/Sources/Screens/Insights/InsightsEntityList.swift index 2303b90..8bec20b 100644 --- a/Sources/Screens/Insights/InsightsEntityList.swift +++ b/Sources/Screens/Insights/InsightsEntityList.swift @@ -27,41 +27,43 @@ struct InsightsEntityList: View, ViewDataProvider, LoadStateProvider, TimerLifec var body: some View { NavigationView { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = insightsStore.entities { - Group { - if entities.count == 0 { - Text("There are no entities.") - } else { - List { - ForEach(entities, id: \.text) { entity in - InsightsEntityRow(entity) - .onAppear { - onListItemAppear(entity.text) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = insightsStore.entities { + Group { + if entities.count == 0 { + Text("There are no entities.") + } else { + List { + ForEach(entities, id: \.text) { entity in + InsightsEntityRow(entity) + .onAppear { + onListItemAppear(entity.text) + } + } } } } - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Insights") - .refreshable { - insightsStore.fetchEntityNextPage(replace: true) - } - .searchable(text: $searchText) - .onChange(of: searchText) { - insightsStore.searchPublisher.send($1) - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { - dismiss() - } + .refreshable { + insightsStore.fetchEntityNextPage(replace: true) } + .searchable(text: $searchText) + .onChange(of: searchText) { + insightsStore.searchPublisher.send($1) + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Insights") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() } } } diff --git a/Sources/Screens/Insights/InsightsSettings.swift b/Sources/Screens/Insights/InsightsSettings.swift index ed13058..b6f5618 100644 --- a/Sources/Screens/Insights/InsightsSettings.swift +++ b/Sources/Screens/Insights/InsightsSettings.swift @@ -82,7 +82,6 @@ struct InsightsSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecyc } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { insightsStore.fileID = file.id if let token = tokenStore.token { @@ -100,6 +99,7 @@ struct InsightsSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecyc onAppearOrChange() } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var canCreate: Bool { @@ -121,10 +121,11 @@ struct InsightsSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecyc } private func performPatch() { - isPatching = true withErrorHandling { _ = try await insightsStore.patch() return true + } before: { + isPatching = true } success: { dismiss() } failure: { message in @@ -136,10 +137,11 @@ struct InsightsSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecyc } private func performDelete() { - isDeleting = true withErrorHandling { _ = try await insightsStore.delete() return true + } before: { + isDeleting = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Insights/InsightsStore.swift b/Sources/Screens/Insights/InsightsStore.swift index cb95652..45dc8d8 100644 --- a/Sources/Screens/Insights/InsightsStore.swift +++ b/Sources/Screens/Insights/InsightsStore.swift @@ -237,15 +237,17 @@ class InsightsStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchEntityList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchEntityList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/Invitation/InvitationCreate.swift b/Sources/Screens/Invitation/InvitationCreate.swift index 75977ab..361a477 100644 --- a/Sources/Screens/Invitation/InvitationCreate.swift +++ b/Sources/Screens/Invitation/InvitationCreate.swift @@ -18,7 +18,7 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta @Environment(\.dismiss) private var dismiss @State private var commaSeparated = "" @State private var emails: [String] = [] - @State private var isSending = false + @State private var isProcessing = false private let organizationID: String init(_ organizationID: String) { @@ -32,7 +32,7 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta TextEditor(text: $commaSeparated) .autocorrectionDisabled() .textInputAutocapitalization(.never) - .disabled(isSending) + .disabled(isProcessing) .onChange(of: commaSeparated) { parseEmails() } @@ -52,7 +52,7 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta .navigationTitle("New Invitations") .toolbar { ToolbarItem(placement: .topBarTrailing) { - if isSending { + if isProcessing { ProgressView() } else { Button("Send") { @@ -63,7 +63,6 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { invitationStore.organizationID = organizationID if let token = tokenStore.token { @@ -75,6 +74,7 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta assignTokenToStores(newToken) } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func parseEmails() { @@ -89,17 +89,18 @@ struct InvitationCreate: View, FormValidatable, TokenDistributing, ErrorPresenta } private func performCreate() { - isSending = true withErrorHandling { _ = try await invitationStore.create(emails: emails) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSending = false + isProcessing = false } } diff --git a/Sources/Screens/Invitation/InvitationOutgoingList.swift b/Sources/Screens/Invitation/InvitationOutgoingList.swift index c66dce8..81f6bec 100644 --- a/Sources/Screens/Invitation/InvitationOutgoingList.swift +++ b/Sources/Screens/Invitation/InvitationOutgoingList.swift @@ -17,7 +17,7 @@ struct InvitationOutgoingList: View, ViewDataProvider, LoadStateProvider, TimerL @EnvironmentObject private var tokenStore: TokenStore @StateObject private var invitationStore = InvitationStore() @StateObject private var organizationStore = OrganizationStore() - @State private var showCreate = false + @State private var createIsPresented = false @State private var invitation: VOInvitation.Entity? private let organizationID: String @@ -65,7 +65,7 @@ struct InvitationOutgoingList: View, ViewDataProvider, LoadStateProvider, TimerL .toolbar { ToolbarItem(placement: .topBarLeading) { Button { - showCreate = true + createIsPresented = true } label: { Image(systemName: "plus") } @@ -76,7 +76,7 @@ struct InvitationOutgoingList: View, ViewDataProvider, LoadStateProvider, TimerL } } } - .sheet(isPresented: $showCreate) { + .sheet(isPresented: $createIsPresented) { InvitationCreate(organizationID) } .onAppear { diff --git a/Sources/Screens/Invitation/InvitationOverview.swift b/Sources/Screens/Invitation/InvitationOverview.swift index b12284f..0fcad79 100644 --- a/Sources/Screens/Invitation/InvitationOverview.swift +++ b/Sources/Screens/Invitation/InvitationOverview.swift @@ -154,10 +154,11 @@ struct InvitationOverview: View, TokenDistributing, ErrorPresentable { } private func performAccept() { - isAccepting = true withErrorHandling { try await invitationStore.accept(invitation.id) return true + } before: { + isAccepting = true } success: { dismiss() } failure: { message in @@ -169,10 +170,11 @@ struct InvitationOverview: View, TokenDistributing, ErrorPresentable { } private func performDecline() { - isDeclining = true withErrorHandling { try await invitationStore.decline(invitation.id) return true + } before: { + isDeclining = true } success: { dismiss() } failure: { message in @@ -184,10 +186,11 @@ struct InvitationOverview: View, TokenDistributing, ErrorPresentable { } private func performDelete() { - isDeleting = true withErrorHandling { try await invitationStore.delete(invitation.id) return true + } before: { + isDeleting = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Invitation/InvitationStore.swift b/Sources/Screens/Invitation/InvitationStore.swift index feaa615..6914f8e 100644 --- a/Sources/Screens/Invitation/InvitationStore.swift +++ b/Sources/Screens/Invitation/InvitationStore.swift @@ -187,23 +187,27 @@ class InvitationStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } - Task { - let incomingCount = try await self.fetchIncomingCount() - if let incomingCount { - DispatchQueue.main.async { - self.incomingCount = incomingCount + if self.incomingCount != nil { + Task { + let incomingCount = try await self.fetchIncomingCount() + if let incomingCount { + DispatchQueue.main.async { + self.incomingCount = incomingCount + } } } } diff --git a/Sources/Screens/Mosaic/MosaicCreate.swift b/Sources/Screens/Mosaic/MosaicCreate.swift index 9417e06..e59a892 100644 --- a/Sources/Screens/Mosaic/MosaicCreate.swift +++ b/Sources/Screens/Mosaic/MosaicCreate.swift @@ -60,7 +60,6 @@ struct MosaicCreate: View, ErrorPresentable, TokenDistributing { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { mosaicStore.fileID = fileID if let token = tokenStore.token { @@ -73,13 +72,15 @@ struct MosaicCreate: View, ErrorPresentable, TokenDistributing { } } .presentationDetents([.fraction(0.35)]) + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func performCreate() { - isCreating = true withErrorHandling { _ = try await mosaicStore.create() return true + } before: { + isCreating = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Mosaic/MosaicSettings.swift b/Sources/Screens/Mosaic/MosaicSettings.swift index 140b740..2bfd5c4 100644 --- a/Sources/Screens/Mosaic/MosaicSettings.swift +++ b/Sources/Screens/Mosaic/MosaicSettings.swift @@ -82,7 +82,6 @@ struct MosaicSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecycle } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { mosaicStore.fileID = file.id if let token = tokenStore.token { @@ -101,6 +100,7 @@ struct MosaicSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecycle } } .presentationDetents([.fraction(UIDevice.current.userInterfaceIdiom == .pad ? 0.50 : 0.40)]) + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var canCreate: Bool { @@ -122,10 +122,11 @@ struct MosaicSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecycle } private func performCreate() { - isCreating = true withErrorHandling { _ = try await mosaicStore.create() return true + } before: { + isCreating = true } success: { dismiss() } failure: { message in @@ -137,10 +138,11 @@ struct MosaicSettings: View, ViewDataProvider, LoadStateProvider, TimerLifecycle } private func performDelete() { - isDeleting = true withErrorHandling { _ = try await mosaicStore.delete() return true + } before: { + isDeleting = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Mosaic/MosaicStore.swift b/Sources/Screens/Mosaic/MosaicStore.swift index 4fe3cbf..787eaed 100644 --- a/Sources/Screens/Mosaic/MosaicStore.swift +++ b/Sources/Screens/Mosaic/MosaicStore.swift @@ -65,11 +65,13 @@ class MosaicStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - let info = try await self.fetchInfo() - if let info { - DispatchQueue.main.async { - self.info = info + if self.info != nil { + Task { + let info = try await self.fetchInfo() + if let info { + DispatchQueue.main.async { + self.info = info + } } } } diff --git a/Sources/Screens/Organization/OrganizationCreate.swift b/Sources/Screens/Organization/OrganizationCreate.swift index 8158201..ac5aee7 100644 --- a/Sources/Screens/Organization/OrganizationCreate.swift +++ b/Sources/Screens/Organization/OrganizationCreate.swift @@ -48,8 +48,8 @@ struct OrganizationCreate: View, FormValidatable, ErrorPresentable { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedName: String { @@ -57,12 +57,12 @@ struct OrganizationCreate: View, FormValidatable, ErrorPresentable { } private func performCreate() { - isProcessing = true var organization: VOOrganization.Entity? - withErrorHandling { organization = try await organizationStore.create(name: normalizedName) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let organization { diff --git a/Sources/Screens/Organization/OrganizationEditName.swift b/Sources/Screens/Organization/OrganizationEditName.swift index 8f3d761..b7466cd 100644 --- a/Sources/Screens/Organization/OrganizationEditName.swift +++ b/Sources/Screens/Organization/OrganizationEditName.swift @@ -15,7 +15,7 @@ struct OrganizationEditName: View, FormValidatable, ErrorPresentable { @ObservedObject private var organizationStore: OrganizationStore @Environment(\.dismiss) private var dismiss @State private var value = "" - @State private var isSaving = false + @State private var isProcessing = false private let onCompletion: ((VOOrganization.Entity) -> Void)? init(organizationStore: OrganizationStore, onCompletion: ((VOOrganization.Entity) -> Void)? = nil) { @@ -24,35 +24,37 @@ struct OrganizationEditName: View, FormValidatable, ErrorPresentable { } var body: some View { - if let current = organizationStore.current { - Form { - TextField("Name", text: $value) - .disabled(isSaving) + VStack { + if let current = organizationStore.current { + Form { + TextField("Name", text: $value) + .disabled(isProcessing) + } + .onAppear { + value = current.name + } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Name") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Name") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = current.name - } - .onChange(of: organizationStore.current) { _, newCurrent in - if let newCurrent { - value = newCurrent.name - } + } + .onChange(of: organizationStore.current) { _, newCurrent in + if let newCurrent { + value = newCurrent.name } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var nornalizedValue: String { @@ -61,12 +63,13 @@ struct OrganizationEditName: View, FormValidatable, ErrorPresentable { private func performSave() { guard let current = organizationStore.current else { return } - isSaving = true var updatedOrganization: VOOrganization.Entity? withErrorHandling { updatedOrganization = try await organizationStore.patchName(current.id, name: nornalizedValue) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let updatedOrganization { @@ -76,7 +79,7 @@ struct OrganizationEditName: View, FormValidatable, ErrorPresentable { errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Organization/OrganizationList.swift b/Sources/Screens/Organization/OrganizationList.swift index fee1431..36d866e 100644 --- a/Sources/Screens/Organization/OrganizationList.swift +++ b/Sources/Screens/Organization/OrganizationList.swift @@ -24,66 +24,67 @@ struct OrganizationList: View, ViewDataProvider, LoadStateProvider, TimerLifecyc var body: some View { NavigationStack { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = organizationStore.entities { - Group { - if entities.count == 0 { - Text("There are no organizations.") - } else { - List { - ForEach(entities, id: \.id) { organization in - NavigationLink { - OrganizationOverview(organization, organizationStore: organizationStore) - } label: { - OrganizationRow(organization) - .onAppear { - onListItemAppear(organization.id) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = organizationStore.entities { + Group { + if entities.count == 0 { + Text("There are no organizations.") + } else { + List { + ForEach(entities, id: \.id) { organization in + NavigationLink { + OrganizationOverview(organization, organizationStore: organizationStore) + } label: { + OrganizationRow(organization) + .onAppear { + onListItemAppear(organization.id) + } + } + } + } + .navigationDestination(isPresented: $overviewIsPresented) { + if let newOrganization { + OrganizationOverview(newOrganization, organizationStore: organizationStore) } } } - } - } - .navigationTitle("Organizations") - .refreshable { - organizationStore.fetchNextPage(replace: true) - } - .searchable(text: $searchText) - .onChange(of: searchText) { - organizationStore.searchPublisher.send($1) - } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - createIsPresented = true - } label: { - Image(systemName: "plus") - } + .refreshable { + organizationStore.fetchNextPage(replace: true) } - ToolbarItem(placement: .topBarLeading) { - if organizationStore.entitiesIsLoading { - ProgressView() - } + .searchable(text: $searchText) + .onChange(of: searchText) { + organizationStore.searchPublisher.send($1) } } - .sheet(isPresented: $createIsPresented) { - OrganizationCreate(organizationStore: organizationStore) { newOrganization in - self.newOrganization = newOrganization - overviewIsPresented = true - } + } + } + .navigationTitle("Organizations") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + createIsPresented = true + } label: { + Image(systemName: "plus") } - .navigationDestination(isPresented: $overviewIsPresented) { - if let newOrganization { - OrganizationOverview(newOrganization, organizationStore: organizationStore) - } + } + ToolbarItem(placement: .topBarLeading) { + if organizationStore.entitiesIsLoading { + ProgressView() } } } + .sheet(isPresented: $createIsPresented) { + OrganizationCreate(organizationStore: organizationStore) { newOrganization in + self.newOrganization = newOrganization + overviewIsPresented = true + } + } } .onAppear { if let token = tokenStore.token { diff --git a/Sources/Screens/Organization/OrganizationSelector.swift b/Sources/Screens/Organization/OrganizationSelector.swift index 1be6be2..ae6ba66 100644 --- a/Sources/Screens/Organization/OrganizationSelector.swift +++ b/Sources/Screens/Organization/OrganizationSelector.swift @@ -27,44 +27,50 @@ struct OrganizationSelector: View, ViewDataProvider, LoadStateProvider, TimerLif var body: some View { NavigationStack { - if let entities = organizationStore.entities { - Group { - if entities.count == 0 { - Text("There are no organizations.") - } else { - List(selection: $selection) { - ForEach(entities, id: \.id) { organization in - Button { - dismiss() - onCompletion?(organization) - } label: { - OrganizationRow(organization) - .onAppear { - onListItemAppear(organization.id) + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = organizationStore.entities { + Group { + if entities.count == 0 { + Text("There are no organizations.") + } else { + List(selection: $selection) { + ForEach(entities, id: \.id) { organization in + Button { + dismiss() + onCompletion?(organization) + } label: { + OrganizationRow(organization) + .onAppear { + onListItemAppear(organization.id) + } } + } } } } + .refreshable { + organizationStore.fetchNextPage(replace: true) + } + .searchable(text: $searchText) + .onChange(of: searchText) { + organizationStore.searchPublisher.send($1) + } } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Select Organization") - .refreshable { - organizationStore.fetchNextPage(replace: true) - } - .searchable(text: $searchText) - .onChange(of: searchText) { - organizationStore.searchPublisher.send($1) - } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if organizationStore.entitiesIsLoading { - ProgressView() - } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Select Organization") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if organizationStore.entitiesIsLoading { + ProgressView() } } - } else { - ProgressView() } } .onAppear { diff --git a/Sources/Screens/Organization/OrganizationSettings.swift b/Sources/Screens/Organization/OrganizationSettings.swift index 4351d98..fde5136 100644 --- a/Sources/Screens/Organization/OrganizationSettings.swift +++ b/Sources/Screens/Organization/OrganizationSettings.swift @@ -74,12 +74,12 @@ struct OrganizationSettings: View, ErrorPresentable { } private func performDelete() { - isDeleting = true let current = organizationStore.current - withErrorHandling { try await organizationStore.delete() return true + } before: { + isDeleting = true } success: { dismiss() if let current { diff --git a/Sources/Screens/Organization/OrganizationStore.swift b/Sources/Screens/Organization/OrganizationStore.swift index e6254dc..ff746d4 100644 --- a/Sources/Screens/Organization/OrganizationStore.swift +++ b/Sources/Screens/Organization/OrganizationStore.swift @@ -169,15 +169,17 @@ class OrganizationStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/Server/ServerCreate.swift b/Sources/Screens/Server/ServerCreate.swift index b166c18..8e35c72 100644 --- a/Sources/Screens/Server/ServerCreate.swift +++ b/Sources/Screens/Server/ServerCreate.swift @@ -16,32 +16,32 @@ struct ServerCreate: View { @State private var name = "" @State private var apiURL = "" @State private var idpURL = "" - @State private var isSaving = false + @State private var isProcessing = false var body: some View { Form { Section(header: VOSectionHeader("Name")) { TextField("Name", text: $name) - .disabled(isSaving) + .disabled(isProcessing) } Section(header: VOSectionHeader("API URL")) { TextField("API URL", text: $apiURL) .textInputAutocapitalization(.never) .autocorrectionDisabled() - .disabled(isSaving) + .disabled(isProcessing) } Section(header: VOSectionHeader("Identity Provider URL")) { TextField("Identity Provider URL", text: $idpURL) .textInputAutocapitalization(.never) .autocorrectionDisabled() - .disabled(isSaving) + .disabled(isProcessing) } } .navigationBarTitleDisplayMode(.inline) .navigationTitle("New Server") .toolbar { ToolbarItem(placement: .topBarTrailing) { - if isSaving { + if isProcessing { ProgressView() } else { Button("Save") { @@ -58,7 +58,7 @@ struct ServerCreate: View { } private func performSave() { - isSaving = true + isProcessing = true Task { context.insert( Server( @@ -72,7 +72,7 @@ struct ServerCreate: View { try? context.save() DispatchQueue.main.async { dismiss() - isSaving = false + isProcessing = false } } } diff --git a/Sources/Screens/Server/ServerOverview.swift b/Sources/Screens/Server/ServerOverview.swift index 295d6cc..78f1377 100644 --- a/Sources/Screens/Server/ServerOverview.swift +++ b/Sources/Screens/Server/ServerOverview.swift @@ -16,8 +16,8 @@ struct ServerOverview: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var context @Query private var servers: [Server] - @State private var showActivateConfirmation = false - @State private var showDeleteConfirmation = false + @State private var activateConfirmationIsPresented = false + @State private var deleteConfirmationsIsPresented = false @State private var isActivating = false @State private var isDeleting = false private let server: Server @@ -36,7 +36,7 @@ struct ServerOverview: View { } Section { Button { - showActivateConfirmation = true + activateConfirmationIsPresented = true } label: { HStack { Text("Activate Server") @@ -47,7 +47,7 @@ struct ServerOverview: View { } } .disabled(server.isActive || isProcessing) - .confirmationDialog("Activate Server", isPresented: $showActivateConfirmation) { + .confirmationDialog("Activate Server", isPresented: $activateConfirmationIsPresented) { Button("Activate") { performActivate() } @@ -56,7 +56,7 @@ struct ServerOverview: View { } if !server.isCloud { Button(role: .destructive) { - showDeleteConfirmation = true + deleteConfirmationsIsPresented = true } label: { HStack { Text("Delete Server") @@ -67,7 +67,7 @@ struct ServerOverview: View { } } .disabled(isProcessing) - .confirmationDialog("Delete Server", isPresented: $showDeleteConfirmation) { + .confirmationDialog("Delete Server", isPresented: $deleteConfirmationsIsPresented) { Button("Delete", role: .destructive) { performDelete() dismiss() diff --git a/Sources/Screens/Sharing/SharingGroupPermission.swift b/Sources/Screens/Sharing/SharingGroupPermission.swift index ca067be..baee154 100644 --- a/Sources/Screens/Sharing/SharingGroupPermission.swift +++ b/Sources/Screens/Sharing/SharingGroupPermission.swift @@ -137,10 +137,11 @@ struct SharingGroupPermission: View, FormValidatable, ErrorPresentable { private func performGrant() { guard let group, let permission else { return } - isGranting = true withErrorHandling { try await sharingStore.grantGroupPermission(ids: fileIDs, groupID: group.id, permission: permission) return true + } before: { + isGranting = true } success: { dismiss() } failure: { message in @@ -153,10 +154,11 @@ struct SharingGroupPermission: View, FormValidatable, ErrorPresentable { private func performRevoke() { guard let group, fileIDs.count == 1, let fileID = fileIDs.first else { return } - isRevoking = true withErrorHandling { try await sharingStore.revokeGroupPermission(id: fileID, groupID: group.id) return true + } before: { + isRevoking = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Sharing/SharingUserPermission.swift b/Sources/Screens/Sharing/SharingUserPermission.swift index b2b140d..6b183dc 100644 --- a/Sources/Screens/Sharing/SharingUserPermission.swift +++ b/Sources/Screens/Sharing/SharingUserPermission.swift @@ -140,11 +140,11 @@ struct SharingUserPermission: View, FormValidatable, ErrorPresentable { private func performGrant() { guard let user, let permission else { return } - isGranting = true - withErrorHandling { try await sharingStore.grantUserPermission(ids: fileIDs, userID: user.id, permission: permission) return true + } before: { + isGranting = true } success: { dismiss() } failure: { message in @@ -157,11 +157,11 @@ struct SharingUserPermission: View, FormValidatable, ErrorPresentable { private func performRevoke() { guard let user, fileIDs.count == 1, let fileID = fileIDs.first else { return } - isRevoking = true - withErrorHandling { try await sharingStore.revokeUserPermission(id: fileID, userID: user.id) return true + } before: { + isRevoking = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/SignIn/SignIn.swift b/Sources/Screens/SignIn/SignIn.swift index 9091940..de59787 100644 --- a/Sources/Screens/SignIn/SignIn.swift +++ b/Sources/Screens/SignIn/SignIn.swift @@ -11,16 +11,13 @@ import SwiftUI import VoltaserveCore -struct SignIn: View { +struct SignIn: View, ErrorPresentable { @EnvironmentObject private var tokenStore: TokenStore - @State private var isLoading = false + @State private var isProcessing = false @State private var email: String = "" @State private var password: String = "" - @State private var errorTitle: String? - @State private var errorMessage: String? - @State private var showSignUp = false - @State private var showForgotPassword = false - @State private var showError = false + @State private var signUpIsPresented = false + @State private var forgotPasswordIsPresented = false private let onCompletion: (() -> Void)? init(_ onCompletion: (() -> Void)? = nil) { @@ -37,60 +34,61 @@ struct SignIn: View { .voTextField(width: VOMetrics.formWidth) .textInputAutocapitalization(.never) .autocorrectionDisabled() - .disabled(isLoading) + .disabled(isProcessing) SecureField("Password", text: $password) .voTextField(width: VOMetrics.formWidth) - .disabled(isLoading) + .disabled(isProcessing) Button { - signIn() + if !email.isEmpty && !password.isEmpty { + performSignIn() + } } label: { VOButtonLabel( "Sign In", - isLoading: isLoading, + isLoading: isProcessing, progressViewTint: .white ) } - .voPrimaryButton(width: VOMetrics.formWidth, isDisabled: isLoading) + .voPrimaryButton(width: VOMetrics.formWidth, isDisabled: isProcessing) VStack { HStack { Text("Don't have an account yet?") .voFormHintText() Button { - showSignUp = true + signUpIsPresented = true } label: { Text("Sign Up") .voFormHintLabel() } - .disabled(isLoading) + .disabled(isProcessing) } HStack { Text("Cannot sign in?") .voFormHintText() Button { - showForgotPassword = true + forgotPasswordIsPresented = true } label: { Text("Reset Password") .voFormHintLabel() } - .disabled(isLoading) + .disabled(isProcessing) } } } - .fullScreenCover(isPresented: $showSignUp) { + .fullScreenCover(isPresented: $signUpIsPresented) { SignUp { - showSignUp = false + signUpIsPresented = false } onSignIn: { - showSignUp = false + signUpIsPresented = false } } - .fullScreenCover(isPresented: $showForgotPassword) { + .fullScreenCover(isPresented: $forgotPasswordIsPresented) { ForgotPassword { - showForgotPassword = false + forgotPasswordIsPresented = false } onSignIn: { - showForgotPassword = false + forgotPasswordIsPresented = false } } - .voErrorAlert(isPresented: $showError, title: errorTitle, message: errorMessage) .toolbar { ToolbarItem(placement: .topBarTrailing) { NavigationLink(destination: ServerList()) { @@ -100,16 +98,16 @@ struct SignIn: View { } .padding() } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } - private func signIn() { - isLoading = true - + private func performSignIn() { var token: VOToken.Value? - withErrorHandling { token = try await tokenStore.signIn(username: email, password: password) return true + } before: { + isProcessing = true } success: { if let token { tokenStore.token = token @@ -117,13 +115,17 @@ struct SignIn: View { onCompletion?() } } failure: { message in - errorTitle = "Error: Signing In" errorMessage = message - showError = true + errorIsPresented = true } anyways: { - isLoading = false + isProcessing = false } } + + // MARK: - ErrorPresentable + + @State var errorIsPresented: Bool = false + @State var errorMessage: String? } #Preview { diff --git a/Sources/Screens/Snapshot/SnapshotList.swift b/Sources/Screens/Snapshot/SnapshotList.swift index 448bf75..cdab1b8 100644 --- a/Sources/Screens/Snapshot/SnapshotList.swift +++ b/Sources/Screens/Snapshot/SnapshotList.swift @@ -23,46 +23,48 @@ struct SnapshotList: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, var body: some View { NavigationStack { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = snapshotStore.entities { - Group { - if entities.count == 0 { - Text("There are no snapshots.") - } else { - List { - ForEach(entities, id: \.id) { snapshot in - NavigationLink { - SnapshotOverview(snapshot, snapshotStore: snapshotStore) - } label: { - SnapshotRow(snapshot) - .onAppear { - onListItemAppear(snapshot.id) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = snapshotStore.entities { + Group { + if entities.count == 0 { + Text("There are no snapshots.") + } else { + List { + ForEach(entities, id: \.id) { snapshot in + NavigationLink { + SnapshotOverview(snapshot, snapshotStore: snapshotStore) + } label: { + SnapshotRow(snapshot) + .onAppear { + onListItemAppear(snapshot.id) + } + } } } } } + .refreshable { + snapshotStore.fetchNextPage(replace: true) + } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Snapshots") - .refreshable { - snapshotStore.fetchNextPage(replace: true) + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Snapshots") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { - dismiss() - } - } - ToolbarItem(placement: .topBarLeading) { - if snapshotStore.entitiesIsLoading { - ProgressView() - } - } + } + ToolbarItem(placement: .topBarLeading) { + if snapshotStore.entitiesIsLoading { + ProgressView() } } } diff --git a/Sources/Screens/Snapshot/SnapshotOverview.swift b/Sources/Screens/Snapshot/SnapshotOverview.swift index e42a662..6b466a3 100644 --- a/Sources/Screens/Snapshot/SnapshotOverview.swift +++ b/Sources/Screens/Snapshot/SnapshotOverview.swift @@ -116,10 +116,11 @@ struct SnapshotOverview: View, ErrorPresentable { } private func performActivate() { - isActivating = true withErrorHandling { try await snapshotStore.activate(snapshot.id) return true + } before: { + isActivating = true } success: { dismiss() } failure: { message in @@ -131,10 +132,11 @@ struct SnapshotOverview: View, ErrorPresentable { } private func performDetach() { - isDetaching = true withErrorHandling { try await snapshotStore.detach(snapshot.id) return true + } before: { + isDetaching = true } success: { dismiss() } failure: { message in diff --git a/Sources/Screens/Snapshot/SnapshotStore.swift b/Sources/Screens/Snapshot/SnapshotStore.swift index d5f27ed..b574099 100644 --- a/Sources/Screens/Snapshot/SnapshotStore.swift +++ b/Sources/Screens/Snapshot/SnapshotStore.swift @@ -156,15 +156,17 @@ class SnapshotStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/Task/TaskList.swift b/Sources/Screens/Task/TaskList.swift index a4e2bd9..ec67e34 100644 --- a/Sources/Screens/Task/TaskList.swift +++ b/Sources/Screens/Task/TaskList.swift @@ -26,60 +26,61 @@ struct TaskList: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, Toke var body: some View { NavigationStack { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = taskStore.entities { - Group { - if entities.count == 0 { - Text("There are no tasks.") - } else { - List { - ForEach(entities, id: \.id) { task in - NavigationLink { - TaskOverview(task, taskStore: taskStore, fileStore: fileStore) - } label: { - TaskRow(task) - .onAppear { - onListItemAppear(task.id) - } - } - } - } - } - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Tasks") - .refreshable { - taskStore.fetchNextPage(replace: true) - } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if isDismissingAll { - ProgressView() + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = taskStore.entities { + Group { + if entities.count == 0 { + Text("There are no tasks.") } else { - Button("Dismiss All") { - performDismissAll() + List { + ForEach(entities, id: \.id) { task in + NavigationLink { + TaskOverview(task, taskStore: taskStore, fileStore: fileStore) + } label: { + TaskRow(task) + .onAppear { + onListItemAppear(task.id) + } + } + } } } } - ToolbarItem(placement: .topBarTrailing) { - Button("Done") { - dismiss() - } + .refreshable { + taskStore.fetchNextPage(replace: true) } - ToolbarItem(placement: .topBarLeading) { - if taskStore.entitiesIsLoading { - ProgressView() - } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Tasks") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if isDismissingAll { + ProgressView() + } else { + Button("Dismiss All") { + performDismissAll() } } } + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { + dismiss() + } + } + ToolbarItem(placement: .topBarLeading) { + if taskStore.entitiesIsLoading { + ProgressView() + } + } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) .onAppear { if let token = tokenStore.token { assignTokenToStores(token) @@ -96,6 +97,7 @@ struct TaskList: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, Toke onAppearOrChange() } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func performDismissAll() { diff --git a/Sources/Screens/Task/TaskStore.swift b/Sources/Screens/Task/TaskStore.swift index 31251e4..83e13f3 100644 --- a/Sources/Screens/Task/TaskStore.swift +++ b/Sources/Screens/Task/TaskStore.swift @@ -146,15 +146,17 @@ class TaskStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/User/UserSelector.swift b/Sources/Screens/User/UserSelector.swift index 7a9ab41..b55c74d 100644 --- a/Sources/Screens/User/UserSelector.swift +++ b/Sources/Screens/User/UserSelector.swift @@ -70,18 +70,18 @@ struct UserSelector: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, .onChange(of: searchText) { userStore.searchPublisher.send($1) } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if userStore.entitiesIsLoading { - ProgressView() - } - } - } } } } .navigationBarTitleDisplayMode(.inline) .navigationTitle("Select User") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if userStore.entitiesIsLoading { + ProgressView() + } + } + } .onAppear { if let groupID { userStore.groupID = groupID diff --git a/Sources/Screens/User/UserStore.swift b/Sources/Screens/User/UserStore.swift index c296faa..2377acf 100644 --- a/Sources/Screens/User/UserStore.swift +++ b/Sources/Screens/User/UserStore.swift @@ -199,15 +199,17 @@ class UserStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } diff --git a/Sources/Screens/Workspace/WorkspaceCreate.swift b/Sources/Screens/Workspace/WorkspaceCreate.swift index db70884..6f9d298 100644 --- a/Sources/Screens/Workspace/WorkspaceCreate.swift +++ b/Sources/Screens/Workspace/WorkspaceCreate.swift @@ -73,8 +73,8 @@ struct WorkspaceCreate: View, FormValidatable, ErrorPresentable { } } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedName: String { @@ -83,7 +83,6 @@ struct WorkspaceCreate: View, FormValidatable, ErrorPresentable { private func performCreate() { guard let organization, let storageCapacity else { return } - isProcessing = true var workspace: VOWorkspace.Entity? withErrorHandling { @@ -93,6 +92,8 @@ struct WorkspaceCreate: View, FormValidatable, ErrorPresentable { storageCapacity: storageCapacity ) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let workspace { diff --git a/Sources/Screens/Workspace/WorkspaceEditName.swift b/Sources/Screens/Workspace/WorkspaceEditName.swift index 4b37d1d..469f676 100644 --- a/Sources/Screens/Workspace/WorkspaceEditName.swift +++ b/Sources/Screens/Workspace/WorkspaceEditName.swift @@ -15,7 +15,7 @@ struct WorkspaceEditName: View, FormValidatable, ErrorPresentable { @ObservedObject private var workspaceStore: WorkspaceStore @Environment(\.dismiss) private var dismiss @State private var value = "" - @State private var isSaving = false + @State private var isProcessing = false private let onCompletion: ((VOWorkspace.Entity) -> Void)? init(workspaceStore: WorkspaceStore, onCompletion: ((VOWorkspace.Entity) -> Void)? = nil) { @@ -24,35 +24,37 @@ struct WorkspaceEditName: View, FormValidatable, ErrorPresentable { } var body: some View { - if let current = workspaceStore.current { - Form { - TextField("Name", text: $value) - .disabled(isSaving) + VStack { + if let current = workspaceStore.current { + Form { + TextField("Name", text: $value) + .disabled(isProcessing) + } + .onAppear { + value = current.name + } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Name") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Name") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = current.name - } - .onChange(of: workspaceStore.current) { _, newCurrent in - if let newCurrent { - value = newCurrent.name - } + } + .onChange(of: workspaceStore.current) { _, newCurrent in + if let newCurrent { + value = newCurrent.name } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private var normalizedValue: String { @@ -61,12 +63,13 @@ struct WorkspaceEditName: View, FormValidatable, ErrorPresentable { private func performSave() { guard let current = workspaceStore.current else { return } - isSaving = true var updatedWorkspace: VOWorkspace.Entity? withErrorHandling { updatedWorkspace = try await workspaceStore.patchName(current.id, name: normalizedValue) return true + } before: { + isProcessing = true } success: { dismiss() if let onCompletion, let updatedWorkspace { @@ -76,7 +79,7 @@ struct WorkspaceEditName: View, FormValidatable, ErrorPresentable { errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Workspace/WorkspaceEditStorageCapacity.swift b/Sources/Screens/Workspace/WorkspaceEditStorageCapacity.swift index 63e50e9..1079019 100644 --- a/Sources/Screens/Workspace/WorkspaceEditStorageCapacity.swift +++ b/Sources/Screens/Workspace/WorkspaceEditStorageCapacity.swift @@ -15,58 +15,60 @@ struct WorkspaceEditStorageCapacity: View, FormValidatable, ErrorPresentable { @ObservedObject private var workspaceStore: WorkspaceStore @Environment(\.dismiss) private var dismiss @State private var value: Int? - @State private var isSaving = false + @State private var isProcessing = false init(workspaceStore: WorkspaceStore) { self.workspaceStore = workspaceStore } var body: some View { - if let current = workspaceStore.current { - Form { - VOStoragePicker(value: $value) - .disabled(isSaving) + VStack { + if let current = workspaceStore.current { + Form { + VOStoragePicker(value: $value) + .disabled(isProcessing) + } + .onAppear { + value = current.storageCapacity + } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Change Storage Capacity") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if isSaving { - ProgressView() - } else { - Button("Save") { - performSave() - } - .disabled(!isValid()) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Storage Capacity") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() } + .disabled(!isValid()) } } - .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) - .onAppear { - value = current.storageCapacity - } - .onChange(of: workspaceStore.current) { _, newCurrent in - if let newCurrent { - value = newCurrent.storageCapacity - } + } + .onChange(of: workspaceStore.current) { _, newCurrent in + if let newCurrent { + value = newCurrent.storageCapacity } } + .voErrorSheet(isPresented: $errorIsPresented, message: errorMessage) } private func performSave() { guard let value else { return } - isSaving = true - withErrorHandling { _ = try await workspaceStore.patchStorageCapacity(storageCapacity: value) return true + } before: { + isProcessing = true } success: { dismiss() } failure: { message in errorMessage = message errorIsPresented = true } anyways: { - isSaving = false + isProcessing = false } } diff --git a/Sources/Screens/Workspace/WorkspaceList.swift b/Sources/Screens/Workspace/WorkspaceList.swift index b277fb0..5c0318f 100644 --- a/Sources/Screens/Workspace/WorkspaceList.swift +++ b/Sources/Screens/Workspace/WorkspaceList.swift @@ -26,76 +26,78 @@ struct WorkspaceList: View, ViewDataProvider, LoadStateProvider, TimerLifecycle, var body: some View { NavigationStack { - if isLoading { - ProgressView() - } else if let error { - VOErrorMessage(error) - } else { - if let entities = workspaceStore.entities { - Group { - if entities.count == 0 { - Text("There are no workspaces.") - } else { - List { - ForEach(entities, id: \.id) { workspace in - NavigationLink { - WorkspaceOverview(workspace, workspaceStore: workspaceStore) - } label: { - WorkspaceRow(workspace) - .onAppear { - onListItemAppear(workspace.id) - } + VStack { + if isLoading { + ProgressView() + } else if let error { + VOErrorMessage(error) + } else { + if let entities = workspaceStore.entities { + Group { + if entities.count == 0 { + Text("There are no workspaces.") + } else { + List { + ForEach(entities, id: \.id) { workspace in + NavigationLink { + WorkspaceOverview(workspace, workspaceStore: workspaceStore) + } label: { + WorkspaceRow(workspace) + .onAppear { + onListItemAppear(workspace.id) + } + } } } } } - } - .navigationTitle("Home") - .refreshable { - workspaceStore.fetchNextPage(replace: true) - } - .searchable(text: $searchText) - .onChange(of: searchText) { - workspaceStore.searchPublisher.send($1) - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - if UIDevice.current.userInterfaceIdiom == .phone { - accountButton - .padding(.trailing, VOMetrics.spacingXs) - } else { - accountButton - } + .refreshable { + workspaceStore.fetchNextPage(replace: true) } - ToolbarItem(placement: .topBarLeading) { - Button { - createIsPresented = true - } label: { - Image(systemName: "plus") - } + .searchable(text: $searchText) + .onChange(of: searchText) { + workspaceStore.searchPublisher.send($1) } - ToolbarItem(placement: .topBarLeading) { - if workspaceStore.entitiesIsLoading { - ProgressView() + .navigationDestination(isPresented: $overviewIsPresented) { + if let newWorkspace { + WorkspaceOverview(newWorkspace, workspaceStore: workspaceStore) } } } - .sheet(isPresented: $createIsPresented) { - WorkspaceCreate(workspaceStore: workspaceStore) { newWorkspace in - self.newWorkspace = newWorkspace - overviewIsPresented = true - } + } + } + .navigationTitle("Home") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if UIDevice.current.userInterfaceIdiom == .phone { + accountButton + .padding(.trailing, VOMetrics.spacingXs) + } else { + accountButton } - .sheet(isPresented: $accountIsPresented) { - AccountOverview() + } + ToolbarItem(placement: .topBarLeading) { + Button { + createIsPresented = true + } label: { + Image(systemName: "plus") } - .navigationDestination(isPresented: $overviewIsPresented) { - if let newWorkspace { - WorkspaceOverview(newWorkspace, workspaceStore: workspaceStore) - } + } + ToolbarItem(placement: .topBarLeading) { + if workspaceStore.entitiesIsLoading { + ProgressView() } } } + .sheet(isPresented: $createIsPresented) { + WorkspaceCreate(workspaceStore: workspaceStore) { newWorkspace in + self.newWorkspace = newWorkspace + overviewIsPresented = true + } + } + .sheet(isPresented: $accountIsPresented) { + AccountOverview() + } } .onAppear { accountStore.tokenStore = tokenStore diff --git a/Sources/Screens/Workspace/WorkspaceSettings.swift b/Sources/Screens/Workspace/WorkspaceSettings.swift index bfc0ef7..57d8c4c 100644 --- a/Sources/Screens/Workspace/WorkspaceSettings.swift +++ b/Sources/Screens/Workspace/WorkspaceSettings.swift @@ -111,12 +111,12 @@ struct WorkspaceSettings: View, ViewDataProvider, LoadStateProvider, ErrorPresen } private func performDelete() { - isDeleting = true let current = workspaceStore.current - withErrorHandling { try await workspaceStore.delete() return true + } before: { + isDeleting = true } success: { dismiss() if let current { diff --git a/Sources/Screens/Workspace/WorkspaceStore.swift b/Sources/Screens/Workspace/WorkspaceStore.swift index f1fe85c..ef5206b 100644 --- a/Sources/Screens/Workspace/WorkspaceStore.swift +++ b/Sources/Screens/Workspace/WorkspaceStore.swift @@ -12,6 +12,7 @@ import Combine import Foundation import VoltaserveCore +// swiftlint:disable:next type_body_length class WorkspaceStore: ObservableObject { @Published var entities: [VOWorkspace.Entity]? @Published var entitiesIsLoading: Bool = false @@ -245,15 +246,17 @@ class WorkspaceStore: ObservableObject { func startTimer() { guard timer == nil else { return } timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in - Task { - var size = Constants.pageSize - if let list = self.list { - size = Constants.pageSize * list.page - } - let list = try await self.fetchList(page: 1, size: size) - if let list { - DispatchQueue.main.async { - self.entities = list.data + if self.entities != nil { + Task { + var size = Constants.pageSize + if let list = self.list { + size = Constants.pageSize * list.page + } + let list = try await self.fetchList(page: 1, size: size) + if let list { + DispatchQueue.main.async { + self.entities = list.data + } } } } @@ -266,19 +269,23 @@ class WorkspaceStore: ObservableObject { } } } - Task { - let root = try await self.fetchRoot() - if let root { - DispatchQueue.main.async { - self.root = root + if self.root != nil { + Task { + let root = try await self.fetchRoot() + if let root { + DispatchQueue.main.async { + self.root = root + } } } } - Task { - let storageUsage = try await self.fetchStorageUsage() - if let storageUsage { - DispatchQueue.main.async { - self.storageUsage = storageUsage + if self.storageUsage != nil { + Task { + let storageUsage = try await self.fetchStorageUsage() + if let storageUsage { + DispatchQueue.main.async { + self.storageUsage = storageUsage + } } } }