Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -1231,6 +1236,11 @@
AACCAB632CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Localizable.strings; sourceTree = "<group>"; };
AACCAB642CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/InfoPlist.strings; sourceTree = "<group>"; };
AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNavigationTitleSetting.swift; sourceTree = "<group>"; };
AAFC0D012F9AA10000F0A001 /* NCFocusedAutoUploadIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadIntroView.swift; sourceTree = "<group>"; };
AAFC0D022F9AA10000F0A001 /* NCFocusedAutoUploadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadProgressView.swift; sourceTree = "<group>"; };
AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = "<group>"; };
AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadCloudAnimation.swift; sourceTree = "<group>"; };
AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadCounter.swift; sourceTree = "<group>"; };
AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = "<group>"; };
AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = "<group>"; };
AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2662,8 +2672,13 @@
isa = PBXGroup;
children = (
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;
sourceTree = "<group>";
Expand Down Expand Up @@ -4785,6 +4800,11 @@
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 */,
AAFC0D0B2F9AA10000F0A001 /* NCAutoUploadCounter.swift in Sources */,
F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift in Sources */,
F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */,
F75D19E325EFE09000D74598 /* NCContextMenuTrash.swift in Sources */,
Expand Down
33 changes: 33 additions & 0 deletions iOSClient/Data/NCManageDatabase+AutoUpload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +136 to +139
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We filter out auto upload success files. Is that ok? @marinofaggiana

}

func existsAutoUpload(account: String,
autoUploadServerUrlBase: String) -> Bool {
return core.performRealmRead { realm in
Expand Down
1 change: 1 addition & 0 deletions iOSClient/DeepLink/NCDeepLinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
84 changes: 84 additions & 0 deletions iOSClient/Settings/AutoUpload/NCAutoUploadCounter.swift
Original file line number Diff line number Diff line change
@@ -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<Void, Never>?

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
}
}
}
96 changes: 96 additions & 0 deletions iOSClient/Settings/AutoUpload/NCAutoUploadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,7 +16,11 @@ 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
@Environment(NCAutoUploadCounter.self) private var autoUploadCounter

var body: some View {
ZStack {
Expand All @@ -27,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()
Expand All @@ -49,11 +72,72 @@ struct NCAutoUploadView: View {
ConfirmAutoUploadSheet(model: model, isPresented: $showUploadAllPhotosWarning)
.presentationDetents([.medium, .large])
}
.sheet(isPresented: $showFocusedAutoUploadIntro, onDismiss: {
guard openFocusedAutoUploadAfterIntro else { return }

openFocusedAutoUploadAfterIntro = false
guard autoUploadCounter.hasItemsToUpload else { return }

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)
.environment(autoUploadCounter)
}
.onChange(of: model.autoUploadStart) { _, newValue in
if !newValue {
showFocusedAutoUploadIntro = false
showFocusedAutoUploadProgress = false
openFocusedAutoUploadAfterIntro = false
}
updateAutoUploadCounterSubscription()
}
}

@ViewBuilder
var autoUploadOnView: some View {
Form {
if model.autoUploadStart && autoUploadCounter.hasItemsToUpload {
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: {
Expand Down Expand Up @@ -244,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
Expand Down Expand Up @@ -377,4 +472,5 @@ struct ConfirmAutoUploadSheet: View {

#Preview {
NCAutoUploadView(model: NCAutoUploadModel(controller: nil), albumModel: AlbumModel(controller: nil))
.environment(NCAutoUploadCounter())
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading