From 900ec2a17876854ce01d2dccd3966a9d9e35d007 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 10:38:26 +0200 Subject: [PATCH 01/18] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 12 ++ .../AutoUpload/NCAutoUploadView.swift | 55 ++++++++ .../NCFocusedAutoUploadIntroView.swift | 99 ++++++++++++++ .../NCFocusedAutoUploadProgressView.swift | 127 ++++++++++++++++++ .../NCFocusedAutoUploadScreenDimmer.swift | 62 +++++++++ .../en.lproj/Localizable.strings | 11 ++ 6 files changed, 366 insertions(+) create mode 100644 iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift create mode 100644 iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift create mode 100644 iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 3a9447037a..6a5e45abee 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -503,6 +503,9 @@ F76882302C0DD1E7001CF441 /* NCFileNameModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882192C0DD1E7001CF441 /* NCFileNameModel.swift */; }; F76882312C0DD1E7001CF441 /* NCFileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821A2C0DD1E7001CF441 /* NCFileNameView.swift */; }; F76882322C0DD1E7001CF441 /* NCAutoUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */; }; + AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */; }; + AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */; }; + AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; }; F76882332C0DD1E7001CF441 /* NCDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821D2C0DD1E7001CF441 /* NCDisplayModel.swift */; }; F76882342C0DD1E7001CF441 /* NCDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821E2C0DD1E7001CF441 /* NCDisplayView.swift */; }; F76882352C0DD1E7001CF441 /* NCWebBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882202C0DD1E7001CF441 /* NCWebBrowserView.swift */; }; @@ -1475,6 +1478,9 @@ F76882192C0DD1E7001CF441 /* NCFileNameModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCFileNameModel.swift; sourceTree = ""; }; F768821A2C0DD1E7001CF441 /* NCFileNameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCFileNameView.swift; sourceTree = ""; }; F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAutoUploadView.swift; sourceTree = ""; }; + AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadIntroView.swift; sourceTree = ""; }; + AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = ""; }; + AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; }; F768821D2C0DD1E7001CF441 /* NCDisplayModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDisplayModel.swift; sourceTree = ""; }; F768821E2C0DD1E7001CF441 /* NCDisplayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDisplayView.swift; sourceTree = ""; }; F76882202C0DD1E7001CF441 /* NCWebBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCWebBrowserView.swift; sourceTree = ""; }; @@ -2619,6 +2625,9 @@ F39A1EE12D0AF8A200DAD522 /* Albums.swift */, F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */, F71D2FB62E09BBD700B751CC /* NCAutoUploadModel.swift */, + AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */, + AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */, + AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */, ); path = AutoUpload; sourceTree = ""; @@ -4689,6 +4698,9 @@ F76D364628A4F8BF00214537 /* NCActivityIndicator.swift in Sources */, F3A047992BD2668800658E7B /* NCAssistantModel.swift in Sources */, F76882322C0DD1E7001CF441 /* NCAutoUploadView.swift in Sources */, + AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */, + AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */, + AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */, F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */, F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */, F75D19E325EFE09000D74598 /* NCContextMenuTrash.swift in Sources */, diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index f178959853..28ebaaba02 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -15,6 +15,9 @@ struct NCAutoUploadView: View { @State private var showUploadFolder = false @State private var showSelectAlbums = false @State private var showUploadAllPhotosWarning = false + @State private var showFocusedAutoUploadIntro = false + @State private var showFocusedAutoUploadProgress = false + @State private var openFocusedAutoUploadAfterIntro = false @State private var startAutoUpload = false var body: some View { @@ -49,6 +52,28 @@ struct NCAutoUploadView: View { ConfirmAutoUploadSheet(model: model, isPresented: $showUploadAllPhotosWarning) .presentationDetents([.medium, .large]) } + .sheet(isPresented: $showFocusedAutoUploadIntro, onDismiss: { + guard openFocusedAutoUploadAfterIntro else { return } + + openFocusedAutoUploadAfterIntro = false + showFocusedAutoUploadProgress = true + }) { + NCFocusedAutoUploadIntroView { + openFocusedAutoUploadAfterIntro = true + showFocusedAutoUploadIntro = false + } + .presentationDetents([.large]) + } + .fullScreenCover(isPresented: $showFocusedAutoUploadProgress) { + NCFocusedAutoUploadProgressView(isPresented: $showFocusedAutoUploadProgress) + } + .onChange(of: model.autoUploadStart) { _, newValue in + if !newValue { + showFocusedAutoUploadIntro = false + showFocusedAutoUploadProgress = false + openFocusedAutoUploadAfterIntro = false + } + } } @ViewBuilder @@ -207,6 +232,36 @@ struct NCAutoUploadView: View { }) } .disabled(model.autoUploadStart) + + if model.autoUploadStart { + Section(content: { + Button { + showFocusedAutoUploadIntro = true + } label: { + HStack { + Image(systemName: "moon") + .font(.icon()) + .frame(width: 26) + .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) + + Text(NSLocalizedString("_focused_auto_upload_", comment: "")) + .font(.body) + .foregroundStyle(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.footnote) + .fontWeight(.semibold) + .foregroundStyle(.tertiary) + } + } + .buttonStyle(.plain) + }, footer: { + Text(NSLocalizedString("_focused_auto_upload_settings_footer_", comment: "")) + .font(.footnote) + }) + } } .safeAreaInset(edge: .bottom) { autoUploadStartButton diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift new file mode 100644 index 0000000000..a540d4aaa9 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +import SwiftUI + +struct NCFocusedAutoUploadIntroView: View { + @Environment(\.dismiss) private var dismiss + + let onEnable: () -> Void + + var body: some View { + VStack(spacing: 0) { + ZStack { + Text(NSLocalizedString("_focused_auto_upload_", comment: "")) + .font(.title3) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + + HStack { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.system(size: 28, weight: .regular)) + .foregroundStyle(.primary) + .frame(width: 48, height: 48) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(NSLocalizedString("_close_", comment: "")) + + Spacer() + } + } + .padding(.top, 24) + .padding(.horizontal, 24) + + Spacer(minLength: 90) + + VStack(spacing: 24) { + Text(NSLocalizedString("_focused_auto_upload_intro_heading_", comment: "")) + .font(.title2) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .foregroundStyle(.primary) + + Text(NSLocalizedString("_focused_auto_upload_intro_message_", comment: "")) + .font(.body) + .multilineTextAlignment(.center) + .foregroundStyle(.primary) + .fixedSize(horizontal: false, vertical: true) + + VStack(alignment: .leading, spacing: 22) { + guidanceRow(systemImage: "wifi", textKey: "_focused_auto_upload_wifi_") + guidanceRow(systemImage: "battery.100", textKey: "_focused_auto_upload_charger_") + guidanceRow(systemImage: "app", textKey: "_focused_auto_upload_do_not_exit_") + } + .padding(.top, 10) + } + .padding(.horizontal, 36) + + Spacer(minLength: 80) + + Button { + onEnable() + } label: { + Text(NSLocalizedString("_enable_focused_auto_upload_", comment: "")) + .font(.title3) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .background( + Capsule() + .fill(Color(UIColor.darkGray)) + ) + .padding(.horizontal, 38) + .padding(.bottom, 28) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(UIColor.systemBackground)) + } + + private func guidanceRow(systemImage: String, textKey: String) -> some View { + HStack(spacing: 16) { + Image(systemName: systemImage) + .font(.system(size: 28, weight: .regular)) + .foregroundStyle(.secondary) + .frame(width: 34) + + Text(NSLocalizedString(textKey, comment: "")) + .font(.title3) + .foregroundStyle(.primary) + .fixedSize(horizontal: false, vertical: true) + } + } +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift new file mode 100644 index 0000000000..f1b49dd5de --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +import SwiftUI + +@MainActor +struct NCFocusedAutoUploadProgressView: View { + @Binding var isPresented: Bool + @Environment(\.scenePhase) private var scenePhase + + @State private var countdownTask: Task? + @State private var secondsUntilDim = 10 + @State private var isScreenDimmed = false + + private let dimDelay = 10 + + var body: some View { + ZStack { + Color.black + .ignoresSafeArea() + + VStack(spacing: 0) { + Spacer() + + VStack(spacing: 24) { + Divider() + .background(Color.white.opacity(0.15)) + .padding(.horizontal, 36) + + Text(NSLocalizedString("_focused_auto_upload_backing_up_", comment: "")) + .font(.largeTitle) + .fontWeight(.semibold) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + } + + Spacer() + + Text(statusMessage) + .font(.title3) + .foregroundStyle(.white.opacity(0.9)) + .multilineTextAlignment(.center) + .padding(.horizontal, 28) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + Button { + isPresented = false + } label: { + Text(NSLocalizedString("_stop_focused_auto_upload_", comment: "")) + .font(.title3) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .background( + Capsule() + .fill(Color(UIColor.darkGray)) + ) + .padding(.horizontal, 54) + .padding(.bottom, 34) + } + + if isScreenDimmed { + Color.black + .ignoresSafeArea() + .contentShape(Rectangle()) + .onTapGesture { + wakeFocusedScreen() + } + } + } + .preferredColorScheme(.dark) + .statusBarHidden(isScreenDimmed) + .onAppear { + startFocusedMode() + } + .onDisappear { + stopFocusedMode() + } + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + startFocusedMode() + } else { + stopFocusedMode() + } + } + } + + private var statusMessage: String { + return String(format: NSLocalizedString("_focused_auto_upload_countdown_", comment: ""), secondsUntilDim) + } + + private func startFocusedMode() { + countdownTask?.cancel() + secondsUntilDim = dimDelay + isScreenDimmed = false + + NCFocusedAutoUploadScreenDimmer.shared.startKeepingScreenAwake() + + countdownTask = Task { @MainActor in + while secondsUntilDim > 0 { + try? await Task.sleep(for: .seconds(1)) + guard !Task.isCancelled else { return } + secondsUntilDim -= 1 + } + + NCFocusedAutoUploadScreenDimmer.shared.dimScreen() + isScreenDimmed = true + } + } + + private func stopFocusedMode() { + countdownTask?.cancel() + countdownTask = nil + isScreenDimmed = false + NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() + } + + private func wakeFocusedScreen() { + stopFocusedMode() + startFocusedMode() + } +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift new file mode 100644 index 0000000000..47c2d2aade --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +import UIKit + +@MainActor +final class NCFocusedAutoUploadScreenDimmer { + static let shared = NCFocusedAutoUploadScreenDimmer() + + private var originalBrightness: CGFloat? + private var originalIdleTimerDisabled: Bool? + private var keepAwakeTask: Task? + + private init() {} + + func startKeepingScreenAwake() { + if originalIdleTimerDisabled == nil { + originalIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled + } + + UIApplication.shared.isIdleTimerDisabled = true + startKeepAwakeTask() + } + + func dimScreen() { + if originalBrightness == nil { + originalBrightness = UIScreen.main.brightness + } + + startKeepingScreenAwake() + UIScreen.main.brightness = 0 + } + + func restoreScreen() { + keepAwakeTask?.cancel() + keepAwakeTask = nil + + if let originalBrightness { + UIScreen.main.brightness = originalBrightness + } + + if let originalIdleTimerDisabled { + UIApplication.shared.isIdleTimerDisabled = originalIdleTimerDisabled + } + + originalBrightness = nil + originalIdleTimerDisabled = nil + } + + private func startKeepAwakeTask() { + guard keepAwakeTask == nil else { + return + } + + keepAwakeTask = Task { @MainActor in + while !Task.isCancelled { + UIApplication.shared.isIdleTimerDisabled = true + try? await Task.sleep(for: .seconds(1)) + } + } + } +} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 23851fc7e0..9728dcab31 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -764,6 +764,17 @@ You can stop it at any time, adjust the settings, and enable it again."; "_back_up_new_photos_only_" = "Back up new photos/videos only"; "_auto_upload_all_photos_warning_title_" = "Are you sure you want to upload all photos?"; "_auto_upload_all_photos_warning_message_" = "This can take some time to process depending on the amount of photos."; +"_focused_auto_upload_" = "Focused Auto Upload"; +"_focused_auto_upload_settings_footer_" = "Keep the app open and darken the screen while auto upload is backing up."; +"_focused_auto_upload_intro_heading_" = "Keep backing up with the screen darkened"; +"_focused_auto_upload_intro_message_" = "Photos and videos will continue backing up while the screen is darkened. During the process:"; +"_focused_auto_upload_wifi_" = "Connect to Wi-Fi"; +"_focused_auto_upload_charger_" = "Connect to charger"; +"_focused_auto_upload_do_not_exit_" = "Do not exit the app"; +"_enable_focused_auto_upload_" = "Enable Focused Auto Upload"; +"_focused_auto_upload_backing_up_" = "Backing up"; +"_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"; "_item_with_same_name_already_exists_" = "An item with the same name already exists."; // MARK: Migration Multi Domains From d33093a88ae0ee22a8a7a8b325819c0ec6d6f43c Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 11:28:18 +0200 Subject: [PATCH 02/18] Animation Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 12 ++++---- .../NCFocusedAutoUploadProgressView.swift | 29 +++++++++++++++++++ .../en.lproj/Localizable.strings | 10 +++---- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 6a5e45abee..26b041882e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -44,6 +44,9 @@ AABD0C8A2D5F67A400F009E6 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C892D5F67A200F009E6 /* XCUIElement.swift */; }; AABD0C9B2D5F73FC00F009E6 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C9A2D5F73FA00F009E6 /* Placeholder.swift */; }; AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */; }; + AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */; }; + AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */; }; + AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; }; AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; @@ -503,9 +506,6 @@ F76882302C0DD1E7001CF441 /* NCFileNameModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882192C0DD1E7001CF441 /* NCFileNameModel.swift */; }; F76882312C0DD1E7001CF441 /* NCFileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821A2C0DD1E7001CF441 /* NCFileNameView.swift */; }; F76882322C0DD1E7001CF441 /* NCAutoUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */; }; - AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */; }; - AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */; }; - AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; }; F76882332C0DD1E7001CF441 /* NCDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821D2C0DD1E7001CF441 /* NCDisplayModel.swift */; }; F76882342C0DD1E7001CF441 /* NCDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768821E2C0DD1E7001CF441 /* NCDisplayView.swift */; }; F76882352C0DD1E7001CF441 /* NCWebBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882202C0DD1E7001CF441 /* NCWebBrowserView.swift */; }; @@ -1210,6 +1210,9 @@ AACCAB632CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Localizable.strings; sourceTree = ""; }; AACCAB642CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/InfoPlist.strings; sourceTree = ""; }; AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNavigationTitleSetting.swift; sourceTree = ""; }; + AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadIntroView.swift; sourceTree = ""; }; + AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = ""; }; + AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; }; AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; @@ -1478,9 +1481,6 @@ F76882192C0DD1E7001CF441 /* NCFileNameModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCFileNameModel.swift; sourceTree = ""; }; F768821A2C0DD1E7001CF441 /* NCFileNameView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCFileNameView.swift; sourceTree = ""; }; F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAutoUploadView.swift; sourceTree = ""; }; - AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadIntroView.swift; sourceTree = ""; }; - AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = ""; }; - AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; }; F768821D2C0DD1E7001CF441 /* NCDisplayModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDisplayModel.swift; sourceTree = ""; }; F768821E2C0DD1E7001CF441 /* NCDisplayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDisplayView.swift; sourceTree = ""; }; F76882202C0DD1E7001CF441 /* NCWebBrowserView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCWebBrowserView.swift; sourceTree = ""; }; diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index f1b49dd5de..55b1236954 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -11,6 +11,7 @@ struct NCFocusedAutoUploadProgressView: View { @State private var countdownTask: Task? @State private var secondsUntilDim = 10 @State private var isScreenDimmed = false + @State private var isCloudAnimating = false private let dimDelay = 10 @@ -23,6 +24,9 @@ struct NCFocusedAutoUploadProgressView: View { Spacer() VStack(spacing: 24) { + focusedUploadAnimation + .padding(.bottom, 4) + Divider() .background(Color.white.opacity(0.15)) .padding(.horizontal, 36) @@ -76,6 +80,7 @@ struct NCFocusedAutoUploadProgressView: View { .preferredColorScheme(.dark) .statusBarHidden(isScreenDimmed) .onAppear { + isCloudAnimating = true startFocusedMode() } .onDisappear { @@ -94,6 +99,30 @@ struct NCFocusedAutoUploadProgressView: View { return String(format: NSLocalizedString("_focused_auto_upload_countdown_", comment: ""), secondsUntilDim) } + private var focusedUploadAnimation: some View { + ZStack { + Circle() + .stroke(Color.white.opacity(0.18), lineWidth: 2) + .frame(width: 148, height: 148) + .scaleEffect(isCloudAnimating ? 1.08 : 0.88) + .opacity(isCloudAnimating ? 0.1 : 0.36) + + Image(systemName: "icloud.fill") + .font(.system(size: 94, weight: .regular)) + .foregroundStyle(.white) + .shadow(color: .white.opacity(isCloudAnimating ? 0.22 : 0.08), radius: 18) + .offset(y: isCloudAnimating ? -5 : 5) + + Image(systemName: "arrow.up") + .font(.system(size: 30, weight: .bold)) + .foregroundStyle(.black.opacity(0.82)) + .offset(y: isCloudAnimating ? -16 : -4) + } + .frame(width: 176, height: 144) + .animation(.easeInOut(duration: 1.45).repeatForever(autoreverses: true), value: isCloudAnimating) + .accessibilityHidden(true) + } + private func startFocusedMode() { countdownTask?.cancel() secondsUntilDim = dimDelay diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 9728dcab31..6f6c0daac3 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -23,7 +23,7 @@ "_cancel_" = "Cancel"; "_edit_" = "Edit"; "_tap_to_cancel_" = "Tap to cancel"; -"_tap_to_min_max_" = "Tap to minimize / maximize"; +"_tap_to_min_max_" = "Tap to minimize"; "_cancel_request_" = "Do you want to cancel?"; "_upload_file_" = "Upload file"; "_download_file_" = "Download file"; @@ -166,8 +166,8 @@ "_keep_both_for_all_action_title_" = "Keep both for all"; "_more_action_title_" = "More Details"; "_wait_file_preparation_" = "Preparing file upload. Please wait …"; -"_keep_active_for_upload_" = "Keep application active until upload is completed …"; -"_keep_active_for_transfers_" = "Keep application active until the transfers are completed …"; +"_keep_active_for_upload_" = "Keep application open until upload is completed …"; +"_keep_active_for_transfers_" = "Keep application open until the transfers are completed …"; "_wait_file_encryption_" = "Please wait, encrypting file…"; "_passcode_counter_fail_" = "Too many failed attempts, please wait before reattempting."; "_seconds_" = "seconds"; @@ -678,8 +678,8 @@ "_retry_minutes_" = "min left to retry"; "_retry_seconds_" = "sec left to retry"; "_retry_soon_" = "retrying soon …"; -"_large_upload_tip_" = "Large files require the app to remain open until the transfer is complete"; -"_e2ee_upload_tip_" = "End-to-end files require the app to remain open until the transfer is complete"; +"_large_upload_tip_" = "Large files require the app to remain open until the transfer is complete."; +"_e2ee_upload_tip_" = "End-to-end encrypted files require the app to remain open until the transfer is complete."; "_finalizing_wait_" = "Waiting for finalization …"; "_in_this_folder_" = "In this folder"; From 7e8a275e6752ca07fd896ad1cdca97d24ba41caf Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 11:48:55 +0200 Subject: [PATCH 03/18] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 4 + .../NCFocusedAutoUploadCloudAnimation.swift | 87 +++++++++++++++++++ .../NCFocusedAutoUploadIntroView.swift | 5 +- .../NCFocusedAutoUploadProgressView.swift | 33 +------ .../NCFocusedAutoUploadScreenDimmer.swift | 5 +- .../Settings/Settings/NCSettingsModel.swift | 3 + .../Settings/Settings/NCSettingsView.swift | 9 +- 7 files changed, 110 insertions(+), 36 deletions(-) create mode 100644 iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 26b041882e..e0a9a07ba8 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */; }; AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */; }; AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; }; + AAFC0D092F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */; }; AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; @@ -1213,6 +1214,7 @@ AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadIntroView.swift; sourceTree = ""; }; AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = ""; }; AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; }; + AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadCloudAnimation.swift; sourceTree = ""; }; AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; @@ -2628,6 +2630,7 @@ AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */, AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */, AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */, + AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */, ); path = AutoUpload; sourceTree = ""; @@ -4701,6 +4704,7 @@ AAFC0D042F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift in Sources */, AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */, AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */, + AAFC0D092F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift in Sources */, F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */, F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */, F75D19E325EFE09000D74598 /* NCContextMenuTrash.swift in Sources */, diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift new file mode 100644 index 0000000000..0d48af1e60 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct NCFocusedAutoUploadCloudAnimation: View { + let size: CGFloat + let cloudColor: Color + let arrowColor: Color + let ringColor: Color + let showsRing: Bool + let isAnimated: Bool + + init(size: CGFloat = 176, + cloudColor: Color = .white, + arrowColor: Color = .black.opacity(0.82), + ringColor: Color = .white, + showsRing: Bool = true, + isAnimated: Bool = true) { + self.size = size + self.cloudColor = cloudColor + self.arrowColor = arrowColor + self.ringColor = ringColor + self.showsRing = showsRing + self.isAnimated = isAnimated + } + + var body: some View { + if isAnimated { + animatedCloud + } else { + staticCloud + } + } + + private var animatedCloud: some View { + TimelineView(.animation) { timeline in + cloud(progress: animationProgress(at: timeline.date), includesMotion: true) + } + .frame(width: size, height: size * 0.82) + .accessibilityHidden(true) + } + + private var staticCloud: some View { + cloud(progress: 0, includesMotion: false) + .frame(width: size, height: size * 0.82) + .accessibilityHidden(true) + } + + private func cloud(progress: Double, includesMotion: Bool) -> some View { + ZStack { + if showsRing { + Circle() + .stroke(ringColor.opacity(0.75), lineWidth: max(2, size * 0.018)) + .frame(width: size * 0.84, height: size * 0.84) + .scaleEffect(interpolate(from: 0.88, to: 1.08, progress: progress)) + .opacity(interpolate(from: 0.85, to: 0.45, progress: progress)) + } + + Image(systemName: "icloud.fill") + .font(.system(size: size * 0.53, weight: .regular)) + .foregroundStyle(cloudColor) + .shadow(color: ringColor.opacity(includesMotion ? interpolate(from: 0.12, to: 0.28, progress: progress) : 0), + radius: includesMotion && showsRing ? size * 0.1 : 0) + .offset(y: includesMotion ? interpolate(from: size * 0.03, to: -size * 0.03, progress: progress) : 0) + + Image(systemName: "arrow.up") + .font(.system(size: size * 0.17, weight: .bold)) + .foregroundStyle(arrowColor) + .offset(y: includesMotion ? interpolate(from: -size * 0.02, to: -size * 0.09, progress: progress) : -size * 0.055) + } + } + + private func animationProgress(at date: Date) -> Double { + let duration = 1.45 + let cycleDuration = duration * 2 + let elapsed = date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: cycleDuration) + let linearProgress = elapsed <= duration ? elapsed / duration : (cycleDuration - elapsed) / duration + + return (1 - cos(linearProgress * .pi)) / 2 + } + + private func interpolate(from start: CGFloat, to end: CGFloat, progress: Double) -> CGFloat { + start + (end - start) * CGFloat(progress) + } +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift index a540d4aaa9..fda716ee80 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -1,5 +1,6 @@ -// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index 55b1236954..565c8e8e5c 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -1,5 +1,6 @@ -// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI @@ -11,7 +12,6 @@ struct NCFocusedAutoUploadProgressView: View { @State private var countdownTask: Task? @State private var secondsUntilDim = 10 @State private var isScreenDimmed = false - @State private var isCloudAnimating = false private let dimDelay = 10 @@ -24,7 +24,7 @@ struct NCFocusedAutoUploadProgressView: View { Spacer() VStack(spacing: 24) { - focusedUploadAnimation + NCFocusedAutoUploadCloudAnimation() .padding(.bottom, 4) Divider() @@ -80,7 +80,6 @@ struct NCFocusedAutoUploadProgressView: View { .preferredColorScheme(.dark) .statusBarHidden(isScreenDimmed) .onAppear { - isCloudAnimating = true startFocusedMode() } .onDisappear { @@ -99,30 +98,6 @@ struct NCFocusedAutoUploadProgressView: View { return String(format: NSLocalizedString("_focused_auto_upload_countdown_", comment: ""), secondsUntilDim) } - private var focusedUploadAnimation: some View { - ZStack { - Circle() - .stroke(Color.white.opacity(0.18), lineWidth: 2) - .frame(width: 148, height: 148) - .scaleEffect(isCloudAnimating ? 1.08 : 0.88) - .opacity(isCloudAnimating ? 0.1 : 0.36) - - Image(systemName: "icloud.fill") - .font(.system(size: 94, weight: .regular)) - .foregroundStyle(.white) - .shadow(color: .white.opacity(isCloudAnimating ? 0.22 : 0.08), radius: 18) - .offset(y: isCloudAnimating ? -5 : 5) - - Image(systemName: "arrow.up") - .font(.system(size: 30, weight: .bold)) - .foregroundStyle(.black.opacity(0.82)) - .offset(y: isCloudAnimating ? -16 : -4) - } - .frame(width: 176, height: 144) - .animation(.easeInOut(duration: 1.45).repeatForever(autoreverses: true), value: isCloudAnimating) - .accessibilityHidden(true) - } - private func startFocusedMode() { countdownTask?.cancel() secondsUntilDim = dimDelay diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift index 47c2d2aade..cad8bad4d3 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift @@ -1,5 +1,6 @@ -// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit diff --git a/iOSClient/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index b74e600753..3b0a3ff446 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -22,6 +22,8 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { @Published var privacyScreen: Bool = false // State to control @Published var resetWrongAttempts: Bool = false + // State to control the auto upload status indicator + @Published var autoUploadStart: Bool = false // Request account on start @Published var accountRequest: Bool = false // Root View Controller @@ -52,6 +54,7 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { lockScreen = !keychain.requestPasscodeAtStart privacyScreen = keychain.privacyScreenEnabled resetWrongAttempts = keychain.resetAppCounterFail + autoUploadStart = NCManageDatabase.shared.getTableAccount(account: session.account)?.autoUploadStart ?? false accountRequest = keychain.accountRequest footerApp = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, NCUtility().getVersionBuild()) + "\n\n" footerServer = String(format: NCBrandOptions.shared.textCopyrightNextcloudServer, capabilities.serverVersion) + "\n" diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index a2482c78ef..9131e8e62c 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -34,9 +34,12 @@ struct NCSettingsView: View { NCAutoUploadView(model: NCAutoUploadModel(controller: model.controller), albumModel: AlbumModel(controller: model.controller)) }) { HStack { - Image(systemName: "photo.on.rectangle.angled") - .font(.icon()) - .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) + NCFocusedAutoUploadCloudAnimation(size: 44, + cloudColor: Color(NCBrandColor.shared.iconImageColor), + arrowColor: Color(UIColor.systemBackground), + ringColor: Color(NCBrandColor.shared.iconImageColor), + showsRing: model.autoUploadStart, + isAnimated: model.autoUploadStart) .frame(width: 39) Text(NSLocalizedString("_settings_autoupload_", comment: "")) From 68c8b2de9582ec4b1427b51f1e82eb12fcd354c0 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 12:26:44 +0200 Subject: [PATCH 04/18] WIP Signed-off-by: Milen Pivchev --- .../NCFocusedAutoUploadCloudAnimation.swift | 23 ++++-------------- .../NCFocusedAutoUploadIntroView.swift | 24 +++++++++++++++---- .../Settings/Settings/NCSettingsView.swift | 6 ++--- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift index 0d48af1e60..83751b044b 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift @@ -8,21 +8,15 @@ struct NCFocusedAutoUploadCloudAnimation: View { let size: CGFloat let cloudColor: Color let arrowColor: Color - let ringColor: Color - let showsRing: Bool let isAnimated: Bool init(size: CGFloat = 176, cloudColor: Color = .white, arrowColor: Color = .black.opacity(0.82), - ringColor: Color = .white, - showsRing: Bool = true, isAnimated: Bool = true) { self.size = size self.cloudColor = cloudColor self.arrowColor = arrowColor - self.ringColor = ringColor - self.showsRing = showsRing self.isAnimated = isAnimated } @@ -50,25 +44,16 @@ struct NCFocusedAutoUploadCloudAnimation: View { private func cloud(progress: Double, includesMotion: Bool) -> some View { ZStack { - if showsRing { - Circle() - .stroke(ringColor.opacity(0.75), lineWidth: max(2, size * 0.018)) - .frame(width: size * 0.84, height: size * 0.84) - .scaleEffect(interpolate(from: 0.88, to: 1.08, progress: progress)) - .opacity(interpolate(from: 0.85, to: 0.45, progress: progress)) - } - - Image(systemName: "icloud.fill") + Image(systemName: includesMotion ? "icloud.fill" : "icloud") .font(.system(size: size * 0.53, weight: .regular)) .foregroundStyle(cloudColor) - .shadow(color: ringColor.opacity(includesMotion ? interpolate(from: 0.12, to: 0.28, progress: progress) : 0), - radius: includesMotion && showsRing ? size * 0.1 : 0) + .scaleEffect(includesMotion ? interpolate(from: 0.96, to: 1.03, progress: progress) : 1) .offset(y: includesMotion ? interpolate(from: size * 0.03, to: -size * 0.03, progress: progress) : 0) Image(systemName: "arrow.up") - .font(.system(size: size * 0.17, weight: .bold)) + .font(.system(size: includesMotion ? size * 0.17 : size * 0.22, weight: .bold)) .foregroundStyle(arrowColor) - .offset(y: includesMotion ? interpolate(from: -size * 0.02, to: -size * 0.09, progress: progress) : -size * 0.055) + .offset(y: includesMotion ? interpolate(from: -size * 0.02, to: -size * 0.09, progress: progress) : 0) } } diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift index fda716ee80..447a8ad93e 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -54,7 +54,7 @@ struct NCFocusedAutoUploadIntroView: View { VStack(alignment: .leading, spacing: 22) { guidanceRow(systemImage: "wifi", textKey: "_focused_auto_upload_wifi_") guidanceRow(systemImage: "battery.100", textKey: "_focused_auto_upload_charger_") - guidanceRow(systemImage: "app", textKey: "_focused_auto_upload_do_not_exit_") + guidanceRow(systemImage: "logo", textKey: "_focused_auto_upload_do_not_exit_") } .padding(.top, 10) } @@ -86,10 +86,7 @@ struct NCFocusedAutoUploadIntroView: View { private func guidanceRow(systemImage: String, textKey: String) -> some View { HStack(spacing: 16) { - Image(systemName: systemImage) - .font(.system(size: 28, weight: .regular)) - .foregroundStyle(.secondary) - .frame(width: 34) + guidanceIcon(systemImage: systemImage) Text(NSLocalizedString(textKey, comment: "")) .font(.title3) @@ -97,4 +94,21 @@ struct NCFocusedAutoUploadIntroView: View { .fixedSize(horizontal: false, vertical: true) } } + + @ViewBuilder + private func guidanceIcon(systemImage: String) -> some View { + if systemImage == "logo" { + Image("logo") + .renderingMode(.template) + .resizable() + .scaledToFit() + .foregroundStyle(.secondary) + .frame(width: 34, height: 28) + } else { + Image(systemName: systemImage) + .font(.system(size: 28, weight: .regular)) + .foregroundStyle(.secondary) + .frame(width: 34) + } + } } diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index 9131e8e62c..a1cbcbaf7a 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -36,9 +36,9 @@ struct NCSettingsView: View { HStack { NCFocusedAutoUploadCloudAnimation(size: 44, cloudColor: Color(NCBrandColor.shared.iconImageColor), - arrowColor: Color(UIColor.systemBackground), - ringColor: Color(NCBrandColor.shared.iconImageColor), - showsRing: model.autoUploadStart, + arrowColor: model.autoUploadStart + ? Color(UIColor.systemBackground) + : Color(NCBrandColor.shared.iconImageColor), isAnimated: model.autoUploadStart) .frame(width: 39) From eb7f7cf4eca0d62aeba95af581fc6c2f95337a9c Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 13:10:44 +0200 Subject: [PATCH 05/18] WIP Signed-off-by: Milen Pivchev --- .../Data/NCManageDatabase+AutoUpload.swift | 33 +++++++++++++ .../AutoUpload/NCAutoUploadView.swift | 5 +- .../NCFocusedAutoUploadProgressView.swift | 48 +++++++++++++++++-- .../Settings/Settings/NCSettingsModel.swift | 32 +++++++++++++ .../Settings/Settings/NCSettingsView.swift | 7 ++- .../en.lproj/Localizable.strings | 4 +- .../en.lproj/Localizable.stringsdict | 32 +++++++++++++ 7 files changed, 152 insertions(+), 9 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+AutoUpload.swift b/iOSClient/Data/NCManageDatabase+AutoUpload.swift index 17d3f0fecb..834933a61f 100644 --- a/iOSClient/Data/NCManageDatabase+AutoUpload.swift +++ b/iOSClient/Data/NCManageDatabase+AutoUpload.swift @@ -106,6 +106,39 @@ extension NCManageDatabase { } } + func countAutoUploadMetadatasAsync(account: String, + autoUploadServerUrlBase: String, + transfersSuccess: [tableMetadata] = []) async -> Int { + let global = NCGlobal.shared + let excludedIds = Set(transfersSuccess.compactMap { metadata -> String? in + guard metadata.account == account, + metadata.sessionSelector == global.selectorUploadAutoUpload, + metadata.autoUploadServerUrlBase == autoUploadServerUrlBase, + !metadata.ocIdTransfer.isEmpty else { + return nil + } + + return metadata.ocIdTransfer + }) + + return await core.performRealmReadAsync { realm in + let results = realm.objects(tableMetadata.self) + .filter("account == %@ AND autoUploadServerUrlBase == %@ AND directory == false AND sessionSelector == %@ AND status IN %@", + account, + autoUploadServerUrlBase, + global.selectorUploadAutoUpload, + global.metadataStatusUploadingAllMode) + + guard !excludedIds.isEmpty else { + return results.count + } + + return results + .filter("NOT (ocIdTransfer IN %@)", Array(excludedIds)) + .count + } ?? 0 + } + func existsAutoUpload(account: String, autoUploadServerUrlBase: String) -> Bool { return core.performRealmRead { realm in diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 28ebaaba02..9b6589b1ed 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -65,7 +65,10 @@ struct NCAutoUploadView: View { .presentationDetents([.large]) } .fullScreenCover(isPresented: $showFocusedAutoUploadProgress) { - NCFocusedAutoUploadProgressView(isPresented: $showFocusedAutoUploadProgress) + NCFocusedAutoUploadProgressView(isPresented: $showFocusedAutoUploadProgress, + account: model.session.account, + urlBase: model.session.urlBase, + userId: model.session.userId) } .onChange(of: model.autoUploadStart) { _, newValue in if !newValue { diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index 565c8e8e5c..2aa3e20b67 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -9,7 +9,13 @@ struct NCFocusedAutoUploadProgressView: View { @Binding var isPresented: Bool @Environment(\.scenePhase) private var scenePhase + let account: String + let urlBase: String + let userId: String + @State private var countdownTask: Task? + @State private var uploadCountTask: Task? + @State private var autoUploadCount = 0 @State private var secondsUntilDim = 10 @State private var isScreenDimmed = false @@ -31,11 +37,18 @@ struct NCFocusedAutoUploadProgressView: View { .background(Color.white.opacity(0.15)) .padding(.horizontal, 36) - Text(NSLocalizedString("_focused_auto_upload_backing_up_", comment: "")) - .font(.largeTitle) - .fontWeight(.semibold) - .foregroundStyle(.white) - .multilineTextAlignment(.center) + VStack(spacing: 6) { + Text(NSLocalizedString("_focused_auto_upload_backing_up_", comment: "")) + .font(.largeTitle) + .fontWeight(.semibold) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + + Text(uploadCountMessage) + .font(.title3) + .foregroundStyle(.white.opacity(0.9)) + .multilineTextAlignment(.center) + } } Spacer() @@ -98,12 +111,17 @@ struct NCFocusedAutoUploadProgressView: View { return String(format: NSLocalizedString("_focused_auto_upload_countdown_", comment: ""), secondsUntilDim) } + private var uploadCountMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), autoUploadCount) + } + private func startFocusedMode() { countdownTask?.cancel() secondsUntilDim = dimDelay isScreenDimmed = false NCFocusedAutoUploadScreenDimmer.shared.startKeepingScreenAwake() + startUploadCountPolling() countdownTask = Task { @MainActor in while secondsUntilDim > 0 { @@ -120,6 +138,8 @@ struct NCFocusedAutoUploadProgressView: View { private func stopFocusedMode() { countdownTask?.cancel() countdownTask = nil + uploadCountTask?.cancel() + uploadCountTask = nil isScreenDimmed = false NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() } @@ -128,4 +148,22 @@ struct NCFocusedAutoUploadProgressView: View { stopFocusedMode() startFocusedMode() } + + private func startUploadCountPolling() { + uploadCountTask?.cancel() + + uploadCountTask = Task { @MainActor in + let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, + urlBase: urlBase, + userId: userId) + + while !Task.isCancelled { + let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() + autoUploadCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, + autoUploadServerUrlBase: autoUploadServerUrlBase, + transfersSuccess: transfersSuccess) + try? await Task.sleep(for: .seconds(2)) + } + } + } } diff --git a/iOSClient/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index 3b0a3ff446..79b1d82267 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -24,6 +24,8 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { @Published var resetWrongAttempts: Bool = false // State to control the auto upload status indicator @Published var autoUploadStart: Bool = false + // State to control the auto upload queue count + @Published var autoUploadCount: Int = 0 // Request account on start @Published var accountRequest: Bool = false // Root View Controller @@ -55,14 +57,44 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { privacyScreen = keychain.privacyScreenEnabled resetWrongAttempts = keychain.resetAppCounterFail autoUploadStart = NCManageDatabase.shared.getTableAccount(account: session.account)?.autoUploadStart ?? false + if !autoUploadStart { + autoUploadCount = 0 + } accountRequest = keychain.accountRequest footerApp = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, NCUtility().getVersionBuild()) + "\n\n" footerServer = String(format: NCBrandOptions.shared.textCopyrightNextcloudServer, capabilities.serverVersion) + "\n" footerSlogan = capabilities.themingName + " - " + capabilities.themingSlogan + "\n\n" } + var autoUploadCountMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_items_left_", comment: ""), autoUploadCount) + } + // MARK: - All functions + @MainActor + func pollAutoUploadCount() async { + guard autoUploadStart else { + autoUploadCount = 0 + return + } + + let account = session.account + let urlBase = session.urlBase + let userId = session.userId + let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, + urlBase: urlBase, + userId: userId) + + while autoUploadStart && !Task.isCancelled { + let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() + autoUploadCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, + autoUploadServerUrlBase: autoUploadServerUrlBase, + transfersSuccess: transfersSuccess) + try? await Task.sleep(for: .seconds(2)) + } + } + /// Function to update Touch ID / Face ID setting func updateTouchIDSetting() { keychain.touchFaceID = enableTouchFaceID diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index a1cbcbaf7a..3807a7c861 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -42,7 +42,9 @@ struct NCSettingsView: View { isAnimated: model.autoUploadStart) .frame(width: 39) - Text(NSLocalizedString("_settings_autoupload_", comment: "")) + Text(model.autoUploadStart + ? model.autoUploadCountMessage + : NSLocalizedString("_settings_autoupload_", comment: "")) .font(.body) } } @@ -314,6 +316,9 @@ struct NCSettingsView: View { } .navigationBarTitle(NSLocalizedString("_settings_", comment: "")) .defaultViewModifier(model) + .task(id: model.autoUploadStart) { + await model.pollAutoUploadCount() + } } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 6f6c0daac3..86d091d821 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -672,9 +672,9 @@ "_download_in_progress_" = "Download in progress …"; "_upload_in_progress_" = "Upload in progress …"; "_transfer_in_progress_" = "Transfer in progress …"; -"_in_waiting_" = "In waiting"; +"_in_waiting_" = "Scheduled"; "_in_progress_" = "In progress"; -"_in_error_" = "In error"; +"_in_error_" = "Failed"; "_retry_minutes_" = "min left to retry"; "_retry_seconds_" = "sec left to retry"; "_retry_soon_" = "retrying soon …"; diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict index e0e01b6491..ef4ad41a9e 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict +++ b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict @@ -18,5 +18,37 @@ %d remaining downloads allowed + _focused_auto_upload_items_left_ + + NSStringLocalizedFormatKey + %#@items@ + items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d item left + other + %d items left + + + _focused_auto_upload_photos_to_back_up_ + + NSStringLocalizedFormatKey + %#@photos@ + photos + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + You have %d photo to back up + other + You have %d photos to back up + + From 13f1c2fe065666fa5dede12d434e233e26fce739 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 19 May 2026 13:53:36 +0200 Subject: [PATCH 06/18] PR WIP Signed-off-by: Milen Pivchev --- .../AutoUpload/NCAutoUploadView.swift | 62 ++++++++++--------- .../Settings/Settings/NCSettingsView.swift | 14 +++-- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 9b6589b1ed..3fc8843c10 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -82,6 +82,38 @@ struct NCAutoUploadView: View { @ViewBuilder var autoUploadOnView: some View { Form { + if model.autoUploadStart { + Section(content: { + Button { + showFocusedAutoUploadIntro = true + } label: { + HStack { + Image(systemName: "moon") + .font(.icon()) + .frame(width: 26) + .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) + + Text(NSLocalizedString("_focused_auto_upload_", comment: "")) + .font(.body) + .foregroundStyle(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.footnote) + .fontWeight(.semibold) + .foregroundStyle(.tertiary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + }, footer: { + Text(NSLocalizedString("_focused_auto_upload_settings_footer_", comment: "")) + .font(.footnote) + }) + } + Group { Section(content: { Button(action: { @@ -235,36 +267,6 @@ struct NCAutoUploadView: View { }) } .disabled(model.autoUploadStart) - - if model.autoUploadStart { - Section(content: { - Button { - showFocusedAutoUploadIntro = true - } label: { - HStack { - Image(systemName: "moon") - .font(.icon()) - .frame(width: 26) - .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) - - Text(NSLocalizedString("_focused_auto_upload_", comment: "")) - .font(.body) - .foregroundStyle(.primary) - - Spacer() - - Image(systemName: "chevron.right") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.tertiary) - } - } - .buttonStyle(.plain) - }, footer: { - Text(NSLocalizedString("_focused_auto_upload_settings_footer_", comment: "")) - .font(.footnote) - }) - } } .safeAreaInset(edge: .bottom) { autoUploadStartButton diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index 3807a7c861..bb053a4249 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -42,10 +42,16 @@ struct NCSettingsView: View { isAnimated: model.autoUploadStart) .frame(width: 39) - Text(model.autoUploadStart - ? model.autoUploadCountMessage - : NSLocalizedString("_settings_autoupload_", comment: "")) - .font(.body) + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("_settings_autoupload_", comment: "")) + .font(.body) + + if model.autoUploadStart { + Text(model.autoUploadCountMessage) + .font(.footnote) + .foregroundStyle(.secondary) + } + } } } }, footer: { From f43c8ac516247d7e5d67711a579c937b7822ac46 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Sat, 23 May 2026 18:09:31 +0200 Subject: [PATCH 07/18] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 4 + iOSClient/DeepLink/NCDeepLinkHandler.swift | 1 + .../AutoUpload/NCAutoUploadCounter.swift | 84 ++++++++++++++++ .../AutoUpload/NCAutoUploadView.swift | 38 +++++++- .../NCFocusedAutoUploadProgressView.swift | 97 +++++++++++++------ .../Settings/Settings/NCSettingsModel.swift | 32 ------ .../Settings/Settings/NCSettingsView.swift | 30 +++++- .../en.lproj/Localizable.strings | 9 +- .../en.lproj/Localizable.stringsdict | 4 +- 9 files changed, 226 insertions(+), 73 deletions(-) create mode 100644 iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index a0a0b58158..2050510b90 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */; }; AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; }; AAFC0D092F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */; }; + AAFC0D0B2F9AA10000F0A001 /* NCAutoUploadCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */; }; AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; @@ -1239,6 +1240,7 @@ AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = ""; }; AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; }; AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadCloudAnimation.swift; sourceTree = ""; }; + AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadCounter.swift; sourceTree = ""; }; AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; @@ -2670,6 +2672,7 @@ isa = PBXGroup; children = ( F39A1EE12D0AF8A200DAD522 /* Albums.swift */, + AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */, F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */, F71D2FB62E09BBD700B751CC /* NCAutoUploadModel.swift */, AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */, @@ -4801,6 +4804,7 @@ AAFC0D052F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift in Sources */, AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */, AAFC0D092F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift in Sources */, + AAFC0D0B2F9AA10000F0A001 /* NCAutoUploadCounter.swift in Sources */, F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */, F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */, F75D19E325EFE09000D74598 /* NCContextMenuTrash.swift in Sources */, diff --git a/iOSClient/DeepLink/NCDeepLinkHandler.swift b/iOSClient/DeepLink/NCDeepLinkHandler.swift index d3df189e87..53178755e3 100644 --- a/iOSClient/DeepLink/NCDeepLinkHandler.swift +++ b/iOSClient/DeepLink/NCDeepLinkHandler.swift @@ -153,6 +153,7 @@ class NCDeepLinkHandler { navigationController.popToRootViewController(animated: false) let autoUploadView = NCAutoUploadView(model: NCAutoUploadModel(controller: controller), albumModel: AlbumModel(controller: controller)) + .environment(NCAutoUploadCounter()) let autoUploadController = UIHostingController(rootView: autoUploadView) navigationController.pushViewController(autoUploadController, animated: true) } diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift new file mode 100644 index 0000000000..b828769747 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Observation + +@MainActor +@Observable +final class NCAutoUploadCounter { + private(set) var count = 0 + private(set) var isLoaded = false + + var hasItemsToUpload: Bool { + return isLoaded && count > 0 + } + + var itemsLeftMessage: String { + if count == 0 { + return NSLocalizedString("_auto_upload_no_new_items_to_upload_", comment: "") + } + + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_items_left_", comment: ""), count) + } + + var photosToBackUpMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), count) + } + + @ObservationIgnored private var pollTask: Task? + + func start(account: String, + urlBase: String, + userId: String, + autoUploadStart: Bool) { + guard autoUploadStart else { + stopPolling(reset: true) + return + } + + startPolling(account: account, urlBase: urlBase, userId: userId) + } + + func stop(reset: Bool = false) { + stopPolling(reset: reset) + } + + private func startPolling(account: String, urlBase: String, userId: String) { + stopPolling(reset: false) + isLoaded = false + + pollTask = Task { @MainActor in + let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, + urlBase: urlBase, + userId: userId) + + while !Task.isCancelled { + let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() + let newCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, + autoUploadServerUrlBase: autoUploadServerUrlBase, + transfersSuccess: transfersSuccess) + + guard !Task.isCancelled else { + return + } + + count = newCount + isLoaded = true + + try? await Task.sleep(for: .seconds(2)) + } + } + } + + private func stopPolling(reset: Bool) { + pollTask?.cancel() + pollTask = nil + + if reset { + count = 0 + isLoaded = false + } + } +} diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 3fc8843c10..2a9cc261f7 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -7,6 +7,7 @@ import SwiftUI import UIKit /// A view that allows the user to configure the `auto upload settings for Nextcloud` +@MainActor struct NCAutoUploadView: View { @State private var reachedAnchor = false @@ -19,6 +20,7 @@ struct NCAutoUploadView: View { @State private var showFocusedAutoUploadProgress = false @State private var openFocusedAutoUploadAfterIntro = false @State private var startAutoUpload = false + @Environment(NCAutoUploadCounter.self) private var autoUploadCounter var body: some View { ZStack { @@ -30,8 +32,26 @@ struct NCAutoUploadView: View { } .navigationBarTitle(NSLocalizedString("_auto_upload_folder_", comment: "")) .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + VStack(spacing: 0) { + Text(NSLocalizedString("_auto_upload_folder_", comment: "")) + .font(.headline) + + if model.autoUploadStart && autoUploadCounter.isLoaded { + Text(autoUploadCounter.itemsLeftMessage) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + } .onAppear { model.onViewAppear() + updateAutoUploadCounterSubscription() + } + .onDisappear { + stopAutoUploadCounterSubscription() } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in model.checkPermission() @@ -56,6 +76,8 @@ struct NCAutoUploadView: View { guard openFocusedAutoUploadAfterIntro else { return } openFocusedAutoUploadAfterIntro = false + guard autoUploadCounter.hasItemsToUpload else { return } + showFocusedAutoUploadProgress = true }) { NCFocusedAutoUploadIntroView { @@ -69,6 +91,7 @@ struct NCAutoUploadView: View { account: model.session.account, urlBase: model.session.urlBase, userId: model.session.userId) + .environment(autoUploadCounter) } .onChange(of: model.autoUploadStart) { _, newValue in if !newValue { @@ -76,13 +99,14 @@ struct NCAutoUploadView: View { showFocusedAutoUploadProgress = false openFocusedAutoUploadAfterIntro = false } + updateAutoUploadCounterSubscription() } } @ViewBuilder var autoUploadOnView: some View { Form { - if model.autoUploadStart { + if model.autoUploadStart && autoUploadCounter.hasItemsToUpload { Section(content: { Button { showFocusedAutoUploadIntro = true @@ -304,6 +328,17 @@ struct NCAutoUploadView: View { } }) } + + private func updateAutoUploadCounterSubscription() { + autoUploadCounter.start(account: model.session.account, + urlBase: model.session.urlBase, + userId: model.session.userId, + autoUploadStart: model.autoUploadStart) + } + + private func stopAutoUploadCounterSubscription() { + autoUploadCounter.stop() + } } @ViewBuilder @@ -437,4 +472,5 @@ struct ConfirmAutoUploadSheet: View { #Preview { NCAutoUploadView(model: NCAutoUploadModel(controller: nil), albumModel: AlbumModel(controller: nil)) + .environment(NCAutoUploadCounter()) } diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index 2aa3e20b67..c125b456d5 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -14,10 +14,10 @@ struct NCFocusedAutoUploadProgressView: View { let userId: String @State private var countdownTask: Task? - @State private var uploadCountTask: Task? - @State private var autoUploadCount = 0 + @State private var isUploadCompleted = false @State private var secondsUntilDim = 10 @State private var isScreenDimmed = false + @Environment(NCAutoUploadCounter.self) private var autoUploadCounter private let dimDelay = 10 @@ -38,34 +38,42 @@ struct NCFocusedAutoUploadProgressView: View { .padding(.horizontal, 36) VStack(spacing: 6) { - Text(NSLocalizedString("_focused_auto_upload_backing_up_", comment: "")) + Text(isUploadCompleted + ? NSLocalizedString("_focused_auto_upload_completed_", comment: "") + : NSLocalizedString("_focused_auto_upload_backing_up_", comment: "")) .font(.largeTitle) .fontWeight(.semibold) .foregroundStyle(.white) .multilineTextAlignment(.center) - Text(uploadCountMessage) - .font(.title3) - .foregroundStyle(.white.opacity(0.9)) - .multilineTextAlignment(.center) + if autoUploadCounter.isLoaded && !isUploadCompleted { + Text(uploadCountMessage) + .font(.title3) + .foregroundStyle(.white.opacity(0.9)) + .multilineTextAlignment(.center) + } } } Spacer() - Text(statusMessage) - .font(.title3) - .foregroundStyle(.white.opacity(0.9)) - .multilineTextAlignment(.center) - .padding(.horizontal, 28) - .fixedSize(horizontal: false, vertical: true) + if !isUploadCompleted { + Text(statusMessage) + .font(.title3) + .foregroundStyle(.white.opacity(0.9)) + .multilineTextAlignment(.center) + .padding(.horizontal, 28) + .fixedSize(horizontal: false, vertical: true) + } Spacer() Button { isPresented = false } label: { - Text(NSLocalizedString("_stop_focused_auto_upload_", comment: "")) + Text(isUploadCompleted + ? NSLocalizedString("_finish_", comment: "") + : NSLocalizedString("_stop_focused_auto_upload_", comment: "")) .font(.title3) .foregroundStyle(.white) .frame(maxWidth: .infinity) @@ -105,6 +113,12 @@ struct NCFocusedAutoUploadProgressView: View { stopFocusedMode() } } + .onChange(of: autoUploadCounter.count) { + updateFocusedCompletionState() + } + .onChange(of: autoUploadCounter.isLoaded) { + updateFocusedCompletionState() + } } private var statusMessage: String { @@ -112,16 +126,20 @@ struct NCFocusedAutoUploadProgressView: View { } private var uploadCountMessage: String { - return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), autoUploadCount) + return autoUploadCounter.photosToBackUpMessage } private func startFocusedMode() { + guard !isUploadCompleted else { + return + } + countdownTask?.cancel() secondsUntilDim = dimDelay isScreenDimmed = false NCFocusedAutoUploadScreenDimmer.shared.startKeepingScreenAwake() - startUploadCountPolling() + updateAutoUploadCounterSubscription() countdownTask = Task { @MainActor in while secondsUntilDim > 0 { @@ -138,8 +156,7 @@ struct NCFocusedAutoUploadProgressView: View { private func stopFocusedMode() { countdownTask?.cancel() countdownTask = nil - uploadCountTask?.cancel() - uploadCountTask = nil + stopAutoUploadCounterSubscription() isScreenDimmed = false NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() } @@ -149,21 +166,39 @@ struct NCFocusedAutoUploadProgressView: View { startFocusedMode() } - private func startUploadCountPolling() { - uploadCountTask?.cancel() + private func updateAutoUploadCounterSubscription() { + autoUploadCounter.start(account: account, + urlBase: urlBase, + userId: userId, + autoUploadStart: true) + updateFocusedCompletionState() + } - uploadCountTask = Task { @MainActor in - let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, - urlBase: urlBase, - userId: userId) + private func stopAutoUploadCounterSubscription() { + autoUploadCounter.stop() + } - while !Task.isCancelled { - let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() - autoUploadCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, - autoUploadServerUrlBase: autoUploadServerUrlBase, - transfersSuccess: transfersSuccess) - try? await Task.sleep(for: .seconds(2)) - } + private func updateFocusedCompletionState() { + guard autoUploadCounter.isLoaded, + autoUploadCounter.count == 0 else { + return + } + + completeFocusedUpload() + } + + private func completeFocusedUpload() { + guard !isUploadCompleted else { + return + } + + isUploadCompleted = true + countdownTask?.cancel() + countdownTask = nil + stopAutoUploadCounterSubscription() + + if !isScreenDimmed { + NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() } } } diff --git a/iOSClient/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index 79b1d82267..3b0a3ff446 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -24,8 +24,6 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { @Published var resetWrongAttempts: Bool = false // State to control the auto upload status indicator @Published var autoUploadStart: Bool = false - // State to control the auto upload queue count - @Published var autoUploadCount: Int = 0 // Request account on start @Published var accountRequest: Bool = false // Root View Controller @@ -57,44 +55,14 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { privacyScreen = keychain.privacyScreenEnabled resetWrongAttempts = keychain.resetAppCounterFail autoUploadStart = NCManageDatabase.shared.getTableAccount(account: session.account)?.autoUploadStart ?? false - if !autoUploadStart { - autoUploadCount = 0 - } accountRequest = keychain.accountRequest footerApp = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, NCUtility().getVersionBuild()) + "\n\n" footerServer = String(format: NCBrandOptions.shared.textCopyrightNextcloudServer, capabilities.serverVersion) + "\n" footerSlogan = capabilities.themingName + " - " + capabilities.themingSlogan + "\n\n" } - var autoUploadCountMessage: String { - return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_items_left_", comment: ""), autoUploadCount) - } - // MARK: - All functions - @MainActor - func pollAutoUploadCount() async { - guard autoUploadStart else { - autoUploadCount = 0 - return - } - - let account = session.account - let urlBase = session.urlBase - let userId = session.userId - let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, - urlBase: urlBase, - userId: userId) - - while autoUploadStart && !Task.isCancelled { - let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() - autoUploadCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, - autoUploadServerUrlBase: autoUploadServerUrlBase, - transfersSuccess: transfersSuccess) - try? await Task.sleep(for: .seconds(2)) - } - } - /// Function to update Touch ID / Face ID setting func updateTouchIDSetting() { keychain.touchFaceID = enableTouchFaceID diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index bb053a4249..fb3e651dfd 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -8,6 +8,7 @@ import NextcloudKit import FirebaseCrashlytics /// Settings view for Nextcloud +@MainActor struct NCSettingsView: View { // State to control the visibility of the acknowledgements view @State private var showAcknowledgements = false @@ -21,6 +22,7 @@ struct NCSettingsView: View { @State private var showSourceCode = false // Object of ViewModel of this view @ObservedObject var model: NCSettingsModel + @State private var autoUploadCounter = NCAutoUploadCounter() var capabilities: NKCapabilities.Capabilities { NCNetworking.shared.capabilities[model.controller?.account ?? ""] ?? NKCapabilities.Capabilities() @@ -32,6 +34,7 @@ struct NCSettingsView: View { Section(content: { NavigationLink(destination: LazyView { NCAutoUploadView(model: NCAutoUploadModel(controller: model.controller), albumModel: AlbumModel(controller: model.controller)) + .environment(autoUploadCounter) }) { HStack { NCFocusedAutoUploadCloudAnimation(size: 44, @@ -46,8 +49,8 @@ struct NCSettingsView: View { Text(NSLocalizedString("_settings_autoupload_", comment: "")) .font(.body) - if model.autoUploadStart { - Text(model.autoUploadCountMessage) + if model.autoUploadStart && autoUploadCounter.isLoaded { + Text(autoUploadCounter.itemsLeftMessage) .font(.footnote) .foregroundStyle(.secondary) } @@ -322,9 +325,28 @@ struct NCSettingsView: View { } .navigationBarTitle(NSLocalizedString("_settings_", comment: "")) .defaultViewModifier(model) - .task(id: model.autoUploadStart) { - await model.pollAutoUploadCount() + .onAppear { + updateAutoUploadCounterSubscription() } + .onDisappear { + stopAutoUploadCounterSubscription() + } + .onChange(of: model.autoUploadStart) { + updateAutoUploadCounterSubscription() + } + } + + private func updateAutoUploadCounterSubscription() { + let session = model.session + + autoUploadCounter.start(account: session.account, + urlBase: session.urlBase, + userId: session.userId, + autoUploadStart: model.autoUploadStart) + } + + private func stopAutoUploadCounterSubscription() { + autoUploadCounter.stop() } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 86d091d821..8d955a6768 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -764,17 +764,20 @@ You can stop it at any time, adjust the settings, and enable it again."; "_back_up_new_photos_only_" = "Back up new photos/videos only"; "_auto_upload_all_photos_warning_title_" = "Are you sure you want to upload all photos?"; "_auto_upload_all_photos_warning_message_" = "This can take some time to process depending on the amount of photos."; +"_auto_upload_no_new_items_to_upload_" = "No new items to upload"; "_focused_auto_upload_" = "Focused Auto Upload"; -"_focused_auto_upload_settings_footer_" = "Keep the app open and darken the screen while auto upload is backing up."; -"_focused_auto_upload_intro_heading_" = "Keep backing up with the screen darkened"; -"_focused_auto_upload_intro_message_" = "Photos and videos will continue backing up while the screen is darkened. During the process:"; +"_focused_auto_upload_settings_footer_" = "Keep the app open and darken the screen while auto upload is backing up at higher speed."; +"_focused_auto_upload_intro_heading_" = "Keep backing up with the screen darkened."; +"_focused_auto_upload_intro_message_" = "Photos and videos will continue backing up at higher speed while the screen is darkened. During the process:"; "_focused_auto_upload_wifi_" = "Connect to Wi-Fi"; "_focused_auto_upload_charger_" = "Connect to charger"; "_focused_auto_upload_do_not_exit_" = "Do not exit the app"; "_enable_focused_auto_upload_" = "Enable Focused Auto Upload"; "_focused_auto_upload_backing_up_" = "Backing up"; +"_focused_auto_upload_completed_" = "Upload completed!"; "_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."; // MARK: Migration Multi Domains diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict index ef4ad41a9e..5cf32a2b53 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict +++ b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict @@ -45,9 +45,9 @@ NSStringFormatValueTypeKey d one - You have %d photo to back up + You have %d item to back up other - You have %d photos to back up + You have %d items to back up From 4d5c03837626648cd70c8d3932b9397dde769140 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 2 Jun 2026 14:06:11 +0200 Subject: [PATCH 08/18] WIP Signed-off-by: Milen Pivchev --- .../Data/NCManageDatabase+AutoUpload.swift | 31 +++++++++++-------- .../Networking/NCNetworkingProcess.swift | 5 ++- .../AutoUpload/NCAutoUploadCounter.swift | 27 +++++++++++++--- .../AutoUpload/NCAutoUploadView.swift | 2 +- .../NCFocusedAutoUploadProgressView.swift | 7 +++++ .../Settings/Settings/NCSettingsView.swift | 2 +- .../en.lproj/Localizable.stringsdict | 16 ++++++++++ 7 files changed, 70 insertions(+), 20 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+AutoUpload.swift b/iOSClient/Data/NCManageDatabase+AutoUpload.swift index 834933a61f..4a0fd35311 100644 --- a/iOSClient/Data/NCManageDatabase+AutoUpload.swift +++ b/iOSClient/Data/NCManageDatabase+AutoUpload.swift @@ -108,7 +108,7 @@ extension NCManageDatabase { func countAutoUploadMetadatasAsync(account: String, autoUploadServerUrlBase: String, - transfersSuccess: [tableMetadata] = []) async -> Int { + transfersSuccess: [tableMetadata] = []) async -> (pending: Int, failed: Int) { let global = NCGlobal.shared let excludedIds = Set(transfersSuccess.compactMap { metadata -> String? in guard metadata.account == account, @@ -121,22 +121,27 @@ extension NCManageDatabase { return metadata.ocIdTransfer }) - return await core.performRealmReadAsync { realm in - let results = realm.objects(tableMetadata.self) - .filter("account == %@ AND autoUploadServerUrlBase == %@ AND directory == false AND sessionSelector == %@ AND status IN %@", + let pendingStatuses = [global.metadataStatusWaitUpload, global.metadataStatusUploading] + let failedStatuses = [global.metadataStatusUploadError] + + let result = await core.performRealmReadAsync { realm -> (pending: Int, failed: Int) in + let scope = realm.objects(tableMetadata.self) + .filter("account == %@ AND autoUploadServerUrlBase == %@ AND directory == false AND sessionSelector == %@", account, autoUploadServerUrlBase, - global.selectorUploadAutoUpload, - global.metadataStatusUploadingAllMode) + global.selectorUploadAutoUpload) - guard !excludedIds.isEmpty else { - return results.count - } + let pendingResults = scope.filter("status IN %@", pendingStatuses) + let pendingCount = excludedIds.isEmpty + ? pendingResults.count + : pendingResults.filter("NOT (ocIdTransfer IN %@)", Array(excludedIds)).count + + let failedCount = scope.filter("status IN %@", failedStatuses).count + + return (pending: pendingCount, failed: failedCount) + } - return results - .filter("NOT (ocIdTransfer IN %@)", Array(excludedIds)) - .count - } ?? 0 + return result ?? (pending: 0, failed: 0) } func existsAutoUpload(account: String, diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 930a965320..bfdac6d5b5 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -130,7 +130,10 @@ actor NCNetworkingProcess { let countTransferDownloadingUploadingSuccess = await NCNetworking.shared.metadataTranfersSuccess.count(statuses: NCGlobal.shared.metadatasStatusDownloadingUploading) let countWaitingDownloadUpload = await NCManageDatabase.shared.getMetadatasStatusCountAsync(status: NCGlobal.shared.metadatasStatusInWaitingDownloadUpload) let count = max(0, countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) - + print("----") + print(countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) + print(countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) + print(countTransferDownloadingUploadingSuccess) return count } diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift index b828769747..82a1d91329 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift @@ -9,6 +9,7 @@ import Observation @Observable final class NCAutoUploadCounter { private(set) var count = 0 + private(set) var failedCount = 0 private(set) var isLoaded = false var hasItemsToUpload: Bool { @@ -27,6 +28,22 @@ final class NCAutoUploadCounter { return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), count) } + var failedMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_failed_", comment: ""), failedCount) + } + + var itemsLeftSummary: String { + if failedCount == 0 { + return itemsLeftMessage + } + + if count == 0 { + return failedMessage + } + + return itemsLeftMessage + " · " + failedMessage + } + @ObservationIgnored private var pollTask: Task? func start(account: String, @@ -56,15 +73,16 @@ final class NCAutoUploadCounter { while !Task.isCancelled { let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() - let newCount = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, - autoUploadServerUrlBase: autoUploadServerUrlBase, - transfersSuccess: transfersSuccess) + let counts = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, + autoUploadServerUrlBase: autoUploadServerUrlBase, + transfersSuccess: transfersSuccess) guard !Task.isCancelled else { return } - count = newCount + count = counts.pending + failedCount = counts.failed isLoaded = true try? await Task.sleep(for: .seconds(2)) @@ -78,6 +96,7 @@ final class NCAutoUploadCounter { if reset { count = 0 + failedCount = 0 isLoaded = false } } diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 2a9cc261f7..677fb27942 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -39,7 +39,7 @@ struct NCAutoUploadView: View { .font(.headline) if model.autoUploadStart && autoUploadCounter.isLoaded { - Text(autoUploadCounter.itemsLeftMessage) + Text(autoUploadCounter.itemsLeftSummary) .font(.caption2) .foregroundStyle(.secondary) } diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index c125b456d5..2ad4465040 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -52,6 +52,13 @@ struct NCFocusedAutoUploadProgressView: View { .foregroundStyle(.white.opacity(0.9)) .multilineTextAlignment(.center) } + + if autoUploadCounter.failedCount > 0 { + Text(autoUploadCounter.failedMessage) + .font(.footnote) + .foregroundStyle(.white.opacity(0.7)) + .multilineTextAlignment(.center) + } } } diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index fb3e651dfd..8812897be8 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -50,7 +50,7 @@ struct NCSettingsView: View { .font(.body) if model.autoUploadStart && autoUploadCounter.isLoaded { - Text(autoUploadCounter.itemsLeftMessage) + Text(autoUploadCounter.itemsLeftSummary) .font(.footnote) .foregroundStyle(.secondary) } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict index 5cf32a2b53..3696c67892 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict +++ b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict @@ -50,5 +50,21 @@ You have %d items to back up + _focused_auto_upload_failed_ + + NSStringLocalizedFormatKey + %#@failed@ + failed + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d failed + other + %d failed + + From 1c7b9e0fc8c9f4fa00fbe271655c938956933bf8 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 2 Jun 2026 15:32:04 +0200 Subject: [PATCH 09/18] WIP Signed-off-by: Milen Pivchev --- .../Data/NCManageDatabase+AutoUpload.swift | 22 +++------------- .../Networking/NCNetworkingProcess.swift | 25 ++++++++----------- .../AutoUpload/NCAutoUploadCounter.swift | 4 +-- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+AutoUpload.swift b/iOSClient/Data/NCManageDatabase+AutoUpload.swift index 4a0fd35311..084b4a9fd2 100644 --- a/iOSClient/Data/NCManageDatabase+AutoUpload.swift +++ b/iOSClient/Data/NCManageDatabase+AutoUpload.swift @@ -107,21 +107,9 @@ extension NCManageDatabase { } func countAutoUploadMetadatasAsync(account: String, - autoUploadServerUrlBase: String, - transfersSuccess: [tableMetadata] = []) async -> (pending: Int, failed: Int) { + autoUploadServerUrlBase: String) async -> (pending: Int, failed: Int) { let global = NCGlobal.shared - let excludedIds = Set(transfersSuccess.compactMap { metadata -> String? in - guard metadata.account == account, - metadata.sessionSelector == global.selectorUploadAutoUpload, - metadata.autoUploadServerUrlBase == autoUploadServerUrlBase, - !metadata.ocIdTransfer.isEmpty else { - return nil - } - - return metadata.ocIdTransfer - }) - - let pendingStatuses = [global.metadataStatusWaitUpload, global.metadataStatusUploading] + let pendingStatuses = global.metadatasStatusInWaitingDownloadUpload + global.metadatasStatusDownloadingUploading let failedStatuses = [global.metadataStatusUploadError] let result = await core.performRealmReadAsync { realm -> (pending: Int, failed: Int) in @@ -131,11 +119,7 @@ extension NCManageDatabase { autoUploadServerUrlBase, global.selectorUploadAutoUpload) - let pendingResults = scope.filter("status IN %@", pendingStatuses) - let pendingCount = excludedIds.isEmpty - ? pendingResults.count - : pendingResults.filter("NOT (ocIdTransfer IN %@)", Array(excludedIds)).count - + let pendingCount = scope.filter("status IN %@", pendingStatuses).count let failedCount = scope.filter("status IN %@", failedStatuses).count return (pending: pendingCount, failed: failedCount) diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index bfdac6d5b5..ebeb0c60d8 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -28,7 +28,7 @@ actor NCNetworkingProcess { private var enableControllingScreenAwake = true private var currentAccount = "" - private var inWaitDownloadUploadCount: Int = 0 + private var lastScheduledAndInProgressCount: Int = 0 private var timer: DispatchSourceTimer? private let timerQueue = DispatchQueue(label: "com.nextcloud.timerProcess", qos: .utility) @@ -70,7 +70,7 @@ actor NCNetworkingProcess { guard let self else { return } Task { - let count = await self.inWaitingDownloadUploadCount() + let count = await self.scheduledAndInProgressCount() try? await UNUserNotificationCenter.current().setBadgeCount(count) await self.stopTimer() @@ -126,15 +126,10 @@ actor NCNetworkingProcess { currentAccount = account } - private func inWaitingDownloadUploadCount() async -> Int { - let countTransferDownloadingUploadingSuccess = await NCNetworking.shared.metadataTranfersSuccess.count(statuses: NCGlobal.shared.metadatasStatusDownloadingUploading) - let countWaitingDownloadUpload = await NCManageDatabase.shared.getMetadatasStatusCountAsync(status: NCGlobal.shared.metadatasStatusInWaitingDownloadUpload) - let count = max(0, countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) - print("----") - print(countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) - print(countWaitingDownloadUpload - countTransferDownloadingUploadingSuccess) - print(countTransferDownloadingUploadingSuccess) - return count + private func scheduledAndInProgressCount() async -> Int { + let statuses = NCGlobal.shared.metadatasStatusInWaitingDownloadUpload + NCGlobal.shared.metadatasStatusDownloadingUploading + + return await NCManageDatabase.shared.getMetadatasStatusCountAsync(status: statuses) } func startTimer(interval: TimeInterval) async { @@ -201,11 +196,11 @@ actor NCNetworkingProcess { return } - // UPDATE INWAIT DOWNLOAD UPLOAD & BADGE + // UPDATE SCHEDULED + IN PROGRESS & BADGE // - let count = await inWaitingDownloadUploadCount() - if count != inWaitDownloadUploadCount { - inWaitDownloadUploadCount = count + let count = await scheduledAndInProgressCount() + if count != lastScheduledAndInProgressCount { + lastScheduledAndInProgressCount = count Task { @MainActor in if let controller = getRootController(), let files = controller.tabBar.items?.first { diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift index 82a1d91329..37c1a99dd7 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift @@ -72,10 +72,8 @@ final class NCAutoUploadCounter { userId: userId) while !Task.isCancelled { - let transfersSuccess = await NCNetworking.shared.metadataTranfersSuccess.getAll() let counts = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, - autoUploadServerUrlBase: autoUploadServerUrlBase, - transfersSuccess: transfersSuccess) + autoUploadServerUrlBase: autoUploadServerUrlBase) guard !Task.isCancelled else { return From fece469c9c0245d65280bd73d19ab5fb5a232fcf Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 2 Jun 2026 17:35:56 +0200 Subject: [PATCH 10/18] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 12 +- iOSClient/NCGlobal.swift | 1 + .../Networking/NCNetworkingProcess.swift | 2 + .../AutoUpload/NCAutoUploadCounter.swift | 101 --------------- .../AutoUpload/NCAutoUploadModel.swift | 55 +++++--- .../AutoUpload/NCAutoUploadView.swift | 4 +- .../Utility/NCAutoUploadCounter.swift | 122 ++++++++++++++++++ .../NCFocusedAutoUploadScreenDimmer.swift | 0 8 files changed, 171 insertions(+), 126 deletions(-) delete mode 100644 iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift create mode 100644 iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift rename iOSClient/Settings/AutoUpload/{ => Utility}/NCFocusedAutoUploadScreenDimmer.swift (100%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 3f27d5a438..2c91329913 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -2198,6 +2198,15 @@ path = StatusMessage; sourceTree = ""; }; + F3896B062FCF2CF300DA2B18 /* Utility */ = { + isa = PBXGroup; + children = ( + AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */, + AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */, + ); + path = Utility; + sourceTree = ""; + }; F389C9F32CEE381E00049762 /* SelectAlbum */ = { isa = PBXGroup; children = ( @@ -2671,13 +2680,12 @@ F76882162C0DD1E7001CF441 /* AutoUpload */ = { isa = PBXGroup; children = ( + F3896B062FCF2CF300DA2B18 /* Utility */, F39A1EE12D0AF8A200DAD522 /* Albums.swift */, - AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */, F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */, F71D2FB62E09BBD700B751CC /* NCAutoUploadModel.swift */, AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */, AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */, - AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */, AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */, ); path = AutoUpload; diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 2e6ad38aee..8ca394dfc4 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -291,6 +291,7 @@ final class NCGlobal: Sendable { let notificationCenterUserInteractionMonitor = "serInteractionMonitor" let notificationCenterNetworkingProcess = "networkingProcess" + let notificationCenterTransferCountChanged = "transferCountChanged" // Networking Status let networkingStatusCreateFolder = "statusCreateFolder" diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index ebeb0c60d8..5f8e8d20bd 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -207,6 +207,8 @@ actor NCNetworkingProcess { files.badgeValue = count == 0 ? nil : self.utility.formatBadgeCount(count) } } + + NotificationCenter.default.post(name: NSNotification.Name(rawValue: global.notificationCenterTransferCountChanged), object: nil) } // METADATAS diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift deleted file mode 100644 index 37c1a99dd7..0000000000 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2026 Milen Pivchev -// SPDX-License-Identifier: GPL-3.0-or-later - -import Foundation -import Observation - -@MainActor -@Observable -final class NCAutoUploadCounter { - private(set) var count = 0 - private(set) var failedCount = 0 - private(set) var isLoaded = false - - var hasItemsToUpload: Bool { - return isLoaded && count > 0 - } - - var itemsLeftMessage: String { - if count == 0 { - return NSLocalizedString("_auto_upload_no_new_items_to_upload_", comment: "") - } - - return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_items_left_", comment: ""), count) - } - - var photosToBackUpMessage: String { - return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), count) - } - - var failedMessage: String { - return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_failed_", comment: ""), failedCount) - } - - var itemsLeftSummary: String { - if failedCount == 0 { - return itemsLeftMessage - } - - if count == 0 { - return failedMessage - } - - return itemsLeftMessage + " · " + failedMessage - } - - @ObservationIgnored private var pollTask: Task? - - func start(account: String, - urlBase: String, - userId: String, - autoUploadStart: Bool) { - guard autoUploadStart else { - stopPolling(reset: true) - return - } - - startPolling(account: account, urlBase: urlBase, userId: userId) - } - - func stop(reset: Bool = false) { - stopPolling(reset: reset) - } - - private func startPolling(account: String, urlBase: String, userId: String) { - stopPolling(reset: false) - isLoaded = false - - pollTask = Task { @MainActor in - let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, - urlBase: urlBase, - userId: userId) - - while !Task.isCancelled { - let counts = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, - autoUploadServerUrlBase: autoUploadServerUrlBase) - - guard !Task.isCancelled else { - return - } - - count = counts.pending - failedCount = counts.failed - isLoaded = true - - try? await Task.sleep(for: .seconds(2)) - } - } - } - - private func stopPolling(reset: Bool) { - pollTask?.cancel() - pollTask = nil - - if reset { - count = 0 - failedCount = 0 - isLoaded = false - } - } -} diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift index 0bdabce6cb..9845f8962e 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadModel.swift @@ -17,46 +17,50 @@ enum AutoUploadTimespan: String, CaseIterable, Identifiable { /// A model that allows the user to configure the `auto upload settings for Nextcloud` class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { - // A state variable that indicates whether auto upload for photos is enabled or not + /// Whether auto upload for photos is enabled or not @Published var autoUploadImage: Bool = false - // A state variable that indicates whether auto upload for photos is restricted to Wi-Fi only or not + /// Whether auto upload for photos is restricted to Wi-Fi only or not @Published var autoUploadWWAnPhoto: Bool = false - // A state variable that indicates whether auto upload for videos is enabled or not + /// Whether auto upload for videos is enabled or not @Published var autoUploadVideo: Bool = false - // A state variable that indicates whether auto upload for videos is enabled or not + /// Whether auto upload for videos is enabled or not @Published var autoUploadWWAnVideo: Bool = false - // A state variable that indicates whether auto upload is enabled or not + /// Whether auto upload is enabled or not @Published var autoUploadStart: Bool = false - // A state variable that indicates whether auto upload creates subfolders based on date or not + /// Whether auto upload creates subfolders based on date or not @Published var autoUploadCreateSubfolder: Bool = false - // A state variable that indicates the granularity of the subfolders, either daily, monthly, or yearly + /// The granularity of the subfolders, either daily, monthly, or yearly @Published var autoUploadSubfolderGranularity: Granularity = .monthly - // A state variable that indicates the date from when new photos/videos will be uploaded. + /// The date from when new photos/videos will be uploaded. @Published var autoUploadSinceDate: Date? - // A state variable that indicates whether a warning should be shown if all photos must be uploaded. + /// Whether a warning should be shown if all photos must be uploaded. @Published var showUploadAllPhotosWarning = false - // A state variable that indicates whether Photos permissions have been granted or not. + /// Whether Photos permissions have been granted or not. @Published var photosPermissionsGranted = true - // - @Published var permissionGranted: Bool = false + /// Whether `Always` location authorization has been granted, enabling background location-based auto upload. + @Published var locationAutoUploadPermissionGranted: Bool = false - // A state variable that shows error in view in case of an error + /// Whether the error alert should be shown in the view. @Published var showErrorAlert: Bool = false + /// The currently displayed section name. @Published var sectionName = "" + /// Whether the user is authorized. @Published var isAuthorized: Bool = false - // A string variable that contains error text + /// Error text shown to the user. @Published var error: String = "" + /// Shared Nextcloud database instance. let database = NCManageDatabase.shared - // Root View Controller + /// Root view controller used to present UI from this model. var controller: NCMainTabBarController? - // A variable user for change the auto upload directory + /// Server URL used to change the auto-upload directory. var serverUrl: String = "" - // Get session + /// The current account session. var session: NCSession.Session { NCSession.shared.getSession(controller: controller) } + /// The active window scene, used for presenting banners. var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: controller) } @@ -89,6 +93,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { // MARK: - All functions + /// Requests Photos library authorization and warns the user if background app refresh is disabled. func requestAuthorization() { PHPhotoLibrary.requestAuthorization { status in DispatchQueue.main.async { [self] in @@ -133,6 +138,7 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } } + /// Sets the cut-off date so only photos/videos created after it are uploaded. func handleAutoUploadOnlyNew(newValue: Bool) { if newValue { autoUploadSinceDate = Date.now @@ -209,6 +215,10 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } } + /// Returns a display title for the selected auto-upload albums. + /// + /// - Parameter autoUploadAlbumIds: The local identifiers of the selected albums. + /// - Returns: The album's localized title, "Camera Roll" for the user library, or a localized "multiple albums" string when more than one is selected. func createAlbumTitle(autoUploadAlbumIds: Set) -> String { if autoUploadAlbumIds.count == 1 { let album = PHAssetCollection.allAlbums.first(where: { autoUploadAlbumIds.first == $0.localIdentifier }) @@ -218,11 +228,13 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } } + /// Whether any auto-upload entry exists for the current account. func existsAutoUpload() -> Bool { let autoUploadServerUrlBase = NCManageDatabase.shared.getAccountAutoUploadServerUrlBase(session: session) return NCManageDatabase.shared.existsAutoUpload(account: session.account, autoUploadServerUrlBase: autoUploadServerUrlBase) } + /// Deletes pending auto-upload transfers for the current account. func deleteAutoUploadTransfer() { Task { let autoUploadServerUrlBase = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(session: session) @@ -230,25 +242,26 @@ class NCAutoUploadModel: ObservableObject, ViewOnAppearHandling { } } - /// Updates the auto-upload create subfolder setting. + /// Requests or revokes `Always` location authorization for background location-based auto upload. func handleLocationChange(newValue: Bool) { if let controller = self.controller { if newValue { Task { @MainActor in let result = await NCBackgroundLocationUploadManager.shared.requestAuthorizationAlwaysAsync(from: controller) - self.permissionGranted = result + self.locationAutoUploadPermissionGranted = result NCPreferences().location = result } } else { - self.permissionGranted = false + self.locationAutoUploadPermissionGranted = false NCPreferences().location = false } } } + /// Refreshes `locationAutoUploadPermissionGranted` from the current location authorization status and stored preference. func checkPermission() { let status = CLLocationManager().authorizationStatus - permissionGranted = (status == .authorizedAlways && NCPreferences().location) + locationAutoUploadPermissionGranted = (status == .authorizedAlways && NCPreferences().location) } } diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index 677fb27942..d303233a2b 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -278,11 +278,11 @@ struct NCAutoUploadView: View { // Location Section(content: { - Toggle(NSLocalizedString("_enable_background_location_title_", comment: ""), isOn: $model.permissionGranted) + Toggle(NSLocalizedString("_enable_background_location_title_", comment: ""), isOn: $model.locationAutoUploadPermissionGranted) .font(.body) .tint(Color(NCBrandColor.shared.getElement(account: model.session.account))) .opacity(model.autoUploadStart ? 0.15 : 1) - .onChange(of: model.permissionGranted) { _, newValue in + .onChange(of: model.locationAutoUploadPermissionGranted) { _, newValue in model.handleLocationChange(newValue: newValue) } }, footer: { diff --git a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift new file mode 100644 index 0000000000..98ef396375 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Observation + +@MainActor +@Observable +final class NCAutoUploadCounter { + private(set) var count = 0 + private(set) var failedCount = 0 + private(set) var isLoaded = false + + var hasItemsToUpload: Bool { + return isLoaded && count > 0 + } + + var itemsLeftMessage: String { + if count == 0 { + return NSLocalizedString("_auto_upload_no_new_items_to_upload_", comment: "") + } + + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_items_left_", comment: ""), count) + } + + var photosToBackUpMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), count) + } + + var failedMessage: String { + return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_failed_", comment: ""), failedCount) + } + + var itemsLeftSummary: String { + if failedCount == 0 { + return itemsLeftMessage + } + + if count == 0 { + return failedMessage + } + + return itemsLeftMessage + " · " + failedMessage + } + + @ObservationIgnored private var task: Task? + @ObservationIgnored private var observer: NSObjectProtocol? + @ObservationIgnored private var account: String? + @ObservationIgnored private var autoUploadServerUrlBase: String? + + func start(account: String, + urlBase: String, + userId: String, + autoUploadStart: Bool) { + guard autoUploadStart else { + stop(reset: true) + return + } + + // Transfers badge updates this so the update tick is the same. The subscription + // persists across screen changes; only an explicit reset tears it down. + if observer == nil { + observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterTransferCountChanged), + object: nil, + queue: .main) { [weak self] _ in + Task { @MainActor in + await self?.refresh() + } + } + } + + task?.cancel() + task = Task { @MainActor in + let base = await NCManageDatabase.shared.getAccountAutoUploadServerUrlBaseAsync(account: account, + urlBase: urlBase, + userId: userId) + + guard !Task.isCancelled else { + return + } + + self.account = account + self.autoUploadServerUrlBase = base + + await refresh() + } + } + + func stop(reset: Bool = false) { + guard reset else { + return + } + + task?.cancel() + task = nil + + if let observer { + NotificationCenter.default.removeObserver(observer) + self.observer = nil + } + + account = nil + autoUploadServerUrlBase = nil + count = 0 + failedCount = 0 + isLoaded = false + } + + private func refresh() async { + guard let account, let autoUploadServerUrlBase else { + return + } + + let counts = await NCManageDatabase.shared.countAutoUploadMetadatasAsync(account: account, + autoUploadServerUrlBase: autoUploadServerUrlBase) + + count = counts.pending + failedCount = counts.failed + isLoaded = true + } +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift b/iOSClient/Settings/AutoUpload/Utility/NCFocusedAutoUploadScreenDimmer.swift similarity index 100% rename from iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift rename to iOSClient/Settings/AutoUpload/Utility/NCFocusedAutoUploadScreenDimmer.swift From f5f31c1cd5d985ee1d2f5ebf81bec728fb701684 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 3 Jun 2026 13:32:07 +0200 Subject: [PATCH 11/18] WIP Signed-off-by: Milen Pivchev --- .../NCFocusedAutoUploadIntroView.swift | 4 ++ .../NCFocusedAutoUploadProgressView.swift | 40 +++++++++++++------ .../Utility/NCAutoUploadCounter.swift | 33 +++++++++------ 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift index 447a8ad93e..2774f55ca0 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -112,3 +112,7 @@ struct NCFocusedAutoUploadIntroView: View { } } } + +#Preview { + NCFocusedAutoUploadIntroView(onEnable: {}) +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index 2ad4465040..6cb288a881 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -52,13 +52,6 @@ struct NCFocusedAutoUploadProgressView: View { .foregroundStyle(.white.opacity(0.9)) .multilineTextAlignment(.center) } - - if autoUploadCounter.failedCount > 0 { - Text(autoUploadCounter.failedMessage) - .font(.footnote) - .foregroundStyle(.white.opacity(0.7)) - .multilineTextAlignment(.center) - } } } @@ -137,16 +130,20 @@ struct NCFocusedAutoUploadProgressView: View { } private func startFocusedMode() { - guard !isUploadCompleted else { + guard !isUploadCompleted, !isXcodeRunningForPreviews else { return } + startDimCountdown() + updateAutoUploadCounterSubscription() + } + + private func startDimCountdown() { countdownTask?.cancel() secondsUntilDim = dimDelay isScreenDimmed = false NCFocusedAutoUploadScreenDimmer.shared.startKeepingScreenAwake() - updateAutoUploadCounterSubscription() countdownTask = Task { @MainActor in while secondsUntilDim > 0 { @@ -186,12 +183,15 @@ struct NCFocusedAutoUploadProgressView: View { } private func updateFocusedCompletionState() { - guard autoUploadCounter.isLoaded, - autoUploadCounter.count == 0 else { + guard autoUploadCounter.isLoaded else { return } - completeFocusedUpload() + if autoUploadCounter.count == 0 { + completeFocusedUpload() + } else if isUploadCompleted { + resumeFocusedUpload() + } } private func completeFocusedUpload() { @@ -202,10 +202,24 @@ struct NCFocusedAutoUploadProgressView: View { isUploadCompleted = true countdownTask?.cancel() countdownTask = nil - stopAutoUploadCounterSubscription() if !isScreenDimmed { NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() } } + + private func resumeFocusedUpload() { + isUploadCompleted = false + startDimCountdown() + } +} + +#if DEBUG +#Preview { + NCFocusedAutoUploadProgressView(isPresented: .constant(true), + account: "preview", + urlBase: "https://cloud.example.com", + userId: "preview") + .environment(NCAutoUploadCounter(previewCount: 42)) } +#endif diff --git a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift index 98ef396375..d564353fd5 100644 --- a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift @@ -12,6 +12,16 @@ final class NCAutoUploadCounter { private(set) var failedCount = 0 private(set) var isLoaded = false + init() {} + +#if DEBUG + init(previewCount: Int, failedCount: Int = 0) { + self.count = previewCount + self.failedCount = failedCount + self.isLoaded = true + } +#endif + var hasItemsToUpload: Bool { return isLoaded && count > 0 } @@ -58,15 +68,12 @@ final class NCAutoUploadCounter { return } - // Transfers badge updates this so the update tick is the same. The subscription - // persists across screen changes; only an explicit reset tears it down. - if observer == nil { - observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterTransferCountChanged), - object: nil, - queue: .main) { [weak self] _ in - Task { @MainActor in - await self?.refresh() - } + // Transfers badge updates this so the update tick is the same. + observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterTransferCountChanged), + object: nil, + queue: .main) { [weak self] _ in + Task { @MainActor in + await self?.refresh() } } @@ -88,10 +95,6 @@ final class NCAutoUploadCounter { } func stop(reset: Bool = false) { - guard reset else { - return - } - task?.cancel() task = nil @@ -100,6 +103,10 @@ final class NCAutoUploadCounter { self.observer = nil } + guard reset else { + return + } + account = nil autoUploadServerUrlBase = nil count = 0 From b528dda91328b8dc9a6616f648b3649f334f0028 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 3 Jun 2026 13:35:09 +0200 Subject: [PATCH 12/18] WIP Signed-off-by: Milen Pivchev --- .../AutoUpload/NCFocusedAutoUploadProgressView.swift | 6 +----- .../Settings/AutoUpload/Utility/NCAutoUploadCounter.swift | 7 +++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift index 6cb288a881..5e7add7bc4 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -47,7 +47,7 @@ struct NCFocusedAutoUploadProgressView: View { .multilineTextAlignment(.center) if autoUploadCounter.isLoaded && !isUploadCompleted { - Text(uploadCountMessage) + Text(autoUploadCounter.photosToBackUpMessage) .font(.title3) .foregroundStyle(.white.opacity(0.9)) .multilineTextAlignment(.center) @@ -125,10 +125,6 @@ struct NCFocusedAutoUploadProgressView: View { return String(format: NSLocalizedString("_focused_auto_upload_countdown_", comment: ""), secondsUntilDim) } - private var uploadCountMessage: String { - return autoUploadCounter.photosToBackUpMessage - } - private func startFocusedMode() { guard !isUploadCompleted, !isXcodeRunningForPreviews else { return diff --git a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift index d564353fd5..b0aee0e586 100644 --- a/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift +++ b/iOSClient/Settings/AutoUpload/Utility/NCAutoUploadCounter.swift @@ -15,9 +15,8 @@ final class NCAutoUploadCounter { init() {} #if DEBUG - init(previewCount: Int, failedCount: Int = 0) { + init(previewCount: Int) { self.count = previewCount - self.failedCount = failedCount self.isLoaded = true } #endif @@ -26,7 +25,7 @@ final class NCAutoUploadCounter { return isLoaded && count > 0 } - var itemsLeftMessage: String { + private var itemsLeftMessage: String { if count == 0 { return NSLocalizedString("_auto_upload_no_new_items_to_upload_", comment: "") } @@ -38,7 +37,7 @@ final class NCAutoUploadCounter { return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_photos_to_back_up_", comment: ""), count) } - var failedMessage: String { + private var failedMessage: String { return String.localizedStringWithFormat(NSLocalizedString("_focused_auto_upload_failed_", comment: ""), failedCount) } From bd12e815aa068a8a679ccacf6a4eb4fe48206e46 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 16:47:16 +0200 Subject: [PATCH 13/18] Move to More Signed-off-by: Milen Pivchev --- iOSClient/More/NCMoreModel.swift | 24 ++++++ iOSClient/More/NCMoreView.swift | 76 +++++++++++++++++-- .../Settings/Settings/NCSettingsModel.swift | 3 - .../Settings/Settings/NCSettingsView.swift | 55 -------------- 4 files changed, 93 insertions(+), 65 deletions(-) diff --git a/iOSClient/More/NCMoreModel.swift b/iOSClient/More/NCMoreModel.swift index 41b8d15f37..1ffb241c28 100644 --- a/iOSClient/More/NCMoreModel.swift +++ b/iOSClient/More/NCMoreModel.swift @@ -37,12 +37,17 @@ final class NCMoreModel: ObservableObject { @Published var quotaProgress: Double = 0 @Published var quotaExternalSiteTitle: String = "" @Published var quotaExternalSiteUrl: String? + @Published var autoUploadStart: Bool = false private weak var controller: NCMainTabBarController? var account: String { controller?.account ?? "" } + var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } + private let database = NCManageDatabase.shared private let utilityFileSystem = NCUtilityFileSystem() @@ -166,6 +171,8 @@ final class NCMoreModel: ObservableObject { return } + autoUploadStart = tableAccount.autoUploadStart + var functionItems: [Item] = [] var externalSiteItems: [Item] = [] var settingsItems: [Item] = [] @@ -510,6 +517,23 @@ final class NCMoreModel: ObservableObject { navigationController.pushViewController(settingsController, animated: true) } + /// Opens the SwiftUI auto-upload screen, injecting the shared counter so the row and the + /// screen observe the same source. + func openAutoUpload(counter: NCAutoUploadCounter) { + guard let controller, + let navigationController = controller.currentNavigationController() else { + return + } + + let autoUploadView = NCAutoUploadView(model: NCAutoUploadModel(controller: controller), + albumModel: AlbumModel(controller: controller)) + .environment(counter) + + let hostingController = UIHostingController(rootView: autoUploadView) + + navigationController.pushViewController(hostingController, animated: true) + } + /// Opens an app using a custom URL scheme. /// /// If the app is not installed or the scheme cannot be handled, the fallback URL is opened. diff --git a/iOSClient/More/NCMoreView.swift b/iOSClient/More/NCMoreView.swift index 4e74704f76..cda1dcc9c2 100644 --- a/iOSClient/More/NCMoreView.swift +++ b/iOSClient/More/NCMoreView.swift @@ -12,6 +12,7 @@ import NextcloudKit /// inside the UIKit-based `NCMoreNavigationController`. struct NCMoreView: View { @StateObject private var model: NCMoreModel + @State private var autoUploadCounter = NCAutoUploadCounter() private let loadItemsOnAppear: Bool private let shortcutIconColor = Color(red: 0, green: 130 / 255, blue: 201 / 255) // Nextcloud Color @@ -48,20 +49,38 @@ struct NCMoreView: View { guard loadItemsOnAppear else { return } await model.loadItems() } + .onAppear { + updateAutoUploadCounter() + } + .onDisappear { + autoUploadCounter.stop() + } + .onChange(of: model.autoUploadStart) { + updateAutoUploadCounter() + } + } + + private func updateAutoUploadCounter() { + let session = model.session + + autoUploadCounter.start(account: session.account, + urlBase: session.urlBase, + userId: session.userId, + autoUploadStart: model.autoUploadStart) } /// Main scrollable content of the More tab. private var content: some View { ScrollView { VStack(spacing: 18) { - ForEach(model.sections) { section in - switch section.type { - case .moreApps: - moreAppsSection(items: section.items) + ForEach(model.sections.filter { $0.type == .moreApps }) { section in + moreAppsSection(items: section.items) + } - case .regular: - menuSection(items: section.items) - } + autoUploadSection + + ForEach(model.sections.filter { $0.type == .regular }) { section in + menuSection(items: section.items) } } .padding(.horizontal, 20) @@ -70,6 +89,49 @@ struct NCMoreView: View { } } + /// Rich Auto Upload row: animated cloud icon plus a live "items left / failed" subtitle. + private var autoUploadSection: some View { + Button { + model.openAutoUpload(counter: autoUploadCounter) + } label: { + HStack(spacing: 16) { + NCFocusedAutoUploadCloudAnimation(size: 44, + cloudColor: Color(NCBrandColor.shared.iconImageColor), + arrowColor: model.autoUploadStart + ? Color(UIColor.systemBackground) + : Color(NCBrandColor.shared.iconImageColor), + isAnimated: model.autoUploadStart) + .frame(width: 39) + + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("_settings_autoupload_", comment: "")) + .font(.body) + .foregroundColor(Color(NCBrandColor.shared.textColor)) + .lineLimit(1) + .minimumScaleFactor(0.85) + + if model.autoUploadStart && autoUploadCounter.isLoaded { + Text(autoUploadCounter.itemsLeftSummary) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(Color(.tertiaryLabel)) + } + .padding(.horizontal, 16) + .frame(minHeight: 54) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + /// Renders the app suggestion shortcut section. /// /// - Parameter items: Shortcut items displayed as cards. diff --git a/iOSClient/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index 3b0a3ff446..b74e600753 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -22,8 +22,6 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { @Published var privacyScreen: Bool = false // State to control @Published var resetWrongAttempts: Bool = false - // State to control the auto upload status indicator - @Published var autoUploadStart: Bool = false // Request account on start @Published var accountRequest: Bool = false // Root View Controller @@ -54,7 +52,6 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { lockScreen = !keychain.requestPasscodeAtStart privacyScreen = keychain.privacyScreenEnabled resetWrongAttempts = keychain.resetAppCounterFail - autoUploadStart = NCManageDatabase.shared.getTableAccount(account: session.account)?.autoUploadStart ?? false accountRequest = keychain.accountRequest footerApp = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, NCUtility().getVersionBuild()) + "\n\n" footerServer = String(format: NCBrandOptions.shared.textCopyrightNextcloudServer, capabilities.serverVersion) + "\n" diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index 8812897be8..2042771f85 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -22,7 +22,6 @@ struct NCSettingsView: View { @State private var showSourceCode = false // Object of ViewModel of this view @ObservedObject var model: NCSettingsModel - @State private var autoUploadCounter = NCAutoUploadCounter() var capabilities: NKCapabilities.Capabilities { NCNetworking.shared.capabilities[model.controller?.account ?? ""] ?? NKCapabilities.Capabilities() @@ -30,38 +29,6 @@ struct NCSettingsView: View { var body: some View { Form { - // `Auto Upload` Section - Section(content: { - NavigationLink(destination: LazyView { - NCAutoUploadView(model: NCAutoUploadModel(controller: model.controller), albumModel: AlbumModel(controller: model.controller)) - .environment(autoUploadCounter) - }) { - HStack { - NCFocusedAutoUploadCloudAnimation(size: 44, - cloudColor: Color(NCBrandColor.shared.iconImageColor), - arrowColor: model.autoUploadStart - ? Color(UIColor.systemBackground) - : Color(NCBrandColor.shared.iconImageColor), - isAnimated: model.autoUploadStart) - .frame(width: 39) - - VStack(alignment: .leading, spacing: 2) { - Text(NSLocalizedString("_settings_autoupload_", comment: "")) - .font(.body) - - if model.autoUploadStart && autoUploadCounter.isLoaded { - Text(autoUploadCounter.itemsLeftSummary) - .font(.footnote) - .foregroundStyle(.secondary) - } - } - } - } - }, footer: { - Text(NSLocalizedString("_autoupload_description_", comment: "")) - .font(.footnote) - }) - // `Privacy` Section Section(content: { Button(action: { @@ -325,28 +292,6 @@ struct NCSettingsView: View { } .navigationBarTitle(NSLocalizedString("_settings_", comment: "")) .defaultViewModifier(model) - .onAppear { - updateAutoUploadCounterSubscription() - } - .onDisappear { - stopAutoUploadCounterSubscription() - } - .onChange(of: model.autoUploadStart) { - updateAutoUploadCounterSubscription() - } - } - - private func updateAutoUploadCounterSubscription() { - let session = model.session - - autoUploadCounter.start(account: session.account, - urlBase: session.urlBase, - userId: session.userId, - autoUploadStart: model.autoUploadStart) - } - - private func stopAutoUploadCounterSubscription() { - autoUploadCounter.stop() } } From df24851b039eb3ba5434b0d7b24e6edcb8f40b55 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 16:54:29 +0200 Subject: [PATCH 14/18] Refactor Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 22 +++++++++---------- .../AutoUpload/NCAutoUploadView.swift | 10 ++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 2c91329913..394a5b7aa7 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -748,7 +748,7 @@ F7C30E01291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; }; F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */; }; F7C55C7C2FB5AEF7004A974F /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */; }; - F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F7C55C882FB5AEF7004A974F /* Assistant Action.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7C55C7A2FB5AEF7004A974F /* Assistant Action.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F7C55C8D2FB5B02C004A974F /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; F7C55C8E2FB5B03D004A974F /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; F7C55C8F2FB5B045004A974F /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; @@ -1136,7 +1136,7 @@ 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */, F7C9739928F17131002C43E2 /* WidgetDashboardIntentHandler.appex in Embed Foundation Extensions */, F7346E1C28B0EF5E006CE2D2 /* Widget.appex in Embed Foundation Extensions */, - F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */, + F7C55C882FB5AEF7004A974F /* Assistant Action.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -1747,7 +1747,7 @@ F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEDelete.swift; sourceTree = ""; }; F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EERename.swift; sourceTree = ""; }; F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantInputModel.swift; sourceTree = ""; }; - F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Action Assistant.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7C55C7A2FB5AEF7004A974F /* Assistant Action.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Assistant Action.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; F7C55CC32FB5CE74004A974F /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; F7C55CC42FB5CE74004A974F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; @@ -3336,7 +3336,7 @@ C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */, F7F1FBA62E27D13700C79E20 /* Frameworks */, F31165012F9674A1009A1E37 /* AppIcon.icon */, - F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */, + F7C55C7A2FB5AEF7004A974F /* Assistant Action.appex */, ); sourceTree = ""; }; @@ -3733,9 +3733,9 @@ productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */; productType = "com.apple.product-type.application"; }; - F7C55C792FB5AEF7004A974F /* Action Assistant */ = { + F7C55C792FB5AEF7004A974F /* Assistant Action */ = { isa = PBXNativeTarget; - buildConfigurationList = F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */; + buildConfigurationList = F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Assistant Action" */; buildPhases = ( F7C55C762FB5AEF7004A974F /* Sources */, F7C55C772FB5AEF7004A974F /* Frameworks */, @@ -3745,12 +3745,12 @@ ); dependencies = ( ); - name = "Action Assistant"; + name = "Assistant Action"; packageProductDependencies = ( F7C55C9E2FB5B83A004A974F /* NextcloudKit */, ); productName = "Action Assistant"; - productReference = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; + productReference = F7C55C7A2FB5AEF7004A974F /* Assistant Action.appex */; productType = "com.apple.product-type.app-extension"; }; F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */ = { @@ -3938,7 +3938,7 @@ F7346E0F28B0EF5B006CE2D2 /* Widget */, F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */, F71459B41D12E3B700CAFEEC /* Share */, - F7C55C792FB5AEF7004A974F /* Action Assistant */, + F7C55C792FB5AEF7004A974F /* Assistant Action */, F771E3CF20E2392D00AFB62D /* File Provider Extension */, F70716E22987F81400E72C1D /* File Provider Extension UI */, 2C33C47E23E2C475005F963B /* Notification Service Extension */, @@ -5043,7 +5043,7 @@ }; F7C55C872FB5AEF7004A974F /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = F7C55C792FB5AEF7004A974F /* Action Assistant */; + target = F7C55C792FB5AEF7004A974F /* Assistant Action */; targetProxy = F7C55C862FB5AEF7004A974F /* PBXContainerItemProxy */; }; F7C9739828F17131002C43E2 /* PBXTargetDependency */ = { @@ -6212,7 +6212,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */ = { + F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Assistant Action" */ = { isa = XCConfigurationList; buildConfigurations = ( F7C55C892FB5AEF7004A974F /* Debug */, diff --git a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift index d303233a2b..3c3024697a 100644 --- a/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift +++ b/iOSClient/Settings/AutoUpload/NCAutoUploadView.swift @@ -18,7 +18,7 @@ struct NCAutoUploadView: View { @State private var showUploadAllPhotosWarning = false @State private var showFocusedAutoUploadIntro = false @State private var showFocusedAutoUploadProgress = false - @State private var openFocusedAutoUploadAfterIntro = false + @State private var openFocusedAutoUploadFinish = false @State private var startAutoUpload = false @Environment(NCAutoUploadCounter.self) private var autoUploadCounter @@ -73,15 +73,15 @@ struct NCAutoUploadView: View { .presentationDetents([.medium, .large]) } .sheet(isPresented: $showFocusedAutoUploadIntro, onDismiss: { - guard openFocusedAutoUploadAfterIntro else { return } + guard openFocusedAutoUploadFinish else { return } - openFocusedAutoUploadAfterIntro = false + openFocusedAutoUploadFinish = false guard autoUploadCounter.hasItemsToUpload else { return } showFocusedAutoUploadProgress = true }) { NCFocusedAutoUploadIntroView { - openFocusedAutoUploadAfterIntro = true + openFocusedAutoUploadFinish = true showFocusedAutoUploadIntro = false } .presentationDetents([.large]) @@ -97,7 +97,7 @@ struct NCAutoUploadView: View { if !newValue { showFocusedAutoUploadIntro = false showFocusedAutoUploadProgress = false - openFocusedAutoUploadAfterIntro = false + openFocusedAutoUploadFinish = false } updateAutoUploadCounterSubscription() } From b33938369228fb52d243b7f20427ce8da14c8e82 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 17:45:33 +0200 Subject: [PATCH 15/18] WIP Signed-off-by: Milen Pivchev --- iOSClient/More/NCMoreView.swift | 75 ++++++++++--------- .../NCFocusedAutoUploadCloudAnimation.swift | 10 +++ .../NCFocusedAutoUploadIntroView.swift | 19 ++--- 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/iOSClient/More/NCMoreView.swift b/iOSClient/More/NCMoreView.swift index cda1dcc9c2..a91c0c56ef 100644 --- a/iOSClient/More/NCMoreView.swift +++ b/iOSClient/More/NCMoreView.swift @@ -91,45 +91,52 @@ struct NCMoreView: View { /// Rich Auto Upload row: animated cloud icon plus a live "items left / failed" subtitle. private var autoUploadSection: some View { - Button { - model.openAutoUpload(counter: autoUploadCounter) - } label: { - HStack(spacing: 16) { - NCFocusedAutoUploadCloudAnimation(size: 44, - cloudColor: Color(NCBrandColor.shared.iconImageColor), - arrowColor: model.autoUploadStart - ? Color(UIColor.systemBackground) - : Color(NCBrandColor.shared.iconImageColor), - isAnimated: model.autoUploadStart) - .frame(width: 39) - - VStack(alignment: .leading, spacing: 2) { - Text(NSLocalizedString("_settings_autoupload_", comment: "")) - .font(.body) - .foregroundColor(Color(NCBrandColor.shared.textColor)) - .lineLimit(1) - .minimumScaleFactor(0.85) - - if model.autoUploadStart && autoUploadCounter.isLoaded { - Text(autoUploadCounter.itemsLeftSummary) - .font(.footnote) - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 6) { + Button { + model.openAutoUpload(counter: autoUploadCounter) + } label: { + HStack(spacing: 16) { + NCFocusedAutoUploadCloudAnimation(size: 44, + cloudColor: Color(NCBrandColor.shared.iconImageColor), + arrowColor: model.autoUploadStart + ? Color(UIColor.systemBackground) + : Color(NCBrandColor.shared.iconImageColor), + isAnimated: model.autoUploadStart) + .frame(width: 39) + + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("_settings_autoupload_", comment: "")) + .font(.body) + .foregroundColor(Color(NCBrandColor.shared.textColor)) + .lineLimit(1) + .minimumScaleFactor(0.85) + + if model.autoUploadStart && autoUploadCounter.isLoaded { + Text(autoUploadCounter.itemsLeftSummary) + .font(.footnote) + .foregroundStyle(.secondary) + } } - } - Spacer() + Spacer() - Image(systemName: "chevron.right") - .font(.caption) - .foregroundColor(Color(.tertiaryLabel)) + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(Color(.tertiaryLabel)) + } + .padding(.horizontal, 16) + .frame(minHeight: 54) + .contentShape(Rectangle()) } - .padding(.horizontal, 16) - .frame(minHeight: 54) - .contentShape(Rectangle()) + .buttonStyle(.plain) + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + + Text(NSLocalizedString("_autoupload_description_", comment: "")) + .font(.footnote) + .foregroundStyle(.secondary) + .padding(.horizontal, 16) } - .buttonStyle(.plain) - .background(Color(.secondarySystemGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } /// Renders the app suggestion shortcut section. diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift index 83751b044b..53a60d9ff2 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift @@ -70,3 +70,13 @@ struct NCFocusedAutoUploadCloudAnimation: View { start + (end - start) * CGFloat(progress) } } + +#Preview { + VStack(spacing: 48) { + NCFocusedAutoUploadCloudAnimation(size: 80) + + NCFocusedAutoUploadCloudAnimation(size: 80, isAnimated: false) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black) +} diff --git a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift index 2774f55ca0..c3f6003683 100644 --- a/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -54,7 +54,7 @@ struct NCFocusedAutoUploadIntroView: View { VStack(alignment: .leading, spacing: 22) { guidanceRow(systemImage: "wifi", textKey: "_focused_auto_upload_wifi_") guidanceRow(systemImage: "battery.100", textKey: "_focused_auto_upload_charger_") - guidanceRow(systemImage: "logo", textKey: "_focused_auto_upload_do_not_exit_") + guidanceRow(systemImage: "arrow.down.right.and.arrow.up.left", textKey: "_focused_auto_upload_do_not_exit_") } .padding(.top, 10) } @@ -97,19 +97,10 @@ struct NCFocusedAutoUploadIntroView: View { @ViewBuilder private func guidanceIcon(systemImage: String) -> some View { - if systemImage == "logo" { - Image("logo") - .renderingMode(.template) - .resizable() - .scaledToFit() - .foregroundStyle(.secondary) - .frame(width: 34, height: 28) - } else { - Image(systemName: systemImage) - .font(.system(size: 28, weight: .regular)) - .foregroundStyle(.secondary) - .frame(width: 34) - } + Image(systemName: systemImage) + .font(.system(size: 28, weight: .regular)) + .foregroundStyle(.secondary) + .frame(width: 34) } } From a708066fd1e7cf2b05f7a2a3beefa87b7c9d8a42 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 18:31:54 +0200 Subject: [PATCH 16/18] Use Form in NCMore Signed-off-by: Milen Pivchev --- iOSClient/More/NCMoreView.swift | 143 ++++++++++++-------------------- 1 file changed, 55 insertions(+), 88 deletions(-) diff --git a/iOSClient/More/NCMoreView.swift b/iOSClient/More/NCMoreView.swift index a91c0c56ef..7c5b5c15dc 100644 --- a/iOSClient/More/NCMoreView.swift +++ b/iOSClient/More/NCMoreView.swift @@ -36,15 +36,22 @@ struct NCMoreView: View { } var body: some View { - ZStack { - Color(.systemGroupedBackground) - .ignoresSafeArea() + VStack(spacing: 0) { + if let appsSection = model.sections.first(where: { $0.type == .moreApps }) { + moreAppsHeader(items: appsSection.items) + } + + Form { + autoUploadSection - VStack(spacing: 0) { - content - quotaSection + ForEach(model.sections.filter { $0.type == .regular }) { section in + menuSection(items: section.items) + } } + + quotaSection } + .background(Color(.systemGroupedBackground).ignoresSafeArea()) .task { guard loadItemsOnAppear else { return } await model.loadItems() @@ -69,29 +76,9 @@ struct NCMoreView: View { autoUploadStart: model.autoUploadStart) } - /// Main scrollable content of the More tab. - private var content: some View { - ScrollView { - VStack(spacing: 18) { - ForEach(model.sections.filter { $0.type == .moreApps }) { section in - moreAppsSection(items: section.items) - } - - autoUploadSection - - ForEach(model.sections.filter { $0.type == .regular }) { section in - menuSection(items: section.items) - } - } - .padding(.horizontal, 20) - .padding(.top, 18) - .padding(.bottom, 20) - } - } - - /// Rich Auto Upload row: animated cloud icon plus a live "items left / failed" subtitle. + /// Auto Upload section: animated cloud icon, a live "items left / failed" subtitle, and a footer description. private var autoUploadSection: some View { - VStack(alignment: .leading, spacing: 6) { + Section { Button { model.openAutoUpload(counter: autoUploadCounter) } label: { @@ -108,8 +95,6 @@ struct NCMoreView: View { Text(NSLocalizedString("_settings_autoupload_", comment: "")) .font(.body) .foregroundColor(Color(NCBrandColor.shared.textColor)) - .lineLimit(1) - .minimumScaleFactor(0.85) if model.autoUploadStart && autoUploadCounter.isLoaded { Text(autoUploadCounter.itemsLeftSummary) @@ -124,33 +109,30 @@ struct NCMoreView: View { .font(.caption) .foregroundColor(Color(.tertiaryLabel)) } - .padding(.horizontal, 16) - .frame(minHeight: 54) .contentShape(Rectangle()) } .buttonStyle(.plain) - .background(Color(.secondarySystemGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) - + } footer: { Text(NSLocalizedString("_autoupload_description_", comment: "")) .font(.footnote) - .foregroundStyle(.secondary) - .padding(.horizontal, 16) } } - /// Renders the app suggestion shortcut section. + /// Renders the app suggestion shortcut strip shown above the Form. /// /// - Parameter items: Shortcut items displayed as cards. - private func moreAppsSection(items: [NCMoreModel.Item]) -> some View { + private func moreAppsHeader(items: [NCMoreModel.Item]) -> some View { HStack(spacing: 14) { ForEach(Array(items.enumerated()), id: \.element.identifier) { _, item in shortcutButton(item) } } + .padding(.horizontal, 20) + .padding(.top, 16) + .padding(.bottom, 4) } - /// Creates a tappable shortcut nextcloud card. + /// Creates a tappable shortcut card. /// /// - Parameter item: Item containing title, image and destination. private func shortcutButton(_ item: NCMoreModel.Item) -> some View { @@ -179,21 +161,15 @@ struct NCMoreView: View { .buttonStyle(.plain) } - /// Renders a rounded menu section containing multiple rows. + /// Renders a menu section as grouped Form rows. /// /// - Parameter items: Items displayed in the section. private func menuSection(items: [NCMoreModel.Item]) -> some View { - VStack(spacing: 0) { - ForEach(Array(items.enumerated()), id: \.element.identifier) { index, item in + Section { + ForEach(Array(items.enumerated()), id: \.element.identifier) { _, item in menuRow(item) - - if index < items.count - 1 { - divider - } } } - .background(Color(.secondarySystemGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } /// Renders a single menu row. @@ -207,13 +183,11 @@ struct NCMoreView: View { Image(systemName: item.image) .font(.icon()) .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) - .frame(width: 26) + .frame(width: 39) Text(NSLocalizedString(item.titleKey, comment: "")) .font(.body) .foregroundColor(Color(NCBrandColor.shared.textColor)) - .lineLimit(1) - .minimumScaleFactor(0.85) .tint(.primary) Spacer() @@ -222,54 +196,47 @@ struct NCMoreView: View { .font(.caption) .foregroundColor(Color(.tertiaryLabel)) } - .padding(.horizontal, 16) - .frame(height: 54) .contentShape(Rectangle()) } .buttonStyle(.plain) } - private var divider: some View { - Rectangle() - .fill(Color(.separator).opacity(0.45)) - .frame(height: 0.5) - .padding(.leading, 58) - } - + @ViewBuilder private var quotaSection: some View { - VStack(alignment: .leading, spacing: 8) { - if !model.quotaDescription.isEmpty { - Text(model.quotaDescription) - .font(.footnote) - .foregroundColor(.primary) - .lineLimit(2) - .tint(.primary) + if !model.quotaDescription.isEmpty || !model.quotaExternalSiteTitle.isEmpty { + VStack(alignment: .leading, spacing: 8) { + if !model.quotaDescription.isEmpty { + Text(model.quotaDescription) + .font(.footnote) + .foregroundColor(.primary) + .lineLimit(2) - quotaProgressView - } + quotaProgressView + } - if !model.quotaExternalSiteTitle.isEmpty, - let url = model.quotaExternalSiteUrl { - Button { - model.perform( - .browser( - url: url, - title: model.quotaExternalSiteTitle + if !model.quotaExternalSiteTitle.isEmpty, + let url = model.quotaExternalSiteUrl { + Button { + model.perform( + .browser( + url: url, + title: model.quotaExternalSiteTitle + ) ) - ) - } label: { - Text(model.quotaExternalSiteTitle) - .font(.footnote) - .lineLimit(1) - .tint(.primary) + } label: { + Text(model.quotaExternalSiteTitle) + .font(.footnote) + .lineLimit(1) + .tint(.primary) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 16) } - .padding(.horizontal, 20) - .padding(.top, 8) - .padding(.bottom, 16) - .background(Color(.systemGroupedBackground)) } private var normalizedQuotaProgress: Double { From b1df4c6a5b057360d7b0cf2e57bf59f94feefb3f Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 18:34:30 +0200 Subject: [PATCH 17/18] Gradient Signed-off-by: Milen Pivchev --- iOSClient/More/NCMoreView.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iOSClient/More/NCMoreView.swift b/iOSClient/More/NCMoreView.swift index 7c5b5c15dc..b2f6bf657c 100644 --- a/iOSClient/More/NCMoreView.swift +++ b/iOSClient/More/NCMoreView.swift @@ -48,6 +48,15 @@ struct NCMoreView: View { menuSection(items: section.items) } } + .overlay(alignment: .bottom) { + // Soften the cut where the scrolling list meets the pinned quota. + LinearGradient(colors: [Color(.systemGroupedBackground).opacity(0), + Color(.systemGroupedBackground)], + startPoint: .top, + endPoint: .bottom) + .frame(height: 32) + .allowsHitTesting(false) + } quotaSection } From 361f7322173f2c29184477648a2c0ece63a3fd0e Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 4 Jun 2026 19:24:33 +0200 Subject: [PATCH 18/18] WIP Signed-off-by: Milen Pivchev --- iOSClient/More/NCMoreView.swift | 63 +++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/iOSClient/More/NCMoreView.swift b/iOSClient/More/NCMoreView.swift index b2f6bf657c..8dd56a1a06 100644 --- a/iOSClient/More/NCMoreView.swift +++ b/iOSClient/More/NCMoreView.swift @@ -37,16 +37,21 @@ struct NCMoreView: View { var body: some View { VStack(spacing: 0) { - if let appsSection = model.sections.first(where: { $0.type == .moreApps }) { - moreAppsHeader(items: appsSection.items) - } + ScrollView { + VStack(spacing: 18) { + if let appsSection = model.sections.first(where: { $0.type == .moreApps }) { + moreAppsSection(items: appsSection.items) + } - Form { - autoUploadSection + autoUploadSection - ForEach(model.sections.filter { $0.type == .regular }) { section in - menuSection(items: section.items) + ForEach(model.sections.filter { $0.type == .regular }) { section in + menuSection(items: section.items) + } } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 20) } .overlay(alignment: .bottom) { // Soften the cut where the scrolling list meets the pinned quota. @@ -85,9 +90,9 @@ struct NCMoreView: View { autoUploadStart: model.autoUploadStart) } - /// Auto Upload section: animated cloud icon, a live "items left / failed" subtitle, and a footer description. + /// Auto Upload card: animated cloud icon, a live "items left / failed" subtitle, and a description below. private var autoUploadSection: some View { - Section { + VStack(alignment: .leading, spacing: 6) { Button { model.openAutoUpload(counter: autoUploadCounter) } label: { @@ -118,27 +123,30 @@ struct NCMoreView: View { .font(.caption) .foregroundColor(Color(.tertiaryLabel)) } + .padding(.horizontal, 16) + .frame(minHeight: 54) .contentShape(Rectangle()) } .buttonStyle(.plain) - } footer: { + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + Text(NSLocalizedString("_autoupload_description_", comment: "")) .font(.footnote) + .foregroundStyle(.secondary) + .padding(.horizontal, 16) } } - /// Renders the app suggestion shortcut strip shown above the Form. + /// Renders the app suggestion shortcut strip; scrolls at the top of the content. /// /// - Parameter items: Shortcut items displayed as cards. - private func moreAppsHeader(items: [NCMoreModel.Item]) -> some View { + private func moreAppsSection(items: [NCMoreModel.Item]) -> some View { HStack(spacing: 14) { ForEach(Array(items.enumerated()), id: \.element.identifier) { _, item in shortcutButton(item) } } - .padding(.horizontal, 20) - .padding(.top, 16) - .padding(.bottom, 4) } /// Creates a tappable shortcut card. @@ -170,15 +178,21 @@ struct NCMoreView: View { .buttonStyle(.plain) } - /// Renders a menu section as grouped Form rows. + /// Renders a rounded menu card containing multiple rows. /// - /// - Parameter items: Items displayed in the section. + /// - Parameter items: Items displayed in the card. private func menuSection(items: [NCMoreModel.Item]) -> some View { - Section { - ForEach(Array(items.enumerated()), id: \.element.identifier) { _, item in + VStack(spacing: 0) { + ForEach(Array(items.enumerated()), id: \.element.identifier) { index, item in menuRow(item) + + if index < items.count - 1 { + divider + } } } + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } /// Renders a single menu row. @@ -197,6 +211,8 @@ struct NCMoreView: View { Text(NSLocalizedString(item.titleKey, comment: "")) .font(.body) .foregroundColor(Color(NCBrandColor.shared.textColor)) + .lineLimit(1) + .minimumScaleFactor(0.85) .tint(.primary) Spacer() @@ -205,11 +221,20 @@ struct NCMoreView: View { .font(.caption) .foregroundColor(Color(.tertiaryLabel)) } + .padding(.horizontal, 16) + .frame(height: 54) .contentShape(Rectangle()) } .buttonStyle(.plain) } + private var divider: some View { + Rectangle() + .fill(Color(.separator).opacity(0.45)) + .frame(height: 0.5) + .padding(.leading, 71) + } + @ViewBuilder private var quotaSection: some View { if !model.quotaDescription.isEmpty || !model.quotaExternalSiteTitle.isEmpty {