summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/Update/UpdatePill.swift
diff options
context:
space:
mode:
Diffstat (limited to 'macos/Sources/Features/Update/UpdatePill.swift')
-rw-r--r--macos/Sources/Features/Update/UpdatePill.swift79
1 files changed, 79 insertions, 0 deletions
diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift
new file mode 100644
index 000000000..ff4af97dd
--- /dev/null
+++ b/macos/Sources/Features/Update/UpdatePill.swift
@@ -0,0 +1,79 @@
+import SwiftUI
+
+/// A pill-shaped button that displays update status and provides access to update actions.
+struct UpdatePill: View {
+ /// The update view model that provides the current state and information
+ @ObservedObject var model: UpdateViewModel
+
+ /// Whether the update popover is currently visible
+ @State private var showPopover = false
+
+ /// Task for auto-dismissing the "No Updates" state
+ @State private var resetTask: Task<Void, Never>?
+
+ /// The font used for the pill text
+ private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium)
+
+ var body: some View {
+ if !model.state.isIdle {
+ pillButton
+ .popover(isPresented: $showPopover, arrowEdge: .bottom) {
+ UpdatePopoverView(model: model)
+ }
+ .transition(.opacity.combined(with: .scale(scale: 0.95)))
+ .onChange(of: model.state) { newState in
+ resetTask?.cancel()
+ if case .notFound = newState {
+ resetTask = Task { [weak model] in
+ try? await Task.sleep(for: .seconds(5))
+ guard !Task.isCancelled, case .notFound? = model?.state else { return }
+ model?.state = .idle
+ }
+ } else {
+ resetTask = nil
+ }
+ }
+ }
+ }
+
+ /// The pill-shaped button view that displays the update badge and text
+ @ViewBuilder
+ private var pillButton: some View {
+ Button(action: {
+ if case .notFound = model.state {
+ model.state = .idle
+ } else {
+ showPopover.toggle()
+ }
+ }) {
+ HStack(spacing: 6) {
+ UpdateBadge(model: model)
+ .frame(width: 14, height: 14)
+
+ Text(model.text)
+ .font(Font(textFont))
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .frame(width: textWidth)
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(model.backgroundColor)
+ )
+ .foregroundColor(model.foregroundColor)
+ .contentShape(Capsule())
+ }
+ .buttonStyle(.plain)
+ .help(model.text)
+ .accessibilityLabel(model.text)
+ }
+
+ /// Calculated width for the text to prevent resizing during progress updates
+ private var textWidth: CGFloat? {
+ let attributes: [NSAttributedString.Key: Any] = [.font: textFont]
+ let size = (model.maxWidthText as NSString).size(withAttributes: attributes)
+ return size.width
+ }
+}