diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 6099fc7faf..cd06c28c8e 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -5,6 +5,37 @@ import SwiftUI import LucidBanner +@MainActor +func showErrorBanner(controller: UITabBarController?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { + let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene + await showErrorBanner(scene: scene, errorDescription: errorDescription, errorCode: errorCode, sleepBefore: sleepBefore) +} + +@MainActor +func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { + try? await Task.sleep(nanoseconds: UInt64(sleepBefore * 1e9)) + var scene = scene + if scene == nil { + scene = UIApplication.shared.mainAppWindow?.windowScene + } + + LucidBanner.shared.show( + scene: scene, + subtitle: errorDescription, + footnote: "(Code: \(errorCode))", + vPosition: .top, + autoDismissAfter: NCGlobal.shared.dismissAfterSecond, + swipeToDismiss: true, + onTap: { _, _ in + LucidBanner.shared.dismiss() + } + ) { state in + ErrorBannerView(state: state) + } +} + +// MARK: - SwiftUI + struct ErrorBannerView: View { @ObservedObject var state: LucidBannerState @@ -53,56 +84,31 @@ struct ErrorBannerView: View { @ViewBuilder func containerView(@ViewBuilder _ content: () -> Content) -> some View { + let contentBase = content() + .contentShape(Rectangle()) + .frame(maxWidth: 500) + if #available(iOS 26, *) { - content() + contentBase .background( RoundedRectangle(cornerRadius: 22) .fill(Color.red.opacity(1)) ) .glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22)) + .frame(maxWidth: .infinity, alignment: .center) } else { - content() + contentBase .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22.0)) .overlay( RoundedRectangle(cornerRadius: 22, style: .continuous) .stroke(.white.opacity(0.9), lineWidth: 0.6) ) .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) + .frame(maxWidth: .infinity, alignment: .center) } } } -// MARK: - Helper - -@MainActor -func showErrorBanner(controller: UITabBarController?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { - let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene - await showErrorBanner(scene: scene, errorDescription: errorDescription, errorCode: errorCode, sleepBefore: sleepBefore) -} - -@MainActor -func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { - try? await Task.sleep(nanoseconds: UInt64(sleepBefore * 1e9)) - var scene = scene - if scene == nil { - scene = UIApplication.shared.mainAppWindow?.windowScene - } - - LucidBanner.shared.show( - scene: scene, - subtitle: errorDescription, - footnote: "(Code: \(errorCode))", - vPosition: .top, - autoDismissAfter: NCGlobal.shared.dismissAfterSecond, - swipeToDismiss: true, - onTap: { _, _ in - LucidBanner.shared.dismiss() - } - ) { state in - ErrorBannerView(state: state) - } -} - // MARK: - Preview #Preview { diff --git a/iOSClient/GUI/Lucid Banner/HudBannerView.swift b/iOSClient/GUI/Lucid Banner/HudBannerView.swift index 02b55bb1aa..a01c9f36f6 100644 --- a/iOSClient/GUI/Lucid Banner/HudBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/HudBannerView.swift @@ -5,6 +5,53 @@ import SwiftUI import LucidBanner +@MainActor +func showHudBanner(scene: UIWindowScene?, title: String? = nil, subtitle: String? = nil, onTap: ((_ token: Int?, _ stage: String?) -> Void)? = nil) -> Int? { + var scene = scene + if scene == nil { + scene = UIApplication.shared.mainAppWindow?.windowScene + } + + return LucidBanner.shared.show( + scene: scene, + title: title, + subtitle: subtitle, + vPosition: .center, + blocksTouches: true, + onTap: { token, stage in + onTap?(token, stage) + } + ) { state in + HudBannerView(state: state) + } +} + +@MainActor +func completeHudBannerSuccess( + token: Int? +) { + LucidBanner.shared.update( + stage: .success, + autoDismissAfter: 2, + for: token + ) +} + +@MainActor +func completeHudBannerError( + subtitle: String? = nil, + token: Int? +) { + LucidBanner.shared.update( + subtitle: subtitle, + stage: .error, + autoDismissAfter: NCGlobal.shared.dismissAfterSecond, + for: token + ) +} + +// MARK: - SwiftUI + struct HudBannerView: View { @ObservedObject var state: LucidBannerState @State private var displayedProgress: Double = 0 @@ -157,53 +204,6 @@ struct HudBannerView: View { } } -// MARK: - Helper - -@MainActor -func showHudBanner(scene: UIWindowScene?, title: String? = nil, subtitle: String? = nil, onTap: ((_ token: Int?, _ stage: String?) -> Void)? = nil) -> Int? { - var scene = scene - if scene == nil { - scene = UIApplication.shared.mainAppWindow?.windowScene - } - - return LucidBanner.shared.show( - scene: scene, - title: title, - subtitle: subtitle, - vPosition: .center, - blocksTouches: true, - onTap: { token, stage in - onTap?(token, stage) - } - ) { state in - HudBannerView(state: state) - } -} - -@MainActor -func completeHudBannerSuccess( - token: Int? -) { - LucidBanner.shared.update( - stage: .success, - autoDismissAfter: 2, - for: token - ) -} - -@MainActor -func completeHudBannerError( - subtitle: String? = nil, - token: Int? -) { - LucidBanner.shared.update( - subtitle: subtitle, - stage: .error, - autoDismissAfter: NCGlobal.shared.dismissAfterSecond, - for: token - ) -} - // MARK: - Preview #Preview("HudBannerView") { diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index 34659f4acf..a7fa63c99e 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -5,6 +5,60 @@ import SwiftUI import LucidBanner +@MainActor +func showUploadBanner(scene: UIWindowScene?, + vPosition: LucidBanner.VerticalPosition = .center, + hAlignment: LucidBanner.HorizontalAlignment = .center, + verticalMargin: CGFloat = 0, + blocksTouches: Bool = false, + draggable: Bool = false, + stage: LucidBanner.Stage? = nil, + policy: LucidBanner.ShowPolicy = .drop, + allowMinimizeOnTap: Bool = false, + onButtonTap: (() -> Void)? = nil) -> Int? { + let token = LucidBanner.shared.show( + scene: scene, + vPosition: vPosition, + hAlignment: hAlignment, + verticalMargin: verticalMargin, + blocksTouches: blocksTouches, + draggable: draggable, + stage: stage, + policy: policy + ) { state in + UploadBannerView(state: state, + allowMinimizeOnTap: allowMinimizeOnTap, + onButtonTap: onButtonTap) + } + +#if !EXTENSION + if allowMinimizeOnTap { + LucidBannerMinimizeCoordinator.shared.register(token: token) { context in + let bounds = context.bounds + 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 - height + ) + } + } +#endif + return token +} + +// MARK: - SwiftUI + struct UploadBannerView: View { @ObservedObject var state: LucidBannerState @State var trigger = true @@ -275,278 +329,6 @@ public extension View { } } -@MainActor -func showUploadBanner(scene: UIWindowScene?, - vPosition: LucidBanner.VerticalPosition = .center, - hAlignment: LucidBanner.HorizontalAlignment = .center, - verticalMargin: CGFloat = 0, - blocksTouches: Bool = false, - draggable: Bool = false, - stage: LucidBanner.Stage? = nil, - policy: LucidBanner.ShowPolicy = .drop, - allowMinimizeOnTap: Bool = false, - onButtonTap: (() -> Void)? = nil) -> Int? { - let token = LucidBanner.shared.show( - scene: scene, - vPosition: vPosition, - hAlignment: hAlignment, - verticalMargin: verticalMargin, - blocksTouches: blocksTouches, - draggable: draggable, - stage: stage, - policy: policy - ) { state in - UploadBannerView(state: state, - allowMinimizeOnTap: allowMinimizeOnTap, - onButtonTap: onButtonTap) - } - -#if !EXTENSION - if allowMinimizeOnTap { - LucidBannerMinimizeCoordinator.shared.register(token: token) { context in - let bounds = context.bounds - 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 - height - ) - } - } -#endif - return token -} - -@MainActor -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 - } - - /// Mandatory resolver used to compute the minimized target point. - /// - /// Return a CGPoint in window coordinates where the banner should move - /// when minimized. - typealias ResolveMinimizePointHandler = @MainActor (_ context: ResolveContext) -> CGPoint - - // MARK: - Stored properties - - 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 } - - Task { @MainActor in - // Small delay to let the window/layout settle after rotation. - try? await Task.sleep(for: .milliseconds(250)) - self.refreshPosition(animated: true) - } - } - } - - deinit { - if let orientationObserver { - NotificationCenter.default.removeObserver(orientationObserver) - } - } - - // MARK: - Registration - - /// 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 - } - - currentToken = token - resolveHandler = resolveMinimizePoint - } - - /// Clears the coordinator state. - func clear() { - currentToken = nil - resolveHandler = nil - } - - // MARK: - Public API - - /// 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 - } - - 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 = 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(for: token, state: state) else { - return - } - - LucidBanner.shared.move( - toX: target.x, - y: target.y, - for: token, - animated: animated - ) - } else { - LucidBanner.shared.resetPosition(for: token, animated: true) - } - } - - /// 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 - } - - 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 - ) - } - - // 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 for the compact (minimized) SwiftUI layout. - LucidBanner.shared.requestRelayout(animated: false) - - if let target = resolvedMinimizePoint(for: token, state: state) { - LucidBanner.shared.move( - toX: target.x, - y: target.y, - for: token, - animated: true - ) - } - } - - 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 for the full SwiftUI layout. - LucidBanner.shared.requestRelayout(animated: false) - - // Let LucidBanner restore the canonical position. - LucidBanner.shared.resetPosition(for: token, animated: true) - } - - 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 ctx = ResolveContext( - token: token, - state: state, - hostView: hostView, - window: window, - bounds: window.bounds, - safeAreaInsets: window.safeAreaInsets - ) - - return resolveHandler(ctx) - } -} - // MARK: - Preview #Preview { diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index 74501b87a7..a4fd4edbc2 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -46,7 +46,7 @@ final class TransfersViewModel: ObservableObject, NCMetadataTransfersSuccessDele @MainActor func pollTransfers() async { while !Task.isCancelled { - if isXcodeRunningForPreviews { + if !isXcodeRunningForPreviews { isLoading = true // Items