From 785a2e904a9976cef0782720327f9da5c2b91df4 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 12:46:42 +0100 Subject: [PATCH 01/12] ipad Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/UploadBannerView.swift | 186 ++++++++++++++---- .../Networking/NCNetworkingProcess.swift | 11 +- 2 files changed, 156 insertions(+), 41 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index d41b3d38d0..b868ef5668 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -161,42 +161,70 @@ struct UploadBannerView: View { func containerView(state: LucidBannerState, @ViewBuilder _ content: () -> Content) -> some View { let isError = (state.typedStage == .error) let cornerRadius: CGFloat = 22 + let isMinimized = state.isMinimized - let contentBase = content() + // Base content con gesture e clear + let base = content() .contentShape(Rectangle()) .onTapGesture { guard allowMinimizeOnTap else { return } UploadBannerCoordinator.shared.handleTap(state) } - .onDisappear { - UploadBannerCoordinator.shared.clear() - } - // Hard cap for very large screens (iPad etc.) - .frame(maxWidth: 500) - .frame(maxWidth: .infinity, alignment: .center) - if #available(iOS 26, *) { - if isError { - contentBase - .background( - RoundedRectangle(cornerRadius: cornerRadius) - .fill(Color.red.opacity(1)) + if isMinimized { + if #available(iOS 26, *) { + if isError { + base + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.red.opacity(1)) + ) + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + } else { + base + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + } + } else { + let colorBg = isError ? Color.red.opacity(0.9) : Color.white.opacity(0.9) + + base + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(colorBg, lineWidth: 0.6) ) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) + } + } else { + let contentBase = base + .frame(maxWidth: 500) + + if #available(iOS 26, *) { + if isError { + contentBase + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.red.opacity(1)) + ) + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .frame(maxWidth: .infinity, alignment: .center) + } else { + contentBase + .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .frame(maxWidth: .infinity, alignment: .center) + } } else { + let colorBg = isError ? Color.red.opacity(0.9) : Color.white.opacity(0.9) + contentBase - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .stroke(colorBg, lineWidth: 0.6) + ) + .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) + .frame(maxWidth: .infinity, alignment: .center) } - } else { - let colorBg = isError ? Color.red.opacity(0.9) : Color.white.opacity(0.9) - - contentBase - .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) - .overlay( - RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) - .stroke(colorBg, lineWidth: 0.6) - ) - .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) } } } @@ -258,7 +286,8 @@ func showUploadBanner(scene: UIWindowScene?, stage: LucidBanner.Stage? = nil, policy: LucidBanner.ShowPolicy = .drop, allowMinimizeOnTap: Bool = false, - minimizePoint: CGPoint? = nil, + inset: CGSize? = nil, + corner: UploadBannerCoordinator.UploadBannerMinimizeAnchor.Corner? = nil, onButtonTap: (() -> Void)? = nil) -> Int? { let token = LucidBanner.shared.show( scene: scene, @@ -275,7 +304,9 @@ func showUploadBanner(scene: UIWindowScene?, onButtonTap: onButtonTap) } - UploadBannerCoordinator.shared.setMinimizePoint(minimizePoint) + if let inset, let corner { + UploadBannerCoordinator.shared.setMinimizeCorner(corner, inset: inset) + } UploadBannerCoordinator.shared.register(token: token) return token } @@ -284,9 +315,37 @@ func showUploadBanner(scene: UIWindowScene?, final class UploadBannerCoordinator { static let shared = UploadBannerCoordinator() + public enum UploadBannerMinimizeAnchor { + case absolute(CGPoint) + case corner(Corner, inset: CGSize) + + enum Corner { + case topLeading + case topTrailing + case bottomLeading + case bottomTrailing + } + } + private var currentToken: Int? private var originalCenter: CGPoint? - private var minimizePoint: CGPoint? + private var minimizeAnchor: UploadBannerMinimizeAnchor? + private var orientationObserver: NSObjectProtocol? + + init() { + orientationObserver = NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self else { return } + + Task { @MainActor in + try? await Task.sleep(for: .milliseconds(50)) + self.refreshMinimizedPosition(animated: true) + } + } + } func register(token: Int?) { currentToken = token @@ -295,14 +354,22 @@ final class UploadBannerCoordinator { func clear() { currentToken = nil originalCenter = nil - minimizePoint = nil + minimizeAnchor = nil } func setMinimizePoint(_ point: CGPoint?) { - minimizePoint = point + if let point { + minimizeAnchor = .absolute(point) + } else { + minimizeAnchor = nil + } + } + + func setMinimizeCorner(_ corner: UploadBannerMinimizeAnchor.Corner, + inset: CGSize = CGSize(width: 20, height: 40)) { + minimizeAnchor = .corner(corner, inset: inset) } - @MainActor func moveIfMinimized(to point: CGPoint, animated: Bool = true) { guard let token = currentToken else { return } guard LucidBanner.shared.isAlive(token) else { @@ -314,7 +381,6 @@ final class UploadBannerCoordinator { return } - // Move the minimized banner LucidBanner.shared.move( toX: point.x, y: point.y, @@ -323,6 +389,28 @@ final class UploadBannerCoordinator { ) } + func refreshMinimizedPosition(animated: Bool = true) { + guard let token = currentToken else { return } + guard LucidBanner.shared.isAlive(token) else { + clear() + return + } + guard let state = LucidBanner.shared.currentState(for: token), + state.isMinimized else { + return + } + guard let target = resolvedMinimizePoint(for: token) else { + return + } + + LucidBanner.shared.move( + toX: target.x, + y: target.y, + for: token, + animated: animated + ) + } + func handleTap(_ state: LucidBannerState) { guard let token = currentToken else { return @@ -349,7 +437,7 @@ final class UploadBannerCoordinator { LucidBanner.shared.setDraggingEnabled(false, for: token) LucidBanner.shared.requestRelayout(animated: true) - if let target = minimizePoint { + if let target = resolvedMinimizePoint(for: token) { LucidBanner.shared.move( toX: target.x, y: target.y, @@ -365,7 +453,6 @@ final class UploadBannerCoordinator { LucidBanner.shared.setDraggingEnabled(true, for: token) LucidBanner.shared.requestRelayout(animated: true) - // Restore if let center = originalCenter { LucidBanner.shared.move( toX: center.x, @@ -379,6 +466,37 @@ final class UploadBannerCoordinator { originalCenter = nil } + + private func resolvedMinimizePoint(for token: Int) -> CGPoint? { + guard let anchor = minimizeAnchor else { return nil } + guard let hostView = LucidBanner.shared.currentHostView(for: token), + let window = hostView.window else { + return nil + } + + let bounds = window.bounds + + switch anchor { + case .absolute(let point): + return point + + case .corner(let corner, let inset): + switch corner { + case .topLeading: + return CGPoint(x: bounds.minX + inset.width, + y: bounds.minY + inset.height) + case .topTrailing: + return CGPoint(x: bounds.maxX - inset.width, + y: bounds.minY + inset.height) + case .bottomLeading: + return CGPoint(x: bounds.minX + inset.width, + y: bounds.maxY - inset.height) + case .bottomTrailing: + return CGPoint(x: bounds.maxX - inset.width, + y: bounds.maxY - inset.height) + } + } + } } // MARK: - Preview diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index a076e5c02c..a208817e33 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -367,7 +367,8 @@ actor NCNetworkingProcess { vPosition: .bottom, verticalMargin: 55, blocksTouches: true, - minimizePoint: CGPoint(x: 0, y: 0), + inset: CGSize(width: 10, height: 10), + corner: .bottomLeading, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() @@ -411,11 +412,6 @@ actor NCNetworkingProcess { var currentUploadTask: Task<(account: String, file: NKFile?, error: NKError), Never>? var tokenBanner: Int? let scene = SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene - let tabBarTopLeft = scene?.tabBarTopLeft ?? CGPoint(x: 0, y: 50) - let minimizePoint = CGPoint( - x: tabBarTopLeft.x + 50, - y: tabBarTopLeft.y - 20 - ) tokenBanner = showUploadBanner(scene: scene, vPosition: .bottom, @@ -423,7 +419,8 @@ actor NCNetworkingProcess { draggable: true, stage: .init(rawValue: "button"), allowMinimizeOnTap: true, - minimizePoint: minimizePoint, + inset: CGSize(width: 10, height: 10), + corner: .bottomLeading, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() From 0b5104ccab3ee94b5ab09eb678d3f8e81f4ef452 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 13:08:21 +0100 Subject: [PATCH 02/12] cod Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/UploadBannerView.swift | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index b868ef5668..c6413c029d 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -328,7 +328,6 @@ final class UploadBannerCoordinator { } private var currentToken: Int? - private var originalCenter: CGPoint? private var minimizeAnchor: UploadBannerMinimizeAnchor? private var orientationObserver: NSObjectProtocol? @@ -341,19 +340,25 @@ final class UploadBannerCoordinator { guard let self else { return } Task { @MainActor in + // Small delay to let the window/layout settle after rotation. try? await Task.sleep(for: .milliseconds(50)) - self.refreshMinimizedPosition(animated: true) + self.refreshPosition(animated: true) } } } + deinit { + if let orientationObserver { + NotificationCenter.default.removeObserver(orientationObserver) + } + } + func register(token: Int?) { currentToken = token } func clear() { currentToken = nil - originalCenter = nil minimizeAnchor = nil } @@ -389,26 +394,30 @@ final class UploadBannerCoordinator { ) } - func refreshMinimizedPosition(animated: Bool = true) { - guard let token = currentToken else { return } - guard LucidBanner.shared.isAlive(token) else { + func refreshPosition(animated: Bool = true) { + guard let token = currentToken, + LucidBanner.shared.isAlive(token) else { clear() return } - guard let state = LucidBanner.shared.currentState(for: token), - state.isMinimized else { - return - } - guard let target = resolvedMinimizePoint(for: token) else { + guard let state = LucidBanner.shared.currentState(for: token) else { return } - LucidBanner.shared.move( - toX: target.x, - y: target.y, - for: token, - animated: animated - ) + if state.isMinimized { + guard let target = resolvedMinimizePoint(for: token) else { + return + } + + LucidBanner.shared.move( + toX: target.x, + y: target.y, + for: token, + animated: animated + ) + } else { + LucidBanner.shared.resetPosition(for: token, animated: true) + } } func handleTap(_ state: LucidBannerState) { @@ -429,13 +438,10 @@ final class UploadBannerCoordinator { } private func minimize(state: LucidBannerState, token: Int) { - if let frame = LucidBanner.shared.currentFrameInWindow(for: token) { - originalCenter = CGPoint(x: frame.midX, y: frame.midY) - } state.isMinimized = true + // Disable dragging while in bubble mode. LucidBanner.shared.setDraggingEnabled(false, for: token) - LucidBanner.shared.requestRelayout(animated: true) if let target = resolvedMinimizePoint(for: token) { LucidBanner.shared.move( @@ -450,21 +456,10 @@ final class UploadBannerCoordinator { private func maximize(state: LucidBannerState, token: Int) { state.isMinimized = false + // Re-enable dragging in full mode. LucidBanner.shared.setDraggingEnabled(true, for: token) - LucidBanner.shared.requestRelayout(animated: true) - - if let center = originalCenter { - LucidBanner.shared.move( - toX: center.x, - y: center.y, - for: token, - animated: true - ) - } else { - LucidBanner.shared.resetPosition(for: token, animated: true) - } - originalCenter = nil + LucidBanner.shared.resetPosition(for: token, animated: true) } private func resolvedMinimizePoint(for token: Int) -> CGPoint? { From 8823d41b0b0c03f35cbb3ccdf781e64a19b7c9cc Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 15:09:52 +0100 Subject: [PATCH 03/12] code Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/ErrorBannerView.swift | 3 +- .../GUI/Lucid Banner/HudBannerView.swift | 3 +- .../GUI/Lucid Banner/UploadBannerView.swift | 30 +++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 6099fc7faf..1293052e78 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -118,7 +118,8 @@ func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: title: "Error", subtitle: "Not avalilable", footnote: "ErroCode. 12", - imageAnimation: .breathe) + imageAnimation: .breathe, + isDraggable: false) ) .padding() } diff --git a/iOSClient/GUI/Lucid Banner/HudBannerView.swift b/iOSClient/GUI/Lucid Banner/HudBannerView.swift index 02b55bb1aa..0e9e030235 100644 --- a/iOSClient/GUI/Lucid Banner/HudBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/HudBannerView.swift @@ -217,7 +217,8 @@ private struct HudBannerPreviewWrapper: View { title: "Uploading files", subtitle: "Syncing your library…", footnote: nil, - imageAnimation: .none + imageAnimation: .none, + isDraggable: false ) var body: some View { diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index c6413c029d..ed8a659fc5 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -163,7 +163,6 @@ struct UploadBannerView: View { let cornerRadius: CGFloat = 22 let isMinimized = state.isMinimized - // Base content con gesture e clear let base = content() .contentShape(Rectangle()) .onTapGesture { @@ -305,9 +304,9 @@ func showUploadBanner(scene: UIWindowScene?, } if let inset, let corner { - UploadBannerCoordinator.shared.setMinimizeCorner(corner, inset: inset) + UploadBannerCoordinator.shared.register(token: token, corner: corner, inset: inset) } - UploadBannerCoordinator.shared.register(token: token) + return token } @@ -337,7 +336,9 @@ final class UploadBannerCoordinator { object: nil, queue: .main ) { [weak self] _ in - guard let self else { return } + guard let self else { + return + } Task { @MainActor in // Small delay to let the window/layout settle after rotation. @@ -353,8 +354,14 @@ final class UploadBannerCoordinator { } } - func register(token: Int?) { + func register(token: Int?, + corner: UploadBannerMinimizeAnchor.Corner, + inset: CGSize = CGSize(width: 20, height: 40)) { + guard let token else { + return + } currentToken = token + minimizeAnchor = .corner(corner, inset: inset) } func clear() { @@ -370,11 +377,6 @@ final class UploadBannerCoordinator { } } - func setMinimizeCorner(_ corner: UploadBannerMinimizeAnchor.Corner, - inset: CGSize = CGSize(width: 20, height: 40)) { - minimizeAnchor = .corner(corner, inset: inset) - } - func moveIfMinimized(to point: CGPoint, animated: Bool = true) { guard let token = currentToken else { return } guard LucidBanner.shared.isAlive(token) else { @@ -440,7 +442,7 @@ final class UploadBannerCoordinator { private func minimize(state: LucidBannerState, token: Int) { state.isMinimized = true - // Disable dragging while in bubble mode. + // Disable dragging. LucidBanner.shared.setDraggingEnabled(false, for: token) if let target = resolvedMinimizePoint(for: token) { @@ -456,8 +458,9 @@ final class UploadBannerCoordinator { private func maximize(state: LucidBannerState, token: Int) { state.isMinimized = false - // Re-enable dragging in full mode. - LucidBanner.shared.setDraggingEnabled(true, for: token) + if state.isDraggable { + LucidBanner.shared.setDraggingEnabled(true, for: token) + } LucidBanner.shared.resetPosition(for: token, animated: true) } @@ -504,6 +507,7 @@ final class UploadBannerCoordinator { systemImage: "arrow.up.circle", imageAnimation: .none, progress: 0.71, + isDraggable: false, stage: "button" ) From 7fe0d3a5556d4dcc678ff6672042be7cb4c33c57 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 16:49:57 +0100 Subject: [PATCH 04/12] cod Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/ErrorBannerView.swift | 3 +- .../GUI/Lucid Banner/HudBannerView.swift | 3 +- .../GUI/Lucid Banner/UploadBannerView.swift | 29 +++++-------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 1293052e78..6099fc7faf 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -118,8 +118,7 @@ func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: title: "Error", subtitle: "Not avalilable", footnote: "ErroCode. 12", - imageAnimation: .breathe, - isDraggable: false) + imageAnimation: .breathe) ) .padding() } diff --git a/iOSClient/GUI/Lucid Banner/HudBannerView.swift b/iOSClient/GUI/Lucid Banner/HudBannerView.swift index 0e9e030235..02b55bb1aa 100644 --- a/iOSClient/GUI/Lucid Banner/HudBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/HudBannerView.swift @@ -217,8 +217,7 @@ private struct HudBannerPreviewWrapper: View { title: "Uploading files", subtitle: "Syncing your library…", footnote: nil, - imageAnimation: .none, - isDraggable: false + imageAnimation: .none ) var body: some View { diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index ed8a659fc5..abfb285837 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -167,7 +167,7 @@ struct UploadBannerView: View { .contentShape(Rectangle()) .onTapGesture { guard allowMinimizeOnTap else { return } - UploadBannerCoordinator.shared.handleTap(state) + LucidBannerMinimizeCoordinator.shared.handleTap(state) } if isMinimized { @@ -286,7 +286,7 @@ func showUploadBanner(scene: UIWindowScene?, policy: LucidBanner.ShowPolicy = .drop, allowMinimizeOnTap: Bool = false, inset: CGSize? = nil, - corner: UploadBannerCoordinator.UploadBannerMinimizeAnchor.Corner? = nil, + corner: LucidBanner.MinimizeAnchor.Corner? = nil, onButtonTap: (() -> Void)? = nil) -> Int? { let token = LucidBanner.shared.show( scene: scene, @@ -304,30 +304,18 @@ func showUploadBanner(scene: UIWindowScene?, } if let inset, let corner { - UploadBannerCoordinator.shared.register(token: token, corner: corner, inset: inset) + LucidBannerMinimizeCoordinator.shared.register(token: token, corner: corner, inset: inset) } return token } @MainActor -final class UploadBannerCoordinator { - static let shared = UploadBannerCoordinator() - - public enum UploadBannerMinimizeAnchor { - case absolute(CGPoint) - case corner(Corner, inset: CGSize) - - enum Corner { - case topLeading - case topTrailing - case bottomLeading - case bottomTrailing - } - } +final class LucidBannerMinimizeCoordinator { + static let shared = LucidBannerMinimizeCoordinator() private var currentToken: Int? - private var minimizeAnchor: UploadBannerMinimizeAnchor? + private var minimizeAnchor: LucidBanner.MinimizeAnchor? private var orientationObserver: NSObjectProtocol? init() { @@ -355,7 +343,7 @@ final class UploadBannerCoordinator { } func register(token: Int?, - corner: UploadBannerMinimizeAnchor.Corner, + corner: LucidBanner.MinimizeAnchor.Corner, inset: CGSize = CGSize(width: 20, height: 40)) { guard let token else { return @@ -458,7 +446,7 @@ final class UploadBannerCoordinator { private func maximize(state: LucidBannerState, token: Int) { state.isMinimized = false - if state.isDraggable { + if state.draggable { LucidBanner.shared.setDraggingEnabled(true, for: token) } @@ -507,7 +495,6 @@ final class UploadBannerCoordinator { systemImage: "arrow.up.circle", imageAnimation: .none, progress: 0.71, - isDraggable: false, stage: "button" ) From be1767cd1837726c4b1613095df94666256117cb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 17:31:36 +0100 Subject: [PATCH 05/12] code Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/UploadBannerView.swift | 30 ++++++++----------- .../Networking/NCNetworkingProcess.swift | 4 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index abfb285837..8d578d93bb 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -314,7 +314,7 @@ func showUploadBanner(scene: UIWindowScene?, final class LucidBannerMinimizeCoordinator { static let shared = LucidBannerMinimizeCoordinator() - private var currentToken: Int? + private var token: Int? private var minimizeAnchor: LucidBanner.MinimizeAnchor? private var orientationObserver: NSObjectProtocol? @@ -344,16 +344,16 @@ final class LucidBannerMinimizeCoordinator { func register(token: Int?, corner: LucidBanner.MinimizeAnchor.Corner, - inset: CGSize = CGSize(width: 20, height: 40)) { + inset: CGSize) { guard let token else { return } - currentToken = token + self.token = token minimizeAnchor = .corner(corner, inset: inset) } func clear() { - currentToken = nil + token = nil minimizeAnchor = nil } @@ -366,8 +366,8 @@ final class LucidBannerMinimizeCoordinator { } func moveIfMinimized(to point: CGPoint, animated: Bool = true) { - guard let token = currentToken else { return } - guard LucidBanner.shared.isAlive(token) else { + guard let token, + LucidBanner.shared.isAlive(token) else { clear() return } @@ -385,7 +385,7 @@ final class LucidBannerMinimizeCoordinator { } func refreshPosition(animated: Bool = true) { - guard let token = currentToken, + guard let token, LucidBanner.shared.isAlive(token) else { clear() return @@ -411,27 +411,23 @@ final class LucidBannerMinimizeCoordinator { } func handleTap(_ state: LucidBannerState) { - guard let token = currentToken else { - return - } - guard LucidBanner.shared.isAlive(token) else { clear() return } if state.isMinimized { - maximize(state: state, token: token) + maximize(state: state) } else { - minimize(state: state, token: token) + minimize(state: state) } } - private func minimize(state: LucidBannerState, token: Int) { + private func minimize(state: LucidBannerState) { state.isMinimized = true - // Disable dragging. LucidBanner.shared.setDraggingEnabled(false, for: token) + LucidBanner.shared.requestRelayout(animated: false) if let target = resolvedMinimizePoint(for: token) { LucidBanner.shared.move( @@ -443,7 +439,7 @@ final class LucidBannerMinimizeCoordinator { } } - private func maximize(state: LucidBannerState, token: Int) { + private func maximize(state: LucidBannerState) { state.isMinimized = false if state.draggable { @@ -453,7 +449,7 @@ final class LucidBannerMinimizeCoordinator { LucidBanner.shared.resetPosition(for: token, animated: true) } - private func resolvedMinimizePoint(for token: Int) -> CGPoint? { + private func resolvedMinimizePoint(for token: Int?) -> CGPoint? { guard let anchor = minimizeAnchor else { return nil } guard let hostView = LucidBanner.shared.currentHostView(for: token), let window = hostView.window else { diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index a208817e33..adc2b34a29 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -367,7 +367,7 @@ actor NCNetworkingProcess { vPosition: .bottom, verticalMargin: 55, blocksTouches: true, - inset: CGSize(width: 10, height: 10), + inset: CGSize(width: 100, height: 100), corner: .bottomLeading, onButtonTap: { if let currentUploadTask { @@ -419,7 +419,7 @@ actor NCNetworkingProcess { draggable: true, stage: .init(rawValue: "button"), allowMinimizeOnTap: true, - inset: CGSize(width: 10, height: 10), + inset: CGSize(width: 40, height: 40), corner: .bottomLeading, onButtonTap: { if let currentUploadTask { From db2af73c75962ce8f5a340b7fc1e8fb565ee6fea Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 17:45:49 +0100 Subject: [PATCH 06/12] code Signed-off-by: Marino Faggiana --- iOSClient/GUI/Lucid Banner/UploadBannerView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index 8d578d93bb..6c17bfdfb0 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -427,8 +427,9 @@ final class LucidBannerMinimizeCoordinator { state.isMinimized = true LucidBanner.shared.setDraggingEnabled(false, for: token) + // Re-measure LucidBanner.shared.requestRelayout(animated: false) - + // Move in the point if let target = resolvedMinimizePoint(for: token) { LucidBanner.shared.move( toX: target.x, @@ -445,7 +446,9 @@ final class LucidBannerMinimizeCoordinator { if state.draggable { LucidBanner.shared.setDraggingEnabled(true, for: token) } - + // Re-measure + LucidBanner.shared.requestRelayout(animated: false) + // Then animate back to the standard position managed by LucidBanner. LucidBanner.shared.resetPosition(for: token, animated: true) } From c0ecdb4664cbdb15a3c3e8dce0fe5a70e9995703 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 11 Dec 2025 20:05:40 +0100 Subject: [PATCH 07/12] cod Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 -- .../Extensions/UIWindowScene+Extension.swift | 29 -------------- .../GUI/Lucid Banner/UploadBannerView.swift | 40 +++++++++++++------ .../Networking/NCNetworkingProcess.swift | 8 ++-- 4 files changed, 33 insertions(+), 48 deletions(-) delete mode 100644 iOSClient/Extensions/UIWindowScene+Extension.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 70036afa87..d6e88b5da0 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -846,7 +846,6 @@ F7E402332BA89551007E5609 /* NCTrash+Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E402322BA89551007E5609 /* NCTrash+Networking.swift */; }; F7E41316294A19B300839300 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E41315294A19B300839300 /* UIView+Extension.swift */; }; F7E4D9C422ED929B003675FD /* NCShareCommentsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E4D9C322ED929B003675FD /* NCShareCommentsCell.swift */; }; - F7E562A22EE978B100FA2FDF /* UIWindowScene+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E562A12EE9789B00FA2FDF /* UIWindowScene+Extension.swift */; }; F7E742F32EC0A10C00E2362A /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; }; F7E742F42EC0A10C00E2362A /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; }; F7E742F52EC0A3DD00E2362A /* NCManageDatabase+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78A10BE29322E8A008499B8 /* NCManageDatabase+Directory.swift */; }; @@ -1756,7 +1755,6 @@ F7E41315294A19B300839300 /* UIView+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; F7E45E6D21E75BF200579249 /* ja-JP */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ja-JP"; path = "ja-JP.lproj/Localizable.strings"; sourceTree = ""; }; F7E4D9C322ED929B003675FD /* NCShareCommentsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCommentsCell.swift; sourceTree = ""; }; - F7E562A12EE9789B00FA2FDF /* UIWindowScene+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowScene+Extension.swift"; sourceTree = ""; }; F7E7AEA42BA32C6500512E52 /* NCCollectionViewDownloadThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewDownloadThumbnail.swift; sourceTree = ""; }; F7E7AEA62BA32D0000512E52 /* NCCollectionViewUnifiedSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewUnifiedSearch.swift; sourceTree = ""; }; F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; @@ -2803,7 +2801,6 @@ F7B7504A2397D38E004E13EC /* UIImage+Extension.swift */, F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */, F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */, - F7E562A12EE9789B00FA2FDF /* UIWindowScene+Extension.swift */, F743C89D2E5B2595000173A9 /* UIScene+Extension.swift */, F77C3F5A2D9BF8B500F3C471 /* UITabBar+Extension.swift */, F77BB747289985270090FC19 /* UITabBarController+Extension.swift */, @@ -4478,7 +4475,6 @@ F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, AA8D31662D411FA100FE2775 /* NCShareDateCell.swift in Sources */, F3F442EE2DDE292D00FD701F /* NCMetadataPermissions.swift in Sources */, - F7E562A22EE978B100FA2FDF /* UIWindowScene+Extension.swift in Sources */, F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */, AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */, AA8D31682D41224800FE2775 /* NCShareToggleCell.swift in Sources */, diff --git a/iOSClient/Extensions/UIWindowScene+Extension.swift b/iOSClient/Extensions/UIWindowScene+Extension.swift deleted file mode 100644 index f351001620..0000000000 --- a/iOSClient/Extensions/UIWindowScene+Extension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2025 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import UIKit - -public extension UIWindowScene { - - /// Returns the top-left coordinate of the UITabBar in this scene, - /// expressed in the coordinate space of the scene's key window. - /// - /// If the scene does not host a UITabBarController as root, - /// or no suitable window is found, the method returns `nil`. - var tabBarTopLeft: CGPoint? { - // Select key window if available, otherwise fallback to the first one. - guard let window = windows.first(where: { $0.isKeyWindow }) ?? windows.first, - let tabBarController = window.rootViewController as? UITabBarController - else { - return nil - } - - let tabBar = tabBarController.tabBar - - // Convert tab bar's bounds to the window coordinate space. - let frameInWindow = tabBar.convert(tabBar.bounds, to: window) - - return CGPoint(x: frameInWindow.minX, y: frameInWindow.minY) - } -} diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index 6c17bfdfb0..540e20ec78 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -395,7 +395,7 @@ final class LucidBannerMinimizeCoordinator { } if state.isMinimized { - guard let target = resolvedMinimizePoint(for: token) else { + guard let target = resolvedMinimizePoint() else { return } @@ -430,7 +430,7 @@ final class LucidBannerMinimizeCoordinator { // Re-measure LucidBanner.shared.requestRelayout(animated: false) // Move in the point - if let target = resolvedMinimizePoint(for: token) { + if let target = resolvedMinimizePoint() { LucidBanner.shared.move( toX: target.x, y: target.y, @@ -452,33 +452,49 @@ final class LucidBannerMinimizeCoordinator { LucidBanner.shared.resetPosition(for: token, animated: true) } - private func resolvedMinimizePoint(for token: Int?) -> CGPoint? { - guard let anchor = minimizeAnchor else { return nil } + private func resolvedMinimizePoint() -> CGPoint? { + guard let anchor = minimizeAnchor else { + return nil + } guard let hostView = LucidBanner.shared.currentHostView(for: token), let window = hostView.window else { return nil } let bounds = window.bounds + let safe = window.safeAreaInsets switch anchor { case .absolute(let point): return point case .corner(let corner, let inset): + let verticalBase = max(safe.top, safe.bottom) + switch corner { case .topLeading: - return CGPoint(x: bounds.minX + inset.width, - y: bounds.minY + inset.height) + return CGPoint( + x: bounds.minX + safe.left + inset.width, + y: bounds.minY + verticalBase + inset.height + ) + case .topTrailing: - return CGPoint(x: bounds.maxX - inset.width, - y: bounds.minY + inset.height) + return CGPoint( + x: bounds.maxX - safe.right - inset.width, + y: bounds.minY + verticalBase + inset.height + ) + case .bottomLeading: - return CGPoint(x: bounds.minX + inset.width, - y: bounds.maxY - inset.height) + return CGPoint( + x: bounds.minX + safe.left + inset.width, + y: bounds.maxY - verticalBase - inset.height + ) + case .bottomTrailing: - return CGPoint(x: bounds.maxX - inset.width, - y: bounds.maxY - inset.height) + return CGPoint( + x: bounds.maxX - safe.right - inset.width, + y: bounds.maxY - verticalBase - inset.height + ) } } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index adc2b34a29..c98fb66dce 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -362,12 +362,13 @@ actor NCNetworkingProcess { var request: UploadRequest? let controller = await getController(account: metadata.account, sceneIdentifier: metadata.sceneIdentifier) let scene = await SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene + let inset = CGSize(width: 0, height: 0) let token = await showUploadBanner(scene: scene, vPosition: .bottom, verticalMargin: 55, blocksTouches: true, - inset: CGSize(width: 100, height: 100), + inset: inset, corner: .bottomLeading, onButtonTap: { if let currentUploadTask { @@ -412,6 +413,7 @@ actor NCNetworkingProcess { var currentUploadTask: Task<(account: String, file: NKFile?, error: NKError), Never>? var tokenBanner: Int? let scene = SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene + let inset = CGSize(width: 100, height: 10) tokenBanner = showUploadBanner(scene: scene, vPosition: .bottom, @@ -419,8 +421,8 @@ actor NCNetworkingProcess { draggable: true, stage: .init(rawValue: "button"), allowMinimizeOnTap: true, - inset: CGSize(width: 40, height: 40), - corner: .bottomLeading, + inset: inset, + corner: .topLeading, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() From 4b910aaece20e76a07e14ee4d373fc3fbf16a928 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 12 Dec 2025 09:35:05 +0100 Subject: [PATCH 08/12] test Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/UploadBannerView.swift | 102 ++++++++++++++++-- iOSClient/Main/NCMainTabBarController.swift | 6 ++ .../Networking/NCNetworkingProcess.swift | 11 +- 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index 540e20ec78..591b771ca3 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -286,7 +286,7 @@ func showUploadBanner(scene: UIWindowScene?, policy: LucidBanner.ShowPolicy = .drop, allowMinimizeOnTap: Bool = false, inset: CGSize? = nil, - corner: LucidBanner.MinimizeAnchor.Corner? = nil, + corner: LucidBannerMinimizeCoordinator.MinimizeAnchor.Corner? = nil, onButtonTap: (() -> Void)? = nil) -> Int? { let token = LucidBanner.shared.show( scene: scene, @@ -314,8 +314,46 @@ func showUploadBanner(scene: UIWindowScene?, final class LucidBannerMinimizeCoordinator { static let shared = LucidBannerMinimizeCoordinator() + struct ResolveContext { + let token: Int + let window: UIWindow + let bounds: CGRect + let safeAreaInsets: UIEdgeInsets + let anchor: MinimizeAnchor + let state: LucidBannerState + } + + /// Logical anchor used when a banner is minimized. + /// + /// This describes *where* the minimized bubble should be placed + /// inside the window coordinate space. + public enum MinimizeAnchor: Equatable { + /// Absolute point in window coordinates. + case absolute(CGPoint) + /// Attach to one of the window corners with a given inset. + case corner(Corner, inset: CGSize) + /// Supported corners for minimized placement. + public enum Corner { + case topLeading + case topCenter + case topTrailing + + case centerLeading + case center + case centerTrailing + + case bottomLeading + case bottomCenter + case bottomTrailing + } + } + + /// Custom resolver used to compute the minimized target point. + /// Return nil to let the coordinator use the default logic. + var resolveMinimizePointHandler: (@MainActor (_ context: ResolveContext) -> CGPoint?)? + private var token: Int? - private var minimizeAnchor: LucidBanner.MinimizeAnchor? + private var minimizeAnchor: MinimizeAnchor? private var orientationObserver: NSObjectProtocol? init() { @@ -342,8 +380,10 @@ final class LucidBannerMinimizeCoordinator { } } + + func register(token: Int?, - corner: LucidBanner.MinimizeAnchor.Corner, + corner: MinimizeAnchor.Corner, inset: CGSize) { guard let token else { return @@ -453,12 +493,26 @@ final class LucidBannerMinimizeCoordinator { } private func resolvedMinimizePoint() -> CGPoint? { - guard let anchor = minimizeAnchor else { + guard let token, + let anchor = minimizeAnchor, + let hostView = LucidBanner.shared.currentHostView(for: token), + let window = hostView.window, + let state = LucidBanner.shared.currentState(for: token) + else { return nil } - guard let hostView = LucidBanner.shared.currentHostView(for: token), - let window = hostView.window else { - return nil + + let context = ResolveContext( + token: token, + window: window, + bounds: window.bounds, + safeAreaInsets: window.safeAreaInsets, + anchor: anchor, + state: state + ) + + if let custom = resolveMinimizePointHandler?(context) { + return custom } let bounds = window.bounds @@ -472,24 +526,58 @@ final class LucidBannerMinimizeCoordinator { let verticalBase = max(safe.top, safe.bottom) switch corner { + + // --- TOP ROW --- case .topLeading: return CGPoint( x: bounds.minX + safe.left + inset.width, y: bounds.minY + verticalBase + inset.height ) + case .topCenter: + return CGPoint( + x: bounds.midX, + y: bounds.minY + verticalBase + inset.height + ) + case .topTrailing: return CGPoint( x: bounds.maxX - safe.right - inset.width, y: bounds.minY + verticalBase + inset.height ) + // --- CENTER ROW --- + case .centerLeading: + return CGPoint( + x: bounds.minX + safe.left + inset.width, + y: bounds.midY + ) + + case .center: + return CGPoint( + x: bounds.midX, + y: bounds.midY + ) + + case .centerTrailing: + return CGPoint( + x: bounds.maxX - safe.right - inset.width, + y: bounds.midY + ) + + // --- BOTTOM ROW --- case .bottomLeading: return CGPoint( x: bounds.minX + safe.left + inset.width, y: bounds.maxY - verticalBase - inset.height ) + case .bottomCenter: + return CGPoint( + x: bounds.midX, + y: bounds.maxY - verticalBase - inset.height + ) + case .bottomTrailing: return CGPoint( x: bounds.maxX - safe.right - inset.width, diff --git a/iOSClient/Main/NCMainTabBarController.swift b/iOSClient/Main/NCMainTabBarController.swift index c6cfaef567..11bb9d9527 100644 --- a/iOSClient/Main/NCMainTabBarController.swift +++ b/iOSClient/Main/NCMainTabBarController.swift @@ -31,6 +31,12 @@ class NCMainTabBarController: UITabBarController { return SceneManager.shared.getWindow(controller: self) } + var visibleBarHeight: CGFloat { + let safeBottom = tabBar.safeAreaInsets.bottom + let height = tabBar.frame.height - safeBottom + return height + } + override func viewDidLoad() { super.viewDidLoad() delegate = self diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index c98fb66dce..bb132deea2 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -362,14 +362,14 @@ actor NCNetworkingProcess { var request: UploadRequest? let controller = await getController(account: metadata.account, sceneIdentifier: metadata.sceneIdentifier) let scene = await SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene - let inset = CGSize(width: 0, height: 0) + let inset = CGSize(width: 0, height: 55) let token = await showUploadBanner(scene: scene, vPosition: .bottom, verticalMargin: 55, blocksTouches: true, inset: inset, - corner: .bottomLeading, + corner: .bottomCenter, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() @@ -413,7 +413,9 @@ actor NCNetworkingProcess { var currentUploadTask: Task<(account: String, file: NKFile?, error: NKError), Never>? var tokenBanner: Int? let scene = SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene - let inset = CGSize(width: 100, height: 10) + let controller = SceneManager.shared.getController(scene: scene) + let visibleBarHeight = controller?.visibleBarHeight ?? 55 + let inset = CGSize(width: 0, height: visibleBarHeight) tokenBanner = showUploadBanner(scene: scene, vPosition: .bottom, @@ -422,7 +424,7 @@ actor NCNetworkingProcess { stage: .init(rawValue: "button"), allowMinimizeOnTap: true, inset: inset, - corner: .topLeading, + corner: .bottomCenter, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() @@ -462,6 +464,7 @@ actor NCNetworkingProcess { title: NSLocalizedString("_finalizing_wait_", comment: ""), systemImage: "gearshape.arrow.triangle.2.circlepath", imageAnimation: .rotate, + progress: 0, stage: .init(rawValue: "none"), for: tokenBanner) } From 5e905fe0ff052abf4ecef07ea9c2e04273057b41 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 12 Dec 2025 10:01:35 +0100 Subject: [PATCH 09/12] code Signed-off-by: Marino Faggiana --- .../GUI/Lucid Banner/UploadBannerView.swift | 286 ++++++++---------- .../Networking/NCNetworkingProcess.swift | 7 - 2 files changed, 119 insertions(+), 174 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index 591b771ca3..d70bd2a067 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -285,8 +285,6 @@ func showUploadBanner(scene: UIWindowScene?, stage: LucidBanner.Stage? = nil, policy: LucidBanner.ShowPolicy = .drop, allowMinimizeOnTap: Bool = false, - inset: CGSize? = nil, - corner: LucidBannerMinimizeCoordinator.MinimizeAnchor.Corner? = nil, onButtonTap: (() -> Void)? = nil) -> Int? { let token = LucidBanner.shared.show( scene: scene, @@ -303,10 +301,22 @@ func showUploadBanner(scene: UIWindowScene?, onButtonTap: onButtonTap) } - if let inset, let corner { - LucidBannerMinimizeCoordinator.shared.register(token: token, corner: corner, inset: inset) +#if !EXTENSION + if allowMinimizeOnTap { + LucidBannerMinimizeCoordinator.shared.register(token: token) { context in + let controller = SceneManager.shared.getController(scene: scene) + let height = controller?.visibleBarHeight ?? 55 + let bounds = context.bounds + let safeAreaInsets = context.safeAreaInsets + let over: CGFloat = 20 + + return CGPoint( + x: bounds.midX, + y: bounds.maxY - safeAreaInsets.bottom - height + over + ) + } } - +#endif return token } @@ -314,61 +324,54 @@ func showUploadBanner(scene: UIWindowScene?, final class LucidBannerMinimizeCoordinator { static let shared = LucidBannerMinimizeCoordinator() + // MARK: - Types + + /// Context passed to the mandatory minimize-point resolver. struct ResolveContext { + /// The active banner token. let token: Int + + /// The shared banner state instance (SwiftUI observes this). + let state: LucidBannerState + + /// The banner host view (UIKit container for the SwiftUI content). + let hostView: UIView + + /// The window hosting the banner. let window: UIWindow + + /// Convenience: window bounds. let bounds: CGRect + + /// Convenience: window safe-area insets. let safeAreaInsets: UIEdgeInsets - let anchor: MinimizeAnchor - let state: LucidBannerState } - /// Logical anchor used when a banner is minimized. + /// Mandatory resolver used to compute the minimized target point. /// - /// This describes *where* the minimized bubble should be placed - /// inside the window coordinate space. - public enum MinimizeAnchor: Equatable { - /// Absolute point in window coordinates. - case absolute(CGPoint) - /// Attach to one of the window corners with a given inset. - case corner(Corner, inset: CGSize) - /// Supported corners for minimized placement. - public enum Corner { - case topLeading - case topCenter - case topTrailing - - case centerLeading - case center - case centerTrailing - - case bottomLeading - case bottomCenter - case bottomTrailing - } - } + /// Return a CGPoint in window coordinates where the banner should move + /// when minimized. + typealias ResolveMinimizePointHandler = @MainActor (_ context: ResolveContext) -> CGPoint - /// Custom resolver used to compute the minimized target point. - /// Return nil to let the coordinator use the default logic. - var resolveMinimizePointHandler: (@MainActor (_ context: ResolveContext) -> CGPoint?)? + // MARK: - Stored properties - private var token: Int? - private var minimizeAnchor: MinimizeAnchor? + private var currentToken: Int? + private var resolveHandler: ResolveMinimizePointHandler? private var orientationObserver: NSObjectProtocol? + // MARK: - Init + init() { orientationObserver = NotificationCenter.default.addObserver( forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main ) { [weak self] _ in - guard let self else { - return - } + guard let self else { return } Task { @MainActor in // Small delay to let the window/layout settle after rotation. - try? await Task.sleep(for: .milliseconds(50)) + try? await Task.sleep(for: .milliseconds(250)) self.refreshPosition(animated: true) } } @@ -380,62 +383,67 @@ final class LucidBannerMinimizeCoordinator { } } + // MARK: - Registration - - func register(token: Int?, - corner: MinimizeAnchor.Corner, - inset: CGSize) { + /// Registers the active banner token and the mandatory resolver. + /// + /// - Parameters: + /// - token: Banner token returned by `LucidBanner.shared.show(...)`. + /// - resolveMinimizePoint: Mandatory handler that returns the minimized target point. + func register(token: Int?, resolveMinimizePoint: @escaping ResolveMinimizePointHandler) { guard let token else { + clear() return } - self.token = token - minimizeAnchor = .corner(corner, inset: inset) + + currentToken = token + resolveHandler = resolveMinimizePoint } + /// Clears the coordinator state. func clear() { - token = nil - minimizeAnchor = nil + currentToken = nil + resolveHandler = nil } - func setMinimizePoint(_ point: CGPoint?) { - if let point { - minimizeAnchor = .absolute(point) - } else { - minimizeAnchor = nil - } - } + // MARK: - Public API - func moveIfMinimized(to point: CGPoint, animated: Bool = true) { - guard let token, - LucidBanner.shared.isAlive(token) else { + /// Handles a tap gesture coming from the SwiftUI banner content. + /// + /// The coordinator uses the registered token and resolver. + func handleTap(_ state: LucidBannerState) { + guard let token = currentToken else { return } + + guard LucidBanner.shared.isAlive(token) else { clear() return } - guard let state = LucidBanner.shared.currentState(for: token), - state.isMinimized else { - return - } - LucidBanner.shared.move( - toX: point.x, - y: point.y, - for: token, - animated: animated - ) + if state.isMinimized { + maximize(state) + } else { + minimize(state) + } } + /// Refreshes banner position after rotation/layout changes. + /// + /// If minimized, recomputes the target via the resolver and moves there. + /// If not minimized, resets to the standard LucidBanner position. func refreshPosition(animated: Bool = true) { - guard let token, - LucidBanner.shared.isAlive(token) else { + guard let token = currentToken else { return } + + guard LucidBanner.shared.isAlive(token) else { clear() return } + guard let state = LucidBanner.shared.currentState(for: token) else { return } if state.isMinimized { - guard let target = resolvedMinimizePoint() else { + guard let target = resolvedMinimizePoint(for: token, state: state) else { return } @@ -450,27 +458,42 @@ final class LucidBannerMinimizeCoordinator { } } - func handleTap(_ state: LucidBannerState) { + /// Moves the banner only if it is currently minimized. + func moveIfMinimized(to point: CGPoint, animated: Bool = true) { + guard let token = currentToken else { return } + guard LucidBanner.shared.isAlive(token) else { clear() return } - if state.isMinimized { - maximize(state: state) - } else { - minimize(state: state) + guard let state = LucidBanner.shared.currentState(for: token), + state.isMinimized else { + return } + + LucidBanner.shared.move( + toX: point.x, + y: point.y, + for: token, + animated: animated + ) } - private func minimize(state: LucidBannerState) { + // MARK: - Private helpers + + private func minimize(_ state: LucidBannerState) { + guard let token = currentToken else { return } + state.isMinimized = true + // Disable dragging while minimized. LucidBanner.shared.setDraggingEnabled(false, for: token) - // Re-measure + + // Re-measure for the compact (minimized) SwiftUI layout. LucidBanner.shared.requestRelayout(animated: false) - // Move in the point - if let target = resolvedMinimizePoint() { + + if let target = resolvedMinimizePoint(for: token, state: state) { LucidBanner.shared.move( toX: target.x, y: target.y, @@ -480,111 +503,40 @@ final class LucidBannerMinimizeCoordinator { } } - private func maximize(state: LucidBannerState) { + private func maximize(_ state: LucidBannerState) { + guard let token = currentToken else { return } + state.isMinimized = false if state.draggable { LucidBanner.shared.setDraggingEnabled(true, for: token) } - // Re-measure + + // Re-measure for the full SwiftUI layout. LucidBanner.shared.requestRelayout(animated: false) - // Then animate back to the standard position managed by LucidBanner. + + // Let LucidBanner restore the canonical position. LucidBanner.shared.resetPosition(for: token, animated: true) } - private func resolvedMinimizePoint() -> CGPoint? { - guard let token, - let anchor = minimizeAnchor, - let hostView = LucidBanner.shared.currentHostView(for: token), - let window = hostView.window, - let state = LucidBanner.shared.currentState(for: token) - else { + private func resolvedMinimizePoint(for token: Int, state: LucidBannerState) -> CGPoint? { + guard let resolveHandler else { return nil } + + guard let hostView = LucidBanner.shared.currentHostView(for: token), + let window = hostView.window else { return nil } - let context = ResolveContext( + let ctx = ResolveContext( token: token, + state: state, + hostView: hostView, window: window, bounds: window.bounds, - safeAreaInsets: window.safeAreaInsets, - anchor: anchor, - state: state + safeAreaInsets: window.safeAreaInsets ) - if let custom = resolveMinimizePointHandler?(context) { - return custom - } - - let bounds = window.bounds - let safe = window.safeAreaInsets - - switch anchor { - case .absolute(let point): - return point - - case .corner(let corner, let inset): - let verticalBase = max(safe.top, safe.bottom) - - switch corner { - - // --- TOP ROW --- - case .topLeading: - return CGPoint( - x: bounds.minX + safe.left + inset.width, - y: bounds.minY + verticalBase + inset.height - ) - - case .topCenter: - return CGPoint( - x: bounds.midX, - y: bounds.minY + verticalBase + inset.height - ) - - case .topTrailing: - return CGPoint( - x: bounds.maxX - safe.right - inset.width, - y: bounds.minY + verticalBase + inset.height - ) - - // --- CENTER ROW --- - case .centerLeading: - return CGPoint( - x: bounds.minX + safe.left + inset.width, - y: bounds.midY - ) - - case .center: - return CGPoint( - x: bounds.midX, - y: bounds.midY - ) - - case .centerTrailing: - return CGPoint( - x: bounds.maxX - safe.right - inset.width, - y: bounds.midY - ) - - // --- BOTTOM ROW --- - case .bottomLeading: - return CGPoint( - x: bounds.minX + safe.left + inset.width, - y: bounds.maxY - verticalBase - inset.height - ) - - case .bottomCenter: - return CGPoint( - x: bounds.midX, - y: bounds.maxY - verticalBase - inset.height - ) - - case .bottomTrailing: - return CGPoint( - x: bounds.maxX - safe.right - inset.width, - y: bounds.maxY - verticalBase - inset.height - ) - } - } + return resolveHandler(ctx) } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index bb132deea2..95d4b5683f 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -368,8 +368,6 @@ actor NCNetworkingProcess { vPosition: .bottom, verticalMargin: 55, blocksTouches: true, - inset: inset, - corner: .bottomCenter, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() @@ -413,9 +411,6 @@ actor NCNetworkingProcess { var currentUploadTask: Task<(account: String, file: NKFile?, error: NKError), Never>? var tokenBanner: Int? let scene = SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene - let controller = SceneManager.shared.getController(scene: scene) - let visibleBarHeight = controller?.visibleBarHeight ?? 55 - let inset = CGSize(width: 0, height: visibleBarHeight) tokenBanner = showUploadBanner(scene: scene, vPosition: .bottom, @@ -423,8 +418,6 @@ actor NCNetworkingProcess { draggable: true, stage: .init(rawValue: "button"), allowMinimizeOnTap: true, - inset: inset, - corner: .bottomCenter, onButtonTap: { if let currentUploadTask { currentUploadTask.cancel() From 92164e343f50af62ea86338f2188db3d4d966cf6 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 12 Dec 2025 10:52:01 +0100 Subject: [PATCH 10/12] cod Signed-off-by: Marino Faggiana --- iOSClient/GUI/Lucid Banner/UploadBannerView.swift | 15 +++++++++++---- iOSClient/Main/NCMainTabBarController.swift | 10 ++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index d70bd2a067..f364bac95e 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -304,15 +304,22 @@ func showUploadBanner(scene: UIWindowScene?, #if !EXTENSION if allowMinimizeOnTap { LucidBannerMinimizeCoordinator.shared.register(token: token) { context in - let controller = SceneManager.shared.getController(scene: scene) - let height = controller?.visibleBarHeight ?? 55 let bounds = context.bounds - let safeAreaInsets = context.safeAreaInsets + let controller = SceneManager.shared.getController(scene: scene) + var height: CGFloat = 55 let over: CGFloat = 20 + if let scene, + let controller, + let window = scene.windows.first { + let isPadLayout = (window.rootViewController?.traitCollection.horizontalSizeClass == .regular) + if !isPadLayout { + height = controller.barHeightBottom + context.safeAreaInsets.bottom + over + } + } return CGPoint( x: bounds.midX, - y: bounds.maxY - safeAreaInsets.bottom - height + over + y: bounds.maxY - height ) } } diff --git a/iOSClient/Main/NCMainTabBarController.swift b/iOSClient/Main/NCMainTabBarController.swift index 11bb9d9527..bc8699fe7a 100644 --- a/iOSClient/Main/NCMainTabBarController.swift +++ b/iOSClient/Main/NCMainTabBarController.swift @@ -31,10 +31,12 @@ class NCMainTabBarController: UITabBarController { return SceneManager.shared.getWindow(controller: self) } - var visibleBarHeight: CGFloat { - let safeBottom = tabBar.safeAreaInsets.bottom - let height = tabBar.frame.height - safeBottom - return height + var barHeightBottom: CGFloat { + return tabBar.frame.height - tabBar.safeAreaInsets.bottom + } + + var barHeightTop: CGFloat { + return tabBar.frame.height - tabBar.safeAreaInsets.top } override func viewDidLoad() { From 136ac6915d37c0a5cb7876cd7b8031f1c5e9fcf0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 12 Dec 2025 11:08:27 +0100 Subject: [PATCH 11/12] code Signed-off-by: Marino Faggiana --- iOSClient/GUI/Lucid Banner/UploadBannerView.swift | 2 +- iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift | 2 +- iOSClient/Networking/NCNetworkingProcess.swift | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index f364bac95e..34659f4acf 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -26,7 +26,7 @@ struct UploadBannerView: View { let isSuccess = (state.typedStage == .success) let isError = (state.typedStage == .error) - let isButton = (state.typedStage == .init(rawValue: "button")) + let isButton = (state.typedStage == .button) containerView(state: state) { if state.isMinimized { diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift index 94f0119e95..fc10b27b2a 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift @@ -259,7 +259,7 @@ class NCNetworkingE2EEUpload: NSObject { systemImage: "gearshape.arrow.triangle.2.circlepath", imageAnimation: .rotate, progress: 0, - stage: .none, + stage: .placeholder, for: tokenBanner) } } diff --git a/iOSClient/Networking/NCNetworkingProcess.swift b/iOSClient/Networking/NCNetworkingProcess.swift index 95d4b5683f..fb4f4bb7e8 100644 --- a/iOSClient/Networking/NCNetworkingProcess.swift +++ b/iOSClient/Networking/NCNetworkingProcess.swift @@ -362,11 +362,8 @@ actor NCNetworkingProcess { var request: UploadRequest? let controller = await getController(account: metadata.account, sceneIdentifier: metadata.sceneIdentifier) let scene = await SceneManager.shared.getWindow(sceneIdentifier: metadata.sceneIdentifier)?.windowScene - let inset = CGSize(width: 0, height: 55) let token = await showUploadBanner(scene: scene, - vPosition: .bottom, - verticalMargin: 55, blocksTouches: true, onButtonTap: { if let currentUploadTask { @@ -379,7 +376,7 @@ actor NCNetworkingProcess { await NCNetworkingE2EEUpload().upload(metadata: metadata, controller: controller, - stageBanner: .init(rawValue: "button"), + stageBanner: .button, tokenBanner: token) { uploadRequest in request = uploadRequest } currentUploadTask: { task in @@ -416,7 +413,7 @@ actor NCNetworkingProcess { vPosition: .bottom, verticalMargin: 55, draggable: true, - stage: .init(rawValue: "button"), + stage: .button, allowMinimizeOnTap: true, onButtonTap: { if let currentUploadTask { @@ -458,7 +455,7 @@ actor NCNetworkingProcess { systemImage: "gearshape.arrow.triangle.2.circlepath", imageAnimation: .rotate, progress: 0, - stage: .init(rawValue: "none"), + stage: .placeholder, for: tokenBanner) } } From 9a621e5dccf7d4dce188255fdbc6fcebfb5a354a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 12 Dec 2025 11:20:27 +0100 Subject: [PATCH 12/12] lint Signed-off-by: Marino Faggiana --- iOSClient/Main/Collection Common/Cell/NCListCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 0f92bde576..8a7f0e3b74 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -270,7 +270,7 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto labelInfo.isHidden = true labelSubinfo.isHidden = true labelInfoSeparator.isHidden = true - + if let tag = tags.first { tag0.text = tag if tags.count > 1 {