summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/Update/UpdateViewModel.swift
blob: 57e438bcdd250587a8ee943db6304e0658a12992 (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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import Foundation
import SwiftUI
import Sparkle

class UpdateViewModel: ObservableObject {
    @Published var state: UpdateState = .idle
    
    /// The text to display for the current update state.
    /// Returns an empty string for idle state, progress percentages for downloading/extracting,
    /// or descriptive text for other states.
    var text: String {
        switch state {
        case .idle:
            return ""
        case .permissionRequest:
            return "Update Permission"
        case .checking:
            return "Checking for Updates…"
        case .updateAvailable(let update):
            return "Update Available: \(update.appcastItem.displayVersionString)"
        case .downloading(let download):
            if let expectedLength = download.expectedLength, expectedLength > 0 {
                let progress = Double(download.progress) / Double(expectedLength)
                return String(format: "Downloading: %.0f%%", progress * 100)
            }
            return "Downloading…"
        case .extracting(let extracting):
            return String(format: "Preparing: %.0f%%", extracting.progress * 100)
        case .readyToInstall:
            return "Install Update"
        case .installing:
            return "Installing…"
        case .notFound:
            return "No Updates Available"
        case .error(let err):
            return err.error.localizedDescription
        }
    }
    
    /// The SF Symbol icon name for the current update state.
    /// Returns nil for idle, downloading, and extracting states.
    var iconName: String? {
        switch state {
        case .idle:
            return nil
        case .permissionRequest:
            return "questionmark.circle"
        case .checking:
            return "arrow.triangle.2.circlepath"
        case .updateAvailable:
            return "arrow.down.circle.fill"
        case .downloading, .extracting:
            return nil
        case .readyToInstall:
            return "checkmark.circle.fill"
        case .installing:
            return "gear"
        case .notFound:
            return "info.circle"
        case .error:
            return "exclamationmark.triangle.fill"
        }
    }
    
    /// The color to apply to the icon for the current update state.
    var iconColor: Color {
        switch state {
        case .idle:
            return .secondary
        case .permissionRequest, .checking:
            return .secondary
        case .updateAvailable, .readyToInstall:
            return .accentColor
        case .downloading, .extracting, .installing:
            return .secondary
        case .notFound:
            return .secondary
        case .error:
            return .orange
        }
    }
    
    /// The background color for the update pill.
    var backgroundColor: Color {
        switch state {
        case .updateAvailable:
            return .accentColor
        case .readyToInstall:
            return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen)
        case .error:
            return .orange.opacity(0.2)
        default:
            return Color(nsColor: .controlBackgroundColor)
        }
    }
    
    /// The foreground (text) color for the update pill.
    var foregroundColor: Color {
        switch state {
        case .updateAvailable, .readyToInstall:
            return .white
        case .error:
            return .orange
        default:
            return .primary
        }
    }
}

enum UpdateState {
    case idle
    case permissionRequest(PermissionRequest)
    case checking(Checking)
    case updateAvailable(UpdateAvailable)
    case notFound
    case error(Error)
    case downloading(Downloading)
    case extracting(Extracting)
    case readyToInstall(ReadyToInstall)
    case installing
    
    var isIdle: Bool {
        if case .idle = self { return true }
        return false
    }
    
    struct PermissionRequest {
        let request: SPUUpdatePermissionRequest
        let reply: @Sendable (SUUpdatePermissionResponse) -> Void
    }
    
    struct Checking {
        let cancel: () -> Void
    }
    
    struct UpdateAvailable {
        let appcastItem: SUAppcastItem
        let reply: @Sendable (SPUUserUpdateChoice) -> Void
    }
    
    struct Error {
        let error: any Swift.Error
        let retry: () -> Void
    }
    
    struct Downloading {
        let cancel: () -> Void
        let expectedLength: UInt64?
        let progress: UInt64
    }
    
    struct Extracting {
        let progress: Double
    }
    
    struct ReadyToInstall {
        let reply: @Sendable (SPUUserUpdateChoice) -> Void
    }
}