diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index cd06c28c8e..da5c656adc 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -38,6 +38,7 @@ func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: struct ErrorBannerView: View { @ObservedObject var state: LucidBannerState + let textColor = Color(.label) var body: some View { let showSubtitle = !(state.subtitle?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) @@ -55,21 +56,21 @@ struct ErrorBannerView: View { .font(.subheadline.weight(.bold)) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.white) + .foregroundStyle(textColor) if showSubtitle, let subtitle = state.subtitle { Text(subtitle) .font(.subheadline) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.white) + .foregroundStyle(textColor) } if showFootnote, let footnote = state.footnote { Text(footnote) .font(.caption) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.white) + .foregroundStyle(textColor) } } } @@ -84,6 +85,9 @@ struct ErrorBannerView: View { @ViewBuilder func containerView(@ViewBuilder _ content: () -> Content) -> some View { + let cornerRadius: CGFloat = 22 + let errorColor = Color.red.opacity(0.75) + let contentBase = content() .contentShape(Rectangle()) .frame(maxWidth: 500) @@ -91,16 +95,16 @@ struct ErrorBannerView: View { if #available(iOS 26, *) { contentBase .background( - RoundedRectangle(cornerRadius: 22) - .fill(Color.red.opacity(1)) + RoundedRectangle(cornerRadius: cornerRadius) + .fill(errorColor) ) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22)) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: 22)) .frame(maxWidth: .infinity, alignment: .center) } else { contentBase - .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22.0)) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) .overlay( - RoundedRectangle(cornerRadius: 22, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(.white.opacity(0.9), lineWidth: 0.6) ) .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) @@ -113,11 +117,14 @@ struct ErrorBannerView: View { #Preview { ZStack { - LinearGradient( - colors: [.white, .gray.opacity(0.1)], - startPoint: .top, - endPoint: .bottom - ) + Text( + Array(0...500) + .map(String.init) + .joined(separator: " ") + ) + .font(.system(size: 16, design: .monospaced)) + .foregroundStyle(.primary) + .padding() ErrorBannerView( state: LucidBannerState( diff --git a/iOSClient/GUI/Lucid Banner/HudBannerView.swift b/iOSClient/GUI/Lucid Banner/HudBannerView.swift index a01c9f36f6..4cbea4cad3 100644 --- a/iOSClient/GUI/Lucid Banner/HudBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/HudBannerView.swift @@ -6,7 +6,11 @@ import SwiftUI import LucidBanner @MainActor -func showHudBanner(scene: UIWindowScene?, title: String? = nil, subtitle: String? = nil, onTap: ((_ token: Int?, _ stage: String?) -> Void)? = nil) -> Int? { +func showHudBanner(scene: UIWindowScene?, + title: String? = nil, + subtitle: String? = nil, + stage: LucidBanner.Stage? = nil, + onButtonTap: (() -> Void)? = nil) -> Int? { var scene = scene if scene == nil { scene = UIApplication.shared.mainAppWindow?.windowScene @@ -18,11 +22,9 @@ func showHudBanner(scene: UIWindowScene?, title: String? = nil, subtitle: String subtitle: subtitle, vPosition: .center, blocksTouches: true, - onTap: { token, stage in - onTap?(token, stage) - } + stage: stage ) { state in - HudBannerView(state: state) + HudBannerView(state: state, onButtonTap: onButtonTap) } } @@ -56,16 +58,25 @@ struct HudBannerView: View { @ObservedObject var state: LucidBannerState @State private var displayedProgress: Double = 0 + let onButtonTap: (() -> Void)? + + private let textColor = Color(.label) private let circleSize: CGFloat = 90 private let lineWidth: CGFloat = 8 + init(state: LucidBannerState, + onButtonTap: (() -> Void)? = nil) { + self.state = state + self.onButtonTap = onButtonTap + } + var body: some View { let rawProgress = state.progress ?? 0 let clampedProgress = min(max(rawProgress, 0), 1) - let stage = state.stage?.lowercased() - let isSuccess = (stage == "success") - let isError = (stage == "error") + let isSuccess = (state.typedStage == .success) + let isError = (state.typedStage == .error) + let isButton = (state.typedStage == .button) let visualProgress: Double = { if isSuccess || isError { @@ -88,7 +99,7 @@ struct HudBannerView: View { if let title = state.title, !title.isEmpty { Text(title) .font(.headline.weight(.semibold)) - .foregroundStyle(.primary) + .foregroundStyle(textColor) .multilineTextAlignment(.center) } @@ -96,7 +107,7 @@ struct HudBannerView: View { if let subtitle = state.subtitle, !subtitle.isEmpty { Text(subtitle) .font(.subheadline) - .foregroundStyle(.primary.opacity(0.95)) + .foregroundStyle(textColor) .multilineTextAlignment(.center) } @@ -121,9 +132,6 @@ struct HudBannerView: View { .frame(width: circleSize, height: circleSize) // Center content: - // - checkmark for success - // - xmark for error - // - percentage for normal progress Group { if isSuccess { Image(systemName: "checkmark") @@ -136,11 +144,26 @@ struct HudBannerView: View { } else { Text("\(Int(visualProgress * 100))%") .font(.headline.monospacedDigit()) - .foregroundStyle(.primary) + .foregroundStyle(textColor) } } } .padding(.top, 4) + + if isButton { + VStack { + Button("_cancel_") { + onButtonTap?() + } + .buttonStyle(.plain) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background( + Capsule() + .stroke(.gray, lineWidth: 1) + ) + } + } } .padding(.horizontal, 22) .padding(.vertical, 24) @@ -189,14 +212,22 @@ struct HudBannerView: View { @ViewBuilder func containerView(@ViewBuilder _ content: () -> Content) -> some View { + let cornerRadius: CGFloat = 22 + let opacity = 0.65 + let backgroundColor = Color(.systemBackground).opacity(0.65) + if #available(iOS 26, *) { content() - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: 22)) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(backgroundColor) + ) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: cornerRadius)) } else { content() - .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 22.0)) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) .overlay( - RoundedRectangle(cornerRadius: 22, style: .continuous) + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) .stroke(.white.opacity(0.9), lineWidth: 0.6) ) .shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 4) @@ -208,6 +239,15 @@ struct HudBannerView: View { #Preview("HudBannerView") { ZStack { + Text( + Array(0...500) + .map(String.init) + .joined(separator: " ") + ) + .font(.system(size: 16, design: .monospaced)) + .foregroundStyle(.primary) + .padding() + HudBannerPreviewWrapper() } } @@ -217,7 +257,8 @@ private struct HudBannerPreviewWrapper: View { title: "Uploading files", subtitle: "Syncing your library…", footnote: nil, - imageAnimation: .none + imageAnimation: .none, + stage: "button" ) var body: some View { diff --git a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift index a7fa63c99e..32210f533e 100644 --- a/iOSClient/GUI/Lucid Banner/UploadBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/UploadBannerView.swift @@ -37,12 +37,15 @@ func showUploadBanner(scene: UIWindowScene?, let bounds = context.bounds let controller = SceneManager.shared.getController(scene: scene) var height: CGFloat = 55 - let over: CGFloat = 20 + let over: CGFloat = 30 if let scene, let controller, let window = scene.windows.first { - let isPadLayout = (window.rootViewController?.traitCollection.horizontalSizeClass == .regular) - if !isPadLayout { + let regularLayout = (window.rootViewController?.traitCollection.horizontalSizeClass == .regular) + let iPad = UIDevice.current.userInterfaceIdiom == .pad + if iPad, regularLayout { + height = controller.barHeightBottom + context.safeAreaInsets.bottom + over + } else { height = controller.barHeightBottom + context.safeAreaInsets.bottom + over } } @@ -64,6 +67,7 @@ struct UploadBannerView: View { @State var trigger = true let onButtonTap: (() -> Void)? let allowMinimizeOnTap: Bool + let textColor = Color(.label) init(state: LucidBannerState, allowMinimizeOnTap: Bool = false, @@ -93,11 +97,12 @@ struct UploadBannerView: View { Text("\(Int(p * 100))%") .font(.caption2.monospacedDigit()) .frame(height: 20) + .foregroundStyle(textColor) } } - .padding(.horizontal, 3) - .padding(.vertical, 3) + .padding(.horizontal, 10) + .padding(.vertical, 10) .clipShape(Capsule()) } else if isSuccess { HStack(alignment: .center, spacing: 10) { @@ -131,13 +136,13 @@ struct UploadBannerView: View { .multilineTextAlignment(.leading) .truncationMode(.tail) .minimumScaleFactor(0.9) - .foregroundStyle(.primary) + .foregroundStyle(textColor) if showSubtitle, let subtitle = state.subtitle { Text(subtitle) .font(.subheadline) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.primary) + .foregroundStyle(textColor) } } } @@ -162,21 +167,21 @@ struct UploadBannerView: View { .multilineTextAlignment(.leading) .truncationMode(.tail) .minimumScaleFactor(0.9) - .foregroundStyle(.primary) + .foregroundStyle(textColor) } if showSubtitle, let subtitle = state.subtitle { Text(subtitle) .font(.subheadline) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.primary) + .foregroundStyle(textColor) } if showFootnote, let footnote = state.footnote { Text(footnote) .font(.caption) .multilineTextAlignment(.leading) .truncationMode(.tail) - .foregroundStyle(.primary) + .foregroundStyle(textColor) } } } @@ -192,11 +197,12 @@ struct UploadBannerView: View { onButtonTap?() } .buttonStyle(.plain) + .foregroundStyle(textColor) .padding(.horizontal, 20) .padding(.vertical, 10) .background( Capsule() - .stroke(.primary.opacity(0.2), lineWidth: 1) + .stroke(.gray, lineWidth: 1) ) } .padding(15) @@ -214,8 +220,11 @@ struct UploadBannerView: View { @ViewBuilder func containerView(state: LucidBannerState, @ViewBuilder _ content: () -> Content) -> some View { let isError = (state.typedStage == .error) - let cornerRadius: CGFloat = 22 + let isSuccess = (state.typedStage == .success) let isMinimized = state.isMinimized + let cornerRadius: CGFloat = state.isMinimized ? 15 : 25 + let backgroundColor = Color(.systemBackground).opacity(0.65) + let errorColor = Color.red.opacity(0.75) let base = content() .contentShape(Rectangle()) @@ -224,18 +233,22 @@ struct UploadBannerView: View { LucidBannerMinimizeCoordinator.shared.handleTap(state) } - if isMinimized { + if isMinimized || isSuccess { if #available(iOS 26, *) { if isError { base .background( RoundedRectangle(cornerRadius: cornerRadius) - .fill(Color.red.opacity(1)) + .fill(errorColor) ) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: cornerRadius)) } else { base - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(backgroundColor) + ) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: cornerRadius)) } } else { let colorBg = isError ? Color.red.opacity(0.9) : Color.white.opacity(0.9) @@ -257,13 +270,17 @@ struct UploadBannerView: View { contentBase .background( RoundedRectangle(cornerRadius: cornerRadius) - .fill(Color.red.opacity(1)) + .fill(errorColor) ) - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: cornerRadius)) .frame(maxWidth: .infinity, alignment: .center) } else { contentBase - .glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius)) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(backgroundColor) + ) + .glassEffect(.clear, in: RoundedRectangle(cornerRadius: cornerRadius)) .frame(maxWidth: .infinity, alignment: .center) } } else { @@ -342,18 +359,21 @@ public extension View { stage: "button" ) - state.isMinimized = true + state.isMinimized = false return ZStack { - LinearGradient( - colors: [.white, .gray.opacity(0.1)], - startPoint: .top, - endPoint: .bottom - ) + Text( + Array(0...500) + .map(String.init) + .joined(separator: " ") + ) + .font(.system(size: 16, design: .monospaced)) + .foregroundStyle(.primary) + .padding() UploadBannerView( state: state, - allowMinimizeOnTap: true + allowMinimizeOnTap: false ) .padding() } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 8f3a08f851..3f2d134ddc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -31,11 +31,13 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { var tokenBanner: Int? await MainActor.run { tokenBanner = showHudBanner(scene: scene, - title: NSLocalizedString("_downloading_", comment: "")) { _, _ in + title: NSLocalizedString("_download_in_progress_", comment: ""), + stage: .button, + onButtonTap: { if let request = downloadRequest { request.cancel() } - } + }) } guard let metadata = await database.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index c2280e256a..0348071348 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -6,6 +6,7 @@ import Foundation import UIKit import NextcloudKit import LucidBanner +import Alamofire class NCCreate: NSObject { let utility = NCUtility() @@ -257,6 +258,7 @@ class NCCreate: NSObject { var exportURLs: [URL] = [] var downloadMetadata: [(tableMetadata, URL)] = [] let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene + var downloadRequest: DownloadRequest? for metadata in metadatas { let localPath = utilityFileSystem.getDirectoryProviderStorageOcId( @@ -275,12 +277,14 @@ class NCCreate: NSObject { } if !downloadMetadata.isEmpty { - let token = showHudBanner( - scene: scene, - title: NSLocalizedString("_download_in_progress_", comment: "") - ) + let token = showHudBanner(scene: scene, + title: NSLocalizedString("_download_in_progress_", comment: ""), + stage: .button) { + if let downloadRequest { + downloadRequest.cancel() + } + } - // Download missing files for (originalMetadata, localFileURL) in downloadMetadata { guard let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync( ocId: originalMetadata.ocId, @@ -294,8 +298,8 @@ class NCCreate: NSObject { let results = await NCNetworking.shared.downloadFile( metadata: metadata - ) { _ in - // downloadStartHandler not used here + ) { request in + downloadRequest = request } progressHandler: { progress in Task { @MainActor in LucidBanner.shared.update( diff --git a/iOSClient/Menu/NCContextMenu.swift b/iOSClient/Menu/NCContextMenu.swift index bccd3b52fb..b69dd7d94d 100644 --- a/iOSClient/Menu/NCContextMenu.swift +++ b/iOSClient/Menu/NCContextMenu.swift @@ -98,12 +98,12 @@ class NCContextMenu: NSObject { return } - let token = showHudBanner( - scene: scene, - title: NSLocalizedString("_download_in_progress_", comment: "")) { _, _ in - if let request = downloadRequest { - request.cancel() - } + let token = showHudBanner(scene: scene, + title: NSLocalizedString("_download_in_progress_", comment: ""), + stage: .button) { + if let request = downloadRequest { + request.cancel() + } } let results = await self.networking.downloadFile(metadata: metadata) { request in diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index bfc52f257c..fdb2c8428a 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -376,7 +376,6 @@ extension NCNetworking { if !metadatasE2EE.isEmpty { #if !EXTENSION - if isOffline { return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) } @@ -385,9 +384,11 @@ extension NCNetworking { var num: Float = 0 let total = Float(metadatasE2EE.count) var cancelOnTap = false + let scene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene - let token = showHudBanner(scene: SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene, - title: NSLocalizedString("_delete_in_progress_", comment: "")) { _, _ in + let token = showHudBanner(scene: scene, + title: NSLocalizedString("_delete_in_progress_", comment: ""), + stage: .button) { cancelOnTap = true } @@ -417,7 +418,6 @@ extension NCNetworking { LucidBanner.shared.dismiss() } - #endif } else { var ocIds = Set() diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index 4d044ee6e0..5b138cd7fe 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -438,7 +438,8 @@ extension NCPlayerToolBar: NCSelectDelegate { } else { var downloadRequest: DownloadRequest? let token = showHudBanner(scene: scene, - title: NSLocalizedString("_downloading_", comment: "")) { _, _ in + title: NSLocalizedString("_download_in_progress_", comment: ""), + stage: .button) { if let request = downloadRequest { request.cancel() } diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift index 0c29fdd9d9..0b2d267e41 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift @@ -157,7 +157,8 @@ class NCViewerMedia: UIViewController { let scene = SceneManager.shared.getWindow(controller: self.tabBarController)?.windowScene var downloadRequest: DownloadRequest? let token = showHudBanner(scene: scene, - title: NSLocalizedString("_download_in_progress_", comment: "")) { _, _ in + title: NSLocalizedString("_download_in_progress_", comment: ""), + stage: .button) { if let request = downloadRequest { request.cancel() }