From 900ec2a17876854ce01d2dccd3966a9d9e35d007 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 5 May 2026 10:38:26 +0200 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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 + +