Skip to content

Commit b9871f8

Browse files
GUI Banner (#3904)
1 parent 0ebf1be commit b9871f8

14 files changed

+385
-158
lines changed

Share/NCShareExtension.swift

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import UIKit
77
import NextcloudKit
8+
import LucidBanner
89

910
enum NCShareExtensionError: Error {
1011
case cancel, fileUpload, noAccount, noFiles, versionMismatch
@@ -42,11 +43,11 @@ class NCShareExtension: UIViewController {
4243
var progress: CGFloat = 0
4344
var counterUploaded: Int = 0
4445
var uploadMetadata: [tableMetadata] = []
45-
let hud = NCHud()
4646
let utilityFileSystem = NCUtilityFileSystem()
4747
let utility = NCUtility()
4848
let global = NCGlobal.shared
4949
var maintenanceMode: Bool = false
50+
var token: Int = 0
5051

5152
// MARK: - View Life Cycle
5253

@@ -363,22 +364,32 @@ extension NCShareExtension {
363364
@MainActor
364365
func uploadAndExit() async {
365366
var error: NKError?
367+
token = showUploadBanner(scene: self.view.window?.windowScene)
368+
366369
for metadata in self.uploadMetadata {
370+
// BANNER
371+
LucidBanner.shared.update(
372+
title: NSLocalizedString("_upload_file_", comment: "") + " \(self.counterUploaded + 1) " + NSLocalizedString("_of_", comment: "") + " \(self.filesName.count)",
373+
systemImage: "arrowshape.up.circle",
374+
imageAnimation: .breathe,
375+
progress: 0
376+
)
377+
367378
error = await self.upload(metadata: metadata)
368379
if error != .success {
369380
break
370381
}
371382
}
372383

373384
if error == .success {
374-
self.hud.success()
385+
LucidBanner.shared.update(stage: .success, for: self.token)
375386
} else {
376-
self.hud.error(text: error?.errorDescription)
387+
LucidBanner.shared.update(subtitle: error?.errorDescription, stage: .error, for: self.token)
377388
}
378389

379-
try? await Task.sleep(nanoseconds: 1_500_000_000)
380-
381-
self.extensionContext?.completeRequest(returningItems: self.extensionContext?.inputItems, completionHandler: nil)
390+
LucidBanner.shared.dismiss(after: 2) {
391+
self.extensionContext?.completeRequest(returningItems: self.extensionContext?.inputItems, completionHandler: nil)
392+
}
382393
}
383394

384395
@MainActor
@@ -407,38 +418,43 @@ extension NCShareExtension {
407418
metadata.e2eEncrypted = metadata.isDirectoryE2EE
408419

409420
self.counterUploaded += 1
410-
hud.ringProgress(view: self.view, text: NSLocalizedString("_upload_file_", comment: "") + " \(self.counterUploaded) " + NSLocalizedString("_of_", comment: "") + " \(self.filesName.count)")
411421

412422
if metadata.isDirectoryE2EE {
413423
error = await NCNetworkingE2EEUpload().upload(metadata: metadata, session: session, controller: self, scene: self.view.window?.windowScene)
414424
} else if metadata.chunk > 0 {
415-
var currentUploadTask: Task<(account: String, file: NKFile?, error: NKError), Never>?
416-
417-
hud.pieProgress(text: NSLocalizedString("_wait_file_preparation_", comment: ""), tapToCancelDetailText: true) {
418-
currentUploadTask?.cancel()
419-
}
420-
425+
LucidBanner.shared.update(
426+
systemImage: "gearshape.arrow.triangle.2.circlepath",
427+
imageAnimation: .rotate,
428+
for: self.token)
421429
let task = Task { () -> (account: String, file: NKFile?, error: NKError) in
422430
let results = await NCNetworking.shared.uploadChunkFile(metadata: metadata) { total, counter in
423-
self.hud.progress(num: Float(counter), total: Float(total))
431+
Task {@MainActor in
432+
LucidBanner.shared.update(progress: Double(counter / total), for: self.token)
433+
}
424434
} uploadStart: { _ in
425-
self.hud.pieProgress(text: NSLocalizedString("_keep_active_for_upload_", comment: ""), tapToCancelDetailText: true) {
426-
currentUploadTask?.cancel()
435+
Task {@MainActor in
436+
LucidBanner.shared.update(
437+
systemImage: "arrowshape.up.circle",
438+
imageAnimation: .breathe,
439+
for: self.token)
427440
}
428441
} uploadProgressHandler: { _, _, progress in
429-
self.hud.progress(progress)
442+
Task {@MainActor in
443+
LucidBanner.shared.update(progress: progress, for: self.token)
444+
}
430445
} assembling: {
431-
self.hud.setText(NSLocalizedString("_wait_", comment: ""))
446+
Task {@MainActor in
447+
LucidBanner.shared.update(
448+
systemImage: "gearshape.arrow.triangle.2.circlepath",
449+
imageAnimation: .rotate,
450+
for: self.token)
451+
}
432452
}
433453

434454
return results
435455
}
436456

437-
currentUploadTask = task
438457
let results = await task.value
439-
440-
hud.dismiss()
441-
442458
error = results.error
443459
} else {
444460
let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId,
@@ -452,7 +468,9 @@ extension NCShareExtension {
452468
creationDate: metadata.creationDate as Date,
453469
dateModificationFile: metadata.date as Date) { _ in
454470
} progressHandler: { _, _, fractionCompleted in
455-
self.hud.progress(fractionCompleted)
471+
Task {@MainActor in
472+
LucidBanner.shared.update(progress: fractionCompleted, for: self.token)
473+
}
456474
}
457475
error = results.error
458476
}

iOSClient/GUI/Lucid Banner/ErrorBannerView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,17 @@ struct ErrorBannerView: View {
7575
// MARK: - Helper
7676

7777
@MainActor
78-
func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: Int) {
78+
func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async {
79+
try? await Task.sleep(nanoseconds: UInt64(sleepBefore * 1e9))
80+
7981
LucidBanner.shared.show(
8082
scene: scene,
8183
subtitle: errorDescription,
8284
footnote: "(Code: \(errorCode))",
83-
autoDismissAfter: NCGlobal.shared.dismissAfterSecond
85+
autoDismissAfter: NCGlobal.shared.dismissAfterSecond,
86+
onTap: { _, _ in
87+
LucidBanner.shared.dismiss()
88+
}
8489
) { state in
8590
ErrorBannerView(state: state)
8691
}

iOSClient/GUI/Lucid Banner/HudBannerView.swift

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,32 @@ import LucidBanner
77

88
struct HudBannerView: View {
99
@ObservedObject var state: LucidBannerState
10+
@State private var displayedProgress: Double = 0
1011

1112
private let circleSize: CGFloat = 90
1213
private let lineWidth: CGFloat = 8
1314

1415
var body: some View {
15-
let progress = min(max(state.progress ?? 0, 0), 1) // clamp 0...1
16+
let rawProgress = state.progress ?? 0
17+
let clampedProgress = min(max(rawProgress, 0), 1)
18+
19+
let stage = state.stage?.lowercased()
20+
let isSuccess = (stage == "success")
21+
let isError = (stage == "error")
22+
23+
let visualProgress: Double = {
24+
if isSuccess || isError {
25+
return 1.0
26+
} else {
27+
return displayedProgress
28+
}
29+
}()
30+
31+
let strokeColor: Color = {
32+
if isSuccess { return .green }
33+
if isError { return .red }
34+
return .primary
35+
}()
1636

1737
containerView {
1838
VStack(spacing: 18) {
@@ -33,34 +53,89 @@ struct HudBannerView: View {
3353
.multilineTextAlignment(.center)
3454
}
3555

36-
// PROGRESS CIRCLE
56+
// PROGRESS CIRCLE + CENTER CONTENT
3757
ZStack {
58+
// Background ring
3859
Circle()
3960
.stroke(
4061
.gray.opacity(0.1),
4162
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
4263
)
4364
.frame(width: circleSize, height: circleSize)
4465

66+
// Foreground ring
4567
Circle()
46-
.trim(from: 0, to: progress)
68+
.trim(from: 0, to: visualProgress)
4769
.stroke(
48-
.primary,
70+
strokeColor,
4971
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
5072
)
5173
.rotationEffect(.degrees(-90))
5274
.frame(width: circleSize, height: circleSize)
53-
.animation(.easeInOut(duration: 0.20), value: progress)
5475

55-
Text("\(Int(progress * 100))%")
56-
.font(.headline.monospacedDigit())
57-
.foregroundStyle(.primary)
76+
// Center content:
77+
// - checkmark for success
78+
// - xmark for error
79+
// - percentage for normal progress
80+
Group {
81+
if isSuccess {
82+
Image(systemName: "checkmark")
83+
.font(.system(size: 34, weight: .bold))
84+
.foregroundStyle(strokeColor)
85+
} else if isError {
86+
Image(systemName: "xmark")
87+
.font(.system(size: 34, weight: .bold))
88+
.foregroundStyle(strokeColor)
89+
} else {
90+
Text("\(Int(visualProgress * 100))%")
91+
.font(.headline.monospacedDigit())
92+
.foregroundStyle(.primary)
93+
}
94+
}
5895
}
5996
.padding(.top, 4)
6097
}
6198
.padding(.horizontal, 22)
6299
.padding(.vertical, 24)
63100
}
101+
.onAppear {
102+
displayedProgress = clampedProgress
103+
}
104+
.onChange(of: state.progress) { _, newValue in
105+
guard let newValue else {
106+
withTransaction(Transaction(animation: nil)) {
107+
displayedProgress = 0
108+
}
109+
return
110+
}
111+
112+
let newClamped = min(max(newValue, 0), 1)
113+
114+
if newClamped < displayedProgress {
115+
let wasComplete = displayedProgress >= 0.95
116+
let isNewStart = newClamped <= 0.1
117+
118+
if wasComplete && isNewStart {
119+
withTransaction(Transaction(animation: nil)) {
120+
displayedProgress = newClamped
121+
}
122+
} else {
123+
return
124+
}
125+
} else {
126+
withAnimation(.easeInOut(duration: 0.20)) {
127+
displayedProgress = newClamped
128+
}
129+
}
130+
}
131+
.onChange(of: state.stage) { _, newStage in
132+
let lower = newStage?.lowercased()
133+
if lower == "success" || lower == "error" {
134+
withAnimation(.easeInOut(duration: 0.20)) {
135+
displayedProgress = 1.0
136+
}
137+
}
138+
}
64139
}
65140

66141
// MARK: - Container
@@ -89,8 +164,8 @@ func showHudBanner(
89164
scene: UIWindowScene?,
90165
title: String? = nil,
91166
subtitle: String? = nil,
92-
onTap: ((_ token: Int, _ stage: String?) -> Void)? = nil) -> Int {
93-
167+
onTap: ((_ token: Int, _ stage: String?) -> Void)? = nil
168+
) -> Int {
94169
LucidBanner.shared.show(
95170
scene: scene,
96171
title: title,
@@ -106,6 +181,30 @@ func showHudBanner(
106181
}
107182
}
108183

184+
@MainActor
185+
func completeHudBannerSuccess(
186+
token: Int
187+
) {
188+
LucidBanner.shared.update(
189+
stage: .success,
190+
autoDismissAfter: 2,
191+
for: token
192+
)
193+
}
194+
195+
@MainActor
196+
func completeHudBannerError(
197+
subtitle: String? = nil,
198+
token: Int
199+
) {
200+
LucidBanner.shared.update(
201+
subtitle: subtitle,
202+
stage: .error,
203+
autoDismissAfter: NCGlobal.shared.dismissAfterSecond,
204+
for: token
205+
)
206+
}
207+
109208
// MARK: - Preview
110209

111210
#Preview("HudBannerView") {
@@ -129,6 +228,9 @@ private struct HudBannerPreviewWrapper: View {
129228
try? await Task.sleep(nanoseconds: 45_000_000)
130229
state.progress = Double(i) / 100
131230
}
231+
232+
try? await Task.sleep(nanoseconds: 400_000_000)
233+
state.stage = "error"
132234
}
133235
}
134236
}

0 commit comments

Comments
 (0)