diff --git a/Share/Model/NCShareViewModel+Accounts.swift b/Share/Model/NCShareViewModel+Accounts.swift index 7d299bf216..4524fa26c3 100644 --- a/Share/Model/NCShareViewModel+Accounts.swift +++ b/Share/Model/NCShareViewModel+Accounts.swift @@ -34,7 +34,7 @@ extension NCShareViewModel { } func selectAccount(_ account: String) { - guard isAuthenticated, !isClosing, !isPreparingUpload, !isSwitchingAccount, + guard isAuthenticated, !isClosing, !isLoading, !isPreparingUpload, !isSwitchingAccount, !isLoadingAccounts, accounts.contains(where: { $0.id == account }) else { return } guard account != sessionStore.getSession().account else { return } changeAccount(account) diff --git a/Share/Model/NCShareViewModel+Folders.swift b/Share/Model/NCShareViewModel+Folders.swift index aa051dc6bb..515a6d31b9 100644 --- a/Share/Model/NCShareViewModel+Folders.swift +++ b/Share/Model/NCShareViewModel+Folders.swift @@ -63,7 +63,8 @@ extension NCShareViewModel { guard isCurrentBrowser(browserID, account: session.account) else { return } // Publish the replacement snapshot together, without an empty intermediate list. rootFolder = root - selectedFolder = root + // Leave the destination unset while the most recent folder is awaiting validation. + selectedFolder = recent.isEmpty ? root : nil isRecentFolderSelected = false expandedFolderURLs = [home] recentFolders = recent @@ -72,14 +73,14 @@ extension NCShareViewModel { func refreshInitialFolderBrowser() { guard let rootFolder else { return } - if let latest = recentFolders.first { + if selectedFolder == nil, let latest = recentFolders.first { selectFolder(latest, fromRecents: true, automatically: true) } requestFolderChildren(rootFolder, refresh: true) } func toggleFolder(_ folder: NCShareFolderItem) { - guard canUseFolderBrowser, canAccessFolder(folder) else { return } + guard canUseFolderBrowser, !isLoading, canAccessFolder(folder) else { return } if expandedFolderURLs.contains(folder.serverUrl) { expandedFolderURLs.remove(folder.serverUrl) } else { @@ -88,7 +89,7 @@ extension NCShareViewModel { } func retryFolder(_ folder: NCShareFolderItem) { - guard canUseFolderBrowser, canAccessFolder(folder) else { return } + guard canUseFolderBrowser, !isLoading, canAccessFolder(folder) else { return } expandedFolderURLs.insert(folder.serverUrl) requestFolderChildren(folder, refresh: true) } @@ -108,14 +109,22 @@ extension NCShareViewModel { /// Validates the destination on the server before selecting it. /// fromRecents controls which of the two occurrences displays the checkmark. func selectFolder(_ folder: NCShareFolderItem, fromRecents: Bool = false, automatically: Bool = false) { - guard canSelectFolder, automatically || !isLoadingAccounts, let capabilities = serverCapabilities else { return } - guard canAccessFolder(folder, showError: !automatically) else { return } + guard canSelectFolder, !isLoading, automatically || !isLoadingAccounts, let capabilities = serverCapabilities else { return } + guard canAccessFolder(folder, showError: !automatically) else { + if automatically { selectedFolder = rootFolder } + return + } let session = sessionStore.getSession() let browserID = folderBrowserID isLoading = true folderSelectionTask = Task { @MainActor in defer { - if folderBrowserID == browserID { isLoading = false } + if isCurrentBrowser(browserID, account: session.account) { + if automatically, isAuthenticated, selectedFolder == nil { + selectedFolder = rootFolder + } + isLoading = false + } } // Recent destinations may have been deleted or moved since last use. let result = await NCNetworking.shared.readFileAsync(serverUrlFileName: folder.serverUrl, account: session.account) @@ -154,7 +163,7 @@ extension NCShareViewModel { } private var canSelectFolder: Bool { - isAuthenticated && !isClosing && isAccountReady && !isLoading && !isPreparingUpload + isAuthenticated && !isClosing && isAccountReady && !isPreparingUpload } private func canAccessFolder(_ folder: NCShareFolderItem, showError: Bool = true) -> Bool { diff --git a/Share/Model/NCShareViewModel+Upload.swift b/Share/Model/NCShareViewModel+Upload.swift index 77d4528467..36fffd0d6c 100644 --- a/Share/Model/NCShareViewModel+Upload.swift +++ b/Share/Model/NCShareViewModel+Upload.swift @@ -17,7 +17,7 @@ extension NCShareViewModel { /// Upload flow: inspect the destination, resolve name conflicts if needed, /// stage local copies, then transfer. Keep editing disabled throughout this flow. func upload() { - guard canUpload, let capabilities = serverCapabilities else { return } + guard canUpload, !isLoading, folderTasks.isEmpty, let capabilities = serverCapabilities else { return } runUpload { [self] in let plan = try await prepareUploadPlan(capabilities: capabilities) try Task.checkCancellation() diff --git a/Share/Model/NCShareViewModel.swift b/Share/Model/NCShareViewModel.swift index d03cb32539..d407b67590 100644 --- a/Share/Model/NCShareViewModel.swift +++ b/Share/Model/NCShareViewModel.swift @@ -58,7 +58,7 @@ final class NCShareViewModel: NSObject, ObservableObject { var canUseUploadDestination: Bool { guard let selectedFolder else { return false } - return canUseFolderBrowser && !isLoadingCapabilities && folderTasks.isEmpty && + return canUseFolderBrowser && !isLoadingCapabilities && folderLoadErrors[selectedFolder.serverUrl] == nil && alertMessage.isEmpty && !isPreparingInput && inputErrorMessage.isEmpty } @@ -192,6 +192,8 @@ final class NCShareViewModel: NSObject, ObservableObject { guard !isOffline, isAuthenticated, !isClosing, !isSwitchingAccount, !isPreparingUpload else { return } if serverCapabilities == nil { retryCapabilities() + } else if selectedFolder == nil { + refreshInitialFolderBrowser() } else { for row in visibleFolderRows where expandedFolderURLs.contains(row.folder.serverUrl) { requestFolderChildren(row.folder, refresh: true) @@ -359,7 +361,8 @@ final class NCShareViewModel: NSObject, ObservableObject { } func createFolder() { - guard canUseUploadDestination, let folder = selectedFolder, let capabilities = serverCapabilities, + guard canUseUploadDestination, !isLoading, folderTasks.isEmpty, + let folder = selectedFolder, let capabilities = serverCapabilities, let presenter, presenter.viewIfLoaded?.window != nil, presenter.presentedViewController == nil, !presenter.isBeingDismissed else { return } @@ -467,6 +470,9 @@ final class NCShareViewModel: NSObject, ObservableObject { guard self.validateAccount() else { return } self.access = .unlocked if self.accounts.isEmpty { self.loadAccounts() } + if self.selectedFolder == nil, self.serverCapabilities != nil { + self.refreshInitialFolderBrowser() + } } } } diff --git a/Share/View/NCShareUploadButton.swift b/Share/View/NCShareUploadButton.swift index ca82b2e1b0..0bca9ae715 100644 --- a/Share/View/NCShareUploadButton.swift +++ b/Share/View/NCShareUploadButton.swift @@ -9,6 +9,7 @@ struct NCShareUploadButton: View { let fileCount: Int var isPreparing = false var title: String? + var minimumLabelHeight: CGFloat? let action: @MainActor () -> Void var body: some View { @@ -25,7 +26,7 @@ struct NCShareUploadButton: View { .lineLimit(2) .truncationMode(.middle) .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, minHeight: minimumLabelHeight) } .buttonStyle(.borderedProminent) .buttonBorderShape(.roundedRectangle(radius: 12)) diff --git a/Share/View/NCShareUploadConflictView.swift b/Share/View/NCShareUploadConflictView.swift index 118f3437d7..1913288136 100644 --- a/Share/View/NCShareUploadConflictView.swift +++ b/Share/View/NCShareUploadConflictView.swift @@ -8,6 +8,7 @@ import SwiftUI struct NCShareUploadConflictView: View { let confirmation: NCShareUploadConfirmation let resolve: @MainActor (NCShareUploadChoice?) -> Void + private let actionLabelHeight: CGFloat = 44 var body: some View { let renamedItems = confirmation.renamedItems @@ -47,14 +48,14 @@ struct NCShareUploadConflictView: View { .padding(.vertical, 12) } .safeAreaInset(edge: .bottom, spacing: 0) { - VStack(spacing: 16) { + VStack(spacing: 24) { if !confirmation.isBatch, confirmation.replacementFileName != nil { Button(role: .destructive) { resolve(.replace) } label: { Text(NSLocalizedString("_share_conflict_replace_", comment: "")) .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, minHeight: actionLabelHeight) } .buttonStyle(.bordered) .buttonBorderShape(.roundedRectangle(radius: 12)) @@ -64,7 +65,8 @@ struct NCShareUploadConflictView: View { NCShareUploadButton( fileCount: confirmation.items.count, - title: confirmation.isBatch ? nil : NSLocalizedString("_share_conflict_upload_with_new_name_", comment: "") + title: confirmation.isBatch ? nil : NSLocalizedString("_share_conflict_upload_with_new_name_", comment: ""), + minimumLabelHeight: actionLabelHeight ) { resolve(.keepBoth) } diff --git a/Share/View/NCShareView.swift b/Share/View/NCShareView.swift index 2cb146b121..8c169e2c4f 100644 --- a/Share/View/NCShareView.swift +++ b/Share/View/NCShareView.swift @@ -11,11 +11,6 @@ struct NCShareView: View { private var canShowContent: Bool { model.rootFolder != nil && model.isAuthenticated } - private var isRefreshing: Bool { - model.isLoading || model.isSwitchingAccount || model.isLoadingAccounts || - model.isLoadingCapabilities || !model.folderTasks.isEmpty - } - var body: some View { NavigationStack { VStack(spacing: 0) { @@ -78,7 +73,7 @@ struct NCShareView: View { if canShowContent { let account = ToolbarItem(placement: .topBarLeading) { NCShareAccountMenu(model: model) - .disabled(model.isLoading || model.isPreparingUpload || model.isSwitchingAccount || model.isLoadingAccounts) + .disabled(model.isPreparingUpload || model.isSwitchingAccount || model.isLoadingAccounts) } if #available(iOS 27.1, *) { account.axisBehavior(.horizontalOnly) @@ -87,18 +82,6 @@ struct NCShareView: View { account } } - if canShowContent && isRefreshing { - let spinner = ToolbarItem(placement: .topBarLeading) { - ProgressView() - .controlSize(.regular) - .accessibilityLabel(NSLocalizedString("_request_in_progress_", comment: "")) - } - if #available(iOS 27.1, *) { - spinner.axisBehavior(.horizontalOnly) - } else { - spinner - } - } ToolbarItem(placement: .topBarTrailing) { Button(NSLocalizedString("_cancel_", comment: "")) { model.cancel() } } @@ -201,8 +184,8 @@ struct NCShareView: View { .frame(maxHeight: .infinity) .clipped() .background(Color(uiColor: .systemGroupedBackground)) - .opacity(isRefreshing ? 0.45 : 1) - .disabled(isRefreshing || model.isPreparingUpload || !model.isAccountReady) + .opacity(model.isSwitchingAccount ? 0.45 : 1) + .disabled(model.isSwitchingAccount) } private func folderListTitle(_ key: String) -> some View { @@ -246,10 +229,10 @@ struct NCShareView: View { model.toggleFolder(folder) } label: { Image(systemName: model.expandedFolderURLs.contains(folder.serverUrl) ? "chevron.down" : "chevron.right") + .foregroundStyle(Color.accentColor) .frame(width: 50, height: 50) .contentShape(Rectangle()) } - .buttonStyle(.borderless) .accessibilityLabel(String(format: NSLocalizedString( model.expandedFolderURLs.contains(folder.serverUrl) ? "_share_collapse_folder_" : "_share_expand_folder_", comment: ""), folder.name)) .padding(.leading, CGFloat(min(depth, 8)) * 16) @@ -304,30 +287,38 @@ struct NCShareView: View { } } Spacer(minLength: 4) - if isSelected { - Image(systemName: "checkmark") - .frame(width: 50, height: 50) - .accessibilityHidden(true) - } + Image(systemName: "checkmark") + .foregroundStyle(Color.accentColor) + .frame(width: 50, height: 50) + .opacity(isSelected ? 1 : 0) + .accessibilityHidden(true) } .frame(minHeight: 50) .contentShape(Rectangle()) } - .buttonStyle(.borderless) .accessibilityAddTraits(isSelected ? .isSelected : []) } + .buttonStyle(FolderRowButtonStyle()) + .disabled(!model.canUseFolderBrowser) .background { - if isSelected { - RoundedRectangle(cornerRadius: 8) - .fill(Color.accentColor.opacity(0.1)) - .padding(.horizontal, 5) - } + RoundedRectangle(cornerRadius: 8) + .fill(Color.accentColor.opacity(isSelected ? 0.1 : 0)) + .padding(.horizontal, 5) } .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 2)) .listRowBackground(Color(uiColor: .secondarySystemGroupedBackground)) } + private struct FolderRowButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .opacity(isEnabled ? 1 : 0.55) + } + } + private func fileList(fillsHeight: Bool = false, horizontalInset: CGFloat = 16) -> some View { List(model.fileNames, id: \.self) { fileName in Button { @@ -388,8 +379,8 @@ struct NCShareView: View { .background(Color(uiColor: .secondarySystemGroupedBackground)) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, horizontalInset) - .opacity(isRefreshing ? 0.45 : 1) - .disabled(isRefreshing || model.isPreparingUpload || model.isPreparingInput || !model.isAccountReady || !model.inputErrorMessage.isEmpty) + .opacity(model.isSwitchingAccount ? 0.45 : 1) + .disabled(model.isPreparingUpload || model.isPreparingInput || !model.isAccountReady || !model.inputErrorMessage.isEmpty) } private var uploadButton: some View { diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 5e4b9868b7..9253621922 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -449,7 +449,7 @@ "_empty_trash_" = "Empty trash"; "_trash_no_trash_" = "No files deleted"; "_trash_no_trash_description_" = "You can restore deleted files from here"; -"_confirm_delete_selected_" = "Are you sure you want to delete the selected items?"; +"_confirm_delete_selected_" = "Are you sure you want to delete the selected files?"; "_set_available_offline_" = "Set as available offline"; "_remove_available_offline_" = "Remove as available offline"; "_create_new_document_" = "Document"; @@ -570,7 +570,7 @@ "_additional_options_" = "Additional options"; "_unauthorized_files_passcode_" = "Files app cannot be used with an activated passcode"; "_files_app_disabled_" = "Files app cannot be used because it is disabled"; -"_no_items_" = "No items"; +"_no_items_" = "Nothing to show"; "_check_back_later_" = "Check back later"; "_no_data_available_" = "No data available"; "_recent_activity_" = "Recent activity"; @@ -603,14 +603,14 @@ "_finalizing_wait_" = "Waiting for finalization …"; "_in_this_folder_" = "In this folder"; "_media_no_longer_available_" = "Media no longer available"; -"_this_item_has_been_deleted_" = "This item has been deleted."; +"_this_item_has_been_deleted_" = "This file has been deleted."; "_video_not_available_" = "Video not available"; "_disable_" = "Disable"; "_no_subtitles_available_" = "No subtitles available"; "_no_audio_tracks_available_" = "No audio tracks available"; "_add_external_subtitle_" = "Add external subtitle"; -"_repeat_current_media_" = "Repeat current item"; -"_play_next_media_automatically_" = "Play next item automatically"; +"_repeat_current_media_" = "Repeat current file"; +"_play_next_media_automatically_" = "Play next file automatically"; "_previous_media_" = "Previous"; "_next_media_" = "Next"; "_play_" = "Play"; @@ -694,7 +694,7 @@ "_multiple_albums_" = "Multiple albums"; "_smart_albums_" = "Smart albums"; "_albums_" = "Albums"; -"_auto_upload_no_new_items_to_upload_" = "No new items to upload"; +"_auto_upload_no_new_items_to_upload_" = "No new files to upload"; "_background_upload_extension_" = "PhotoKit background upload extension"; "_experimental_" = "Experimental"; "_background_upload_extension_footer_" = "Experimental: delegate automatic photo uploads to PhotoKit in the background. The compatibility setting (HEIC to JPG) is ignored, so HEIC images are uploaded in their original format."; @@ -715,12 +715,12 @@ "_focused_auto_upload_countdown_" = "Do not lock the screen or exit the app. The screen will turn dark in %d seconds."; "_stop_focused_auto_upload_" = "Stop Focused Auto Upload"; "_finish_" = "Finish"; -"_item_with_same_name_already_exists_" = "An item with the same name already exists."; +"_item_with_same_name_already_exists_" = "This name is already in use."; // MARK: Migration Multi Domains "_preparing_migration_" = "Preparing migration …"; "_scanning_files_" = "Scanning files …"; -"_moving_items_to_domain_" = "Moving items to correct domain …"; +"_moving_items_to_domain_" = "Moving files to the correct domain…"; "_finishing_up_" = "Finishing up …"; // MARK: E2EE @@ -839,13 +839,13 @@ /* Share extension input preparation */ "_share_import_preparing_" = "Preparing shared files…"; -"_share_import_failed_" = "Some shared items could not be prepared. Review the files before uploading."; -"_share_import_no_files_" = "The shared items could not be prepared."; -"_share_import_item_error_" = "Item %d: %@"; +"_share_import_failed_" = "Some shared files could not be prepared. Review the files before uploading."; +"_share_import_no_files_" = "The shared files could not be prepared."; +"_share_import_item_error_" = "File %d: %@"; /* Share extension upload conflicts */ "_share_conflict_title_" = "Files already exist"; -"_share_conflict_single_message_" = "An item named \"%@\" already exists in this folder."; +"_share_conflict_single_message_" = "A file named \"%@\" already exists in this folder."; "_share_conflict_batch_message_" = "Some names are already in use. These files will be uploaded with new names, keeping the existing files."; "_share_conflict_new_names_" = "New file names"; "_share_conflict_replace_" = "Replace existing file"; diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict index 767f642f68..b2d946f1fa 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict +++ b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict @@ -61,9 +61,9 @@ NSStringFormatValueTypeKey d one - You have %d item to back up + You have %d file to back up other - You have %d items to back up + You have %d files to back up _focused_auto_upload_failed_