Skip to content
Merged

Banner #3948

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@
F7D7A7712DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; };
F7D7A7722DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; };
F7D890752BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */; };
F7DF7B3F2F1A2EF900514020 /* MessageBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B3E2F1A2EE400514020 /* MessageBannerView.swift */; };
F7DF7B422F1A36C100514020 /* HelperBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B412F1A36B600514020 /* HelperBanner.swift */; };
F7DF7B432F1A373E00514020 /* HelperBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B412F1A36B600514020 /* HelperBanner.swift */; };
F7E0710128B13BB00001B882 /* DashboardData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E0710028B13BB00001B882 /* DashboardData.swift */; };
F7E2B64F2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E2B64E2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift */; };
F7E402292BA85D1D007E5609 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */; };
Expand Down Expand Up @@ -1760,6 +1763,8 @@
F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+AutoUpload.swift"; sourceTree = "<group>"; };
F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+DragDrop.swift"; sourceTree = "<group>"; };
F7DE9AB01F482FA5008DFE10 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
F7DF7B3E2F1A2EE400514020 /* MessageBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBannerView.swift; sourceTree = "<group>"; };
F7DF7B412F1A36B600514020 /* HelperBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperBanner.swift; sourceTree = "<group>"; };
F7E0710028B13BB00001B882 /* DashboardData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardData.swift; sourceTree = "<group>"; };
F7E2B64E2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+TransferDelegate.swift"; sourceTree = "<group>"; };
F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2239,7 +2244,9 @@
isa = PBXGroup;
children = (
F70557BA2ED44F1800135623 /* ErrorBannerView.swift */,
F7DF7B412F1A36B600514020 /* HelperBanner.swift */,
F714A1462ED84AF00050A43B /* HudBannerView.swift */,
F7DF7B3E2F1A2EE400514020 /* MessageBannerView.swift */,
F70557BB2ED44F1800135623 /* UploadBannerView.swift */,
);
path = "Lucid Banner";
Expand Down Expand Up @@ -4219,6 +4226,7 @@
F798F0E225880608000DAFFD /* UIColor+Extension.swift in Sources */,
F7C9B9202B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */,
F78E2D6829AF02DB0024D4F3 /* Database.swift in Sources */,
F7DF7B432F1A373E00514020 /* HelperBanner.swift in Sources */,
AA8D31562D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift in Sources */,
F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */,
F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */,
Expand Down Expand Up @@ -4431,6 +4439,7 @@
F733598125C1C188002ABA72 /* NCAskAuthorization.swift in Sources */,
370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */,
F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */,
F7DF7B3F2F1A2EF900514020 /* MessageBannerView.swift in Sources */,
F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */,
F7CAFE1D2F17A35F00DB35A5 /* NCNetworking+Actor.swift in Sources */,
F71CD6CA2930D7B1006C95C1 /* NCApplicationHandle.swift in Sources */,
Expand Down Expand Up @@ -4551,6 +4560,7 @@
F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */,
F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */,
AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */,
F7DF7B422F1A36C100514020 /* HelperBanner.swift in Sources */,
F78ACD4A21903F850088454D /* NCTrashListCell.swift in Sources */,
F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */,
F76882352C0DD1E7001CF441 /* NCWebBrowserView.swift in Sources */,
Expand Down
5 changes: 4 additions & 1 deletion Share/NCShareExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class NCShareExtension: UIViewController {
let global = NCGlobal.shared
var maintenanceMode: Bool = false
var token: Int?
var sceneIdentifier: String = UUID().uuidString

// MARK: - View Life Cycle

Expand Down Expand Up @@ -110,6 +111,8 @@ class NCShareExtension: UIViewController {
if let account = NCShareExtensionData.shared.getTblAccoun()?.account {
accountRequestChangeAccount(account: account, controller: nil)
}

NCNetworking.shared.setupScene(sceneIdentifier: sceneIdentifier, controller: self)
}

override func viewWillAppear(_ animated: Bool) {
Expand Down Expand Up @@ -330,7 +333,7 @@ extension NCShareExtension {
ocId: ocId,
serverUrl: serverUrl,
session: session,
sceneIdentifier: nil)
sceneIdentifier: self.sceneIdentifier)

metadataForUpload.session = NCNetworking.shared.sessionUpload
metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFileShareExtension
Expand Down
6 changes: 3 additions & 3 deletions iOSClient/Activity/NCActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class NCActivity: UIViewController, NCSharePagingContent {
self.commentView?.newCommentField.text?.removeAll()
self.loadComments()
} else {
Task {@MainActor in
Task {
await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription)
}
}
Expand Down Expand Up @@ -438,7 +438,7 @@ extension NCActivity {
if error == .success, let comments = comments {
self.database.addComments(comments, account: metadata.account, objectId: metadata.fileId)
} else if error.errorCode != NCGlobal.shared.errorResourceNotFound {
Task {@MainActor in
Task {
await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription)
}
}
Expand Down Expand Up @@ -610,7 +610,7 @@ extension NCActivity: NCShareCommentsCellDelegate {
if error == .success {
self.loadComments()
} else {
Task {@MainActor in
Task {
await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription)
}
}
Expand Down
2 changes: 1 addition & 1 deletion iOSClient/Activity/NCActivityTableViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ extension NCActivityTableViewCell: UICollectionViewDelegate {
(responder as? UIViewController)!.navigationController?.pushViewController(viewController, animated: true)
} else {
let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_trash_file_not_found_")
Task {@MainActor in
Task {
await showErrorBanner(controller: viewController.controller, errorDescription: error.errorDescription)
}
}
Expand Down
6 changes: 3 additions & 3 deletions iOSClient/Files/NCFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class NCFiles: NCCollectionViewCommon {
}
} else {
// show error
Task {@MainActor in
Task {
await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription)
}
}
Expand All @@ -345,7 +345,7 @@ class NCFiles: NCCollectionViewCommon {

let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, updateVersionV1V2: true, account: account)
if error != .success {
Task {@MainActor in
Task {
await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription)
}
}
Expand All @@ -354,7 +354,7 @@ class NCFiles: NCCollectionViewCommon {
} else {
// Client Diagnostic
await self.database.addDiagnosticAsync(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors)
Task {@MainActor in
Task {
await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription)
}
}
Expand Down
9 changes: 6 additions & 3 deletions iOSClient/GUI/Lucid Banner/ErrorBannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import LucidBanner
@MainActor
func showErrorBanner(controller: UITabBarController?, errorDescription: String, footnote: String? = nil, sleepBefore: Double = 1) async {
let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene
await showErrorBanner(scene: scene, errorDescription: errorDescription, footnote: footnote, sleepBefore: sleepBefore)
await showErrorBanner(scene: scene,
errorDescription: NSLocalizedString(errorDescription, comment: ""),
footnote: NSLocalizedString(footnote ?? "", comment: ""),
sleepBefore: sleepBefore)
}

@MainActor
Expand All @@ -21,8 +24,8 @@ func showErrorBanner(scene: UIWindowScene?, errorDescription: String, footnote:

LucidBanner.shared.show(
scene: scene,
subtitle: errorDescription,
footnote: footnote,
subtitle: NSLocalizedString(errorDescription, comment: ""),
footnote: NSLocalizedString(footnote ?? "", comment: ""),
vPosition: .top,
autoDismissAfter: NCGlobal.shared.dismissAfterSecond,
swipeToDismiss: true,
Expand Down
55 changes: 55 additions & 0 deletions iOSClient/GUI/Lucid Banner/HelperBanner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Marino Faggiana
// SPDX-License-Identifier: GPL-3.0-or-later

import SwiftUI
import LucidBanner

public extension View {
@ViewBuilder
func applyBannerAnimation(_ style: LucidBanner.LucidBannerAnimationStyle) -> some View {
switch style {

// ---- iOS 18+ effects ----
case .rotate, .pulse, .pulsebyLayer, .breathe, .bounce, .wiggle, .scale, .scaleUpbyLayer, .variableColor:
if #available(iOS 18, *) {
switch style {
case .rotate:
self.symbolEffect(.rotate, options: .repeat(.continuous))
case .pulse:
self.symbolEffect(.pulse, options: .repeat(.continuous))
case .pulsebyLayer:
self.symbolEffect(.pulse.byLayer, options: .repeat(.continuous))
case .breathe:
self.symbolEffect(.breathe, options: .repeat(.continuous))
case .bounce:
self.symbolEffect(.bounce, options: .repeat(.continuous))
case .wiggle:
self.symbolEffect(.wiggle, options: .repeat(.continuous))
case .scale:
self.symbolEffect(.scale, options: .repeat(.continuous))
case .scaleUpbyLayer:
self.symbolEffect(.scale.up.byLayer, options: .repeat(.continuous))
case .variableColor:
self.symbolEffect(.variableColor, options: .repeat(.continuous))
default:
self
}
} else {
self
}

// ---- iOS 26+ effect: drawOn ----
case .drawOn:
if #available(iOS 26, *) {
self
} else {
self
}

// ---- no animation ----
case .none:
self
}
}
}
149 changes: 149 additions & 0 deletions iOSClient/GUI/Lucid Banner/MessageBannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Marino Faggiana
// SPDX-License-Identifier: GPL-3.0-or-later

import SwiftUI
import LucidBanner

@MainActor
func showBanner(scene: UIWindowScene?,
title: String?,
subtitle: String? = nil,
footnote: String? = nil,
textColor: UIColor,
image: String?,
imageAnimation: LucidBanner.LucidBannerAnimationStyle,
imageColor: UIColor,
vPosition: LucidBanner.VerticalPosition = .top,
backgroundColor: UIColor) async {
var scene = scene
if scene == nil {
scene = UIApplication.shared.mainAppWindow?.windowScene
}

LucidBanner.shared.show(
scene: scene,
title: NSLocalizedString(title ?? "", comment: ""),
subtitle: NSLocalizedString(subtitle ?? "", comment: ""),
footnote: NSLocalizedString(footnote ?? "", comment: ""),
textColor: Color(uiColor: textColor),
systemImage: image,
imageAnimation: imageAnimation,
imageColor: Color(uiColor: imageColor),
backgroundColor: Color(uiColor: backgroundColor),
vPosition: vPosition,
autoDismissAfter: NCGlobal.shared.dismissAfterSecond,
swipeToDismiss: true,
onTap: { _, _ in
LucidBanner.shared.dismiss()
}
) { state in
MessageBannerView(state: state)
}
}

// MARK: - SwiftUI

struct MessageBannerView: View {
@ObservedObject var state: LucidBannerState

var body: some View {
let showTitle = !(state.title?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
let showSubtitle = !(state.subtitle?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
let showFootnote = !(state.footnote?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)

containerView(state: state) {
VStack(spacing: 15) {
HStack(alignment: .top, spacing: 10) {
Image(systemName: state.systemImage ?? "info.circle")
.applyBannerAnimation(state.imageAnimation)
.font(.system(size: 30, weight: .bold))
.foregroundStyle(state.imageColor)

VStack(alignment: .leading, spacing: 7) {
if showTitle, let title = state.title {
Text(title)
.font(.subheadline.weight(.bold))
.multilineTextAlignment(.leading)
.truncationMode(.tail)
.foregroundStyle(state.textColor)
}

if showSubtitle, let subtitle = state.subtitle {
Text(subtitle)
.font(.subheadline)
.multilineTextAlignment(.leading)
.truncationMode(.tail)
.foregroundStyle(state.textColor)
}
if showFootnote, let footnote = state.footnote {
Text(footnote)
.font(.caption)
.multilineTextAlignment(.leading)
.truncationMode(.tail)
.foregroundStyle(state.textColor)
}
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
}
}

// MARK: - Container

@ViewBuilder
func containerView<Content: View>(state: LucidBannerState, @ViewBuilder _ content: () -> Content) -> some View {
let cornerRadius: CGFloat = 22
let contentBase = content()
.contentShape(Rectangle())
.frame(maxWidth: 500)

if #available(iOS 26, *) {
contentBase
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(state.backgroundColor)
)
.glassEffect(.clear, in: RoundedRectangle(cornerRadius: 22))
.frame(maxWidth: .infinity, alignment: .center)
} else {
contentBase
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius))
.overlay(
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)
.frame(maxWidth: .infinity, alignment: .center)
}
}
}

// MARK: - Preview

#Preview {
ZStack {
Text(
Array(0...500)
.map(String.init)
.joined(separator: " ")
)
.font(.system(size: 16, design: .monospaced))
.foregroundStyle(.primary)
.padding()

MessageBannerView(
state: LucidBannerState(
title: "Title",
subtitle: "Subtitle",
footnote: "footnote",
systemImage: "wifi.circle",
imageAnimation: .variableColor,
)
)
.padding()
}
}
Loading
Loading