summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/Update/UpdatePill.swift
blob: 3b48ac2186c7408fd85c7eab8b07022bd8207883 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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)
                    .frame(width: textWidth)
            }
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(
                Capsule()
                    .fill(model.backgroundColor)
            )
            .foregroundColor(model.foregroundColor)
            .contentShape(Capsule())
        }
        .buttonStyle(.plain)
        .help(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
    }
}