diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 3a9447037a..e0a9a07ba8 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -44,6 +44,10 @@ 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 */; }; + 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 */; }; @@ -1207,6 +1211,10 @@ 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 = ""; }; + 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 = ""; }; @@ -2619,6 +2627,10 @@ F39A1EE12D0AF8A200DAD522 /* Albums.swift */, F768821B2C0DD1E7001CF441 /* NCAutoUploadView.swift */, F71D2FB62E09BBD700B751CC /* NCAutoUploadModel.swift */, + AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */, + AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */, + AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */, + AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */, ); path = AutoUpload; sourceTree = ""; @@ -4689,6 +4701,10 @@ 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 */, + 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/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 f178959853..9b6589b1ed 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,31 @@ 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, + account: model.session.account, + urlBase: model.session.urlBase, + userId: model.session.userId) + } + .onChange(of: model.autoUploadStart) { _, newValue in + if !newValue { + showFocusedAutoUploadIntro = false + showFocusedAutoUploadProgress = false + openFocusedAutoUploadAfterIntro = false + } + } } @ViewBuilder @@ -207,6 +235,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/NCFocusedAutoUploadCloudAnimation.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift new file mode 100644 index 0000000000..83751b044b --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadCloudAnimation.swift @@ -0,0 +1,72 @@ +// 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 isAnimated: Bool + + init(size: CGFloat = 176, + cloudColor: Color = .white, + arrowColor: Color = .black.opacity(0.82), + isAnimated: Bool = true) { + self.size = size + self.cloudColor = cloudColor + self.arrowColor = arrowColor + 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 { + Image(systemName: includesMotion ? "icloud.fill" : "icloud") + .font(.system(size: size * 0.53, weight: .regular)) + .foregroundStyle(cloudColor) + .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: 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) : 0) + } + } + + 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 new file mode 100644 index 0000000000..447a8ad93e --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadIntroView.swift @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.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: "logo", 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) { + guidanceIcon(systemImage: systemImage) + + Text(NSLocalizedString(textKey, comment: "")) + .font(.title3) + .foregroundStyle(.primary) + .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/AutoUpload/NCFocusedAutoUploadProgressView.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift new file mode 100644 index 0000000000..2aa3e20b67 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadProgressView.swift @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +@MainActor +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 + + private let dimDelay = 10 + + var body: some View { + ZStack { + Color.black + .ignoresSafeArea() + + VStack(spacing: 0) { + Spacer() + + VStack(spacing: 24) { + NCFocusedAutoUploadCloudAnimation() + .padding(.bottom, 4) + + Divider() + .background(Color.white.opacity(0.15)) + .padding(.horizontal, 36) + + 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() + + 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 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 { + 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 + uploadCountTask?.cancel() + uploadCountTask = nil + isScreenDimmed = false + NCFocusedAutoUploadScreenDimmer.shared.restoreScreen() + } + + private func wakeFocusedScreen() { + 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/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift new file mode 100644 index 0000000000..cad8bad4d3 --- /dev/null +++ b/iOSClient/Settings/AutoUpload/NCFocusedAutoUploadScreenDimmer.swift @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.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/Settings/Settings/NCSettingsModel.swift b/iOSClient/Settings/Settings/NCSettingsModel.swift index b74e600753..79b1d82267 100644 --- a/iOSClient/Settings/Settings/NCSettingsModel.swift +++ b/iOSClient/Settings/Settings/NCSettingsModel.swift @@ -22,6 +22,10 @@ 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 + // 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 @@ -52,14 +56,45 @@ class NCSettingsModel: ObservableObject, ViewOnAppearHandling { lockScreen = !keychain.requestPasscodeAtStart 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 a2482c78ef..3807a7c861 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -34,12 +34,17 @@ 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: model.autoUploadStart + ? Color(UIColor.systemBackground) + : Color(NCBrandColor.shared.iconImageColor), + isAnimated: model.autoUploadStart) .frame(width: 39) - Text(NSLocalizedString("_settings_autoupload_", comment: "")) + Text(model.autoUploadStart + ? model.autoUploadCountMessage + : NSLocalizedString("_settings_autoupload_", comment: "")) .font(.body) } } @@ -311,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 23851fc7e0..86d091d821 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"; @@ -672,14 +672,14 @@ "_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 …"; -"_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"; @@ -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 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 + +