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_