Skip to content

Commit 0521048

Browse files
committed
feat: max 3 latest release in updater
1 parent 87c611e commit 0521048

File tree

5 files changed

+100
-87
lines changed

5 files changed

+100
-87
lines changed

Micmute/Localizable.xcstrings

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -587,9 +587,6 @@
587587
}
588588
}
589589
}
590-
},
591-
"Pulled straight from GitHub" : {
592-
593590
},
594591
"Push to talk" : {
595592
"localizations" : {
@@ -752,12 +749,8 @@
752749
}
753750
}
754751
}
755-
},
756-
"Showing last \\(releases.count) releases" : {
757-
758752
},
759753
"Showing last %lld releases" : {
760-
"extractionState" : "stale",
761754
"localizations" : {
762755
"pl" : {
763756
"stringUnit" : {

Micmute/Utilities/SettingsUpdaterModel.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ final class SettingsUpdaterModel: ObservableObject {
3636
static let frequency = "com.rokartur.micmute.updater.frequency"
3737
static let nextCheck = "com.rokartur.micmute.updater.nextCheck"
3838
static let lastSeenRelease = "com.rokartur.micmute.updater.lastSeenRelease"
39-
static let lastFetch = "com.rokartur.micmute.updater.lastFetch"
39+
static let lastFetch = "com.rokartur.micmute.updater.lastFetch"
4040
}
4141

4242
private enum FetchReason {
@@ -93,9 +93,9 @@ final class SettingsUpdaterModel: ObservableObject {
9393
return
9494
}
9595

96-
progressMessage = "Preparing download…"
97-
progressValue = 0.1
98-
restartRequired = false
96+
progressMessage = "Preparing download…"
97+
progressValue = 0.1
98+
restartRequired = false
9999

100100
Task { [weak self] in
101101
guard let self else { return }
@@ -152,8 +152,8 @@ final class SettingsUpdaterModel: ObservableObject {
152152
let appURL = Bundle.main.bundleURL
153153
let configuration = NSWorkspace.OpenConfiguration()
154154
configuration.activates = true
155-
configuration.createsNewApplicationInstance = true
156-
configuration.promptsUserIfNeeded = false
155+
configuration.createsNewApplicationInstance = true
156+
configuration.promptsUserIfNeeded = false
157157

158158
restartRequired = false
159159
progressMessage = "Restarting Micmute…"
@@ -290,9 +290,11 @@ final class SettingsUpdaterModel: ObservableObject {
290290
}
291291

292292
private func handleFetchedReleases(_ releases: [GitHubReleaseDTO]) {
293+
// GitHub zwraca w kolejności od najnowszych – filtrujemy i przycinamy do 3
293294
let visible = releases.filter { !$0.draft && !$0.prerelease }
294-
self.releases = visible.map { SettingsRelease(dto: $0) }
295-
let latest = visible.first
295+
let top3 = Array(visible.prefix(3))
296+
self.releases = top3.map { SettingsRelease(dto: $0) }
297+
let latest = top3.first
296298
updateAvailable = determineUpdateAvailability(from: latest)
297299
updateAnnouncementState(using: latest)
298300

@@ -551,10 +553,10 @@ final class SettingsUpdaterModel: ObservableObject {
551553

552554
defer { try? fileManager.removeItem(at: scriptURL) }
553555

554-
let command = "/bin/sh \(shellEscapedPath(scriptURL.path))"
555-
let appleScript = """
556-
do shell script \"\(command)\" with administrator privileges
557-
"""
556+
let command = "/bin/sh \(shellEscapedPath(scriptURL.path))"
557+
let appleScript = """
558+
do shell script \"\(command)\" with administrator privileges
559+
"""
558560

559561
let process = Process()
560562
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")

Micmute/Views/MarkdownReleasesView.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ struct MarkdownReleasesView: View {
55
let releases: [SettingsRelease]
66

77
var body: some View {
8+
let releasesToShow = Array(releases.prefix(3))
9+
810
VStack(alignment: .leading, spacing: 12) {
9-
if releases.isEmpty {
11+
if releasesToShow.isEmpty {
1012
emptyState
1113
} else {
1214
ScrollView {
1315
LazyVStack(alignment: .leading, spacing: 16) {
14-
ForEach(releases, id: \.id) { release in
15-
releaseView(for: release)
16+
ForEach(releasesToShow, id: \.id) { release in
17+
releaseView(for: release, releasesToShow: releasesToShow)
1618
}
1719
}
1820
.padding(.vertical, 4)
1921
.padding(.horizontal, 1)
2022
}
2123

22-
Text("Showing last \\(releases.count) releases")
24+
Text("Showing last \(releasesToShow.count) releases")
2325
.font(.callout)
2426
.opacity(0.55)
2527
.padding(.horizontal, 2)
@@ -47,8 +49,8 @@ struct MarkdownReleasesView: View {
4749
}
4850

4951
@ViewBuilder
50-
private func releaseView(for release: SettingsRelease) -> some View {
51-
let isLatest = release.id == releases.first?.id
52+
private func releaseView(for release: SettingsRelease, releasesToShow: [SettingsRelease]) -> some View {
53+
let isLatest = release.id == releasesToShow.first?.id
5254

5355
VStack(alignment: .leading, spacing: 12) {
5456
header(for: release, isLatest: isLatest)

Micmute/Views/Settings/UpdatesView.swift

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,33 @@ struct UpdatesView: View {
8686
Text("v\(updatesModel.currentVersion)")
8787
.font(.system(size: 11, weight: .medium))
8888
.foregroundStyle(.primary)
89+
90+
if !updatesModel.releases.isEmpty {
91+
Button {
92+
activeSheet = .releaseNotes
93+
} label: {
94+
Text("Release notes")
95+
.font(.system(size: 11, weight: .medium))
96+
}
97+
.buttonStyle(.link)
98+
}
8999
}
90100
}
91101
}
92102

93103
HStack(spacing: 12) {
94104
if let descriptor = currentOperationStatusDescriptor {
95105
UpdateOperationStatusView(descriptor: descriptor)
96-
.frame(maxWidth: .infinity)
97106
} else {
98-
Button(action: updatesModel.checkForUpdates) {
99-
Label("Check for updates", systemImage: "arrow.clockwise")
100-
.font(.system(size: 13, weight: .semibold))
101-
.labelStyle(.titleAndIcon)
107+
// Pokaż ręczne sprawdzanie tylko, gdy auto-check = Never
108+
if updatesModel.frequency == .never {
109+
Button(action: updatesModel.checkForUpdates) {
110+
Label("Check for updates", systemImage: "arrow.clockwise")
111+
.font(.system(size: 13, weight: .semibold))
112+
.labelStyle(.titleAndIcon)
113+
}
114+
.buttonStyle(UpdatesPrimaryButtonStyle()) // domyślny akcentowy kolor
102115
}
103-
.buttonStyle(UpdatesPrimaryButtonStyle())
104116
}
105117

106118
if currentOperationStatusDescriptor == nil && updatesModel.updateAvailable {
@@ -109,27 +121,7 @@ struct UpdatesView: View {
109121
.font(.system(size: 13, weight: .semibold))
110122
.labelStyle(.titleAndIcon)
111123
}
112-
.buttonStyle(UpdatesSecondaryButtonStyle())
113-
}
114-
115-
// if updatesModel.announcementAvailable {
116-
// Button {
117-
// activeSheet = .announcement
118-
// } label: {
119-
// Label("View announcement", systemImage: "sparkles")
120-
// .font(.system(size: 12, weight: .medium))
121-
// }
122-
// .buttonStyle(.link)
123-
// }
124-
125-
if !updatesModel.releases.isEmpty {
126-
Button {
127-
activeSheet = .releaseNotes
128-
} label: {
129-
Label("Release notes", systemImage: "doc.richtext")
130-
.font(.system(size: 12, weight: .medium))
131-
}
132-
.buttonStyle(.link)
124+
.buttonStyle(UpdatesPrimaryButtonStyle(color: .orange))
133125
}
134126
}
135127

@@ -153,11 +145,10 @@ struct UpdatesView: View {
153145
Spacer()
154146
Button(action: updatesModel.restartApplication) {
155147
Label("Restart", systemImage: "power")
156-
.font(.system(size: 12, weight: .semibold))
157-
.padding(.vertical, 4)
158-
.padding(.horizontal, 10)
148+
.font(.system(size: 13, weight: .semibold))
149+
.labelStyle(.titleAndIcon)
159150
}
160-
.buttonStyle(.borderedProminent)
151+
.buttonStyle(UpdatesPrimaryButtonStyle(color: .blue))
161152
}
162153
}
163154
.padding(14)
@@ -459,16 +450,18 @@ private struct ReleaseNotesSheet: View {
459450
let onDismiss: () -> Void
460451

461452
var body: some View {
453+
let releasesToShow = Array(releases.prefix(3))
454+
462455
VStack(spacing: 16) {
463-
header
456+
header(count: releasesToShow.count)
464457

465-
if releases.isEmpty {
458+
if releasesToShow.isEmpty {
466459
emptyState
467460
} else {
468461
ScrollView {
469462
LazyVStack(alignment: .leading, spacing: 18) {
470-
ForEach(releases, id: \.id) { release in
471-
releaseCard(for: release)
463+
ForEach(releasesToShow, id: \.id) { release in
464+
releaseCard(for: release, releasesToShow: releasesToShow)
472465
.padding(.horizontal, 2)
473466
}
474467
}
@@ -482,7 +475,7 @@ private struct ReleaseNotesSheet: View {
482475
.frame(maxWidth: .infinity, alignment: .leading)
483476
}
484477

485-
private var header: some View {
478+
private func header(count: Int) -> some View {
486479
HStack(alignment: .center, spacing: 10) {
487480
Image(systemName: "doc.richtext")
488481
.font(.system(size: 20, weight: .semibold))
@@ -491,7 +484,7 @@ private struct ReleaseNotesSheet: View {
491484
VStack(alignment: .leading, spacing: 2) {
492485
Text("Recent release notes")
493486
.font(.headline)
494-
Text("Pulled straight from GitHub")
487+
Text("Showing last \(count) releases")
495488
.font(.subheadline)
496489
.foregroundStyle(.secondary)
497490
}
@@ -517,9 +510,9 @@ private struct ReleaseNotesSheet: View {
517510
}
518511

519512
@ViewBuilder
520-
private func releaseCard(for release: SettingsRelease) -> some View {
513+
private func releaseCard(for release: SettingsRelease, releasesToShow: [SettingsRelease]) -> some View {
521514
let title = release.tagName.isEmpty ? release.displayTitle : release.tagName
522-
let isLatest = release.id == releases.first?.id
515+
let isLatest = release.id == releasesToShow.first?.id
523516

524517
VStack(alignment: .leading, spacing: 10) {
525518
HStack(alignment: .center, spacing: 8) {
@@ -564,39 +557,66 @@ private struct ReleaseNotesSheet: View {
564557
private struct UpdateOperationStatusView: View {
565558
let descriptor: UpdateOperationStatusDescriptor
566559

560+
// Nie skalujemy widoku, gdy pokazujemy spinner — to eliminuje błąd NSProgressIndicator.
561+
private var scale: CGFloat {
562+
if descriptor.showsSpinner { return 1.0 }
563+
if let progress = descriptor.progress {
564+
return 1.0 + CGFloat(min(max(progress, 0), 1)) * 0.04
565+
}
566+
return 1.0
567+
}
568+
567569
var body: some View {
568-
HStack(alignment: .center, spacing: 12) {
570+
let shape = RoundedRectangle(cornerRadius: 12, style: .continuous)
571+
let base = descriptor.iconColor
572+
573+
HStack(alignment: .center, spacing: 10) {
569574
if descriptor.showsSpinner {
570575
ProgressView()
571576
.controlSize(.small)
577+
.tint(.white)
578+
.frame(width: 16, height: 16)
572579
} else if let icon = descriptor.iconName {
573580
Image(systemName: icon)
574-
.font(.system(size: 16, weight: .semibold))
575-
.foregroundStyle(descriptor.iconColor)
576-
.frame(width: 18)
581+
.font(.system(size: 13, weight: .semibold))
582+
.foregroundStyle(Color.white)
583+
.frame(width: 16, height: 16)
577584
}
578585

579586
Text(descriptor.title)
580587
.font(.system(size: 13, weight: .semibold))
581-
.foregroundStyle(descriptor.textColor)
582-
.frame(maxWidth: .infinity, alignment: .leading)
588+
.foregroundStyle(Color.white)
583589

584590
if let progress = descriptor.progress {
585591
Text(progressFormatted(progress))
586592
.font(.system(size: 12, weight: .semibold))
587-
.foregroundStyle(descriptor.iconColor)
593+
.foregroundStyle(Color.white.opacity(0.95))
588594
}
589595
}
590-
.padding(.vertical, 10)
591-
.padding(.horizontal, 12)
596+
.fixedSize(horizontal: true, vertical: false)
597+
.padding(.vertical, 8)
598+
.padding(.horizontal, 14)
592599
.background(
593-
RoundedRectangle(cornerRadius: 12, style: .continuous)
594-
.fill(descriptor.background)
600+
ZStack {
601+
shape.fill(
602+
LinearGradient(
603+
colors: [base.opacity(0.98), base.opacity(0.78)],
604+
startPoint: .topLeading,
605+
endPoint: .bottomTrailing
606+
)
607+
)
608+
shape.stroke(Color.white.opacity(0.18), lineWidth: 0.6)
609+
.blur(radius: 0.4)
610+
.clipShape(shape)
611+
}
595612
)
596613
.overlay(
597-
RoundedRectangle(cornerRadius: 12, style: .continuous)
598-
.stroke(descriptor.border, lineWidth: 1)
614+
shape.stroke(base.opacity(0.55), lineWidth: 1)
599615
)
616+
.shadow(color: base.opacity(0.45), radius: 6, y: 4)
617+
// Animację skali włączamy tylko, gdy spinner nie jest aktywny.
618+
.scaleEffect(scale)
619+
.animation(descriptor.showsSpinner ? nil : .easeInOut(duration: 0.2), value: scale)
600620
}
601621

602622
private func progressFormatted(_ value: Double) -> String {
@@ -662,7 +682,7 @@ private struct FrequencyOptionButtonStyle: ButtonStyle {
662682
.background(
663683
ZStack {
664684
RoundedRectangle(cornerRadius: 12, style: .continuous)
665-
.fill(.ultraThinMaterial)
685+
.fill(Color.white.opacity(0.03))
666686

667687
if isHovered {
668688
RoundedRectangle(cornerRadius: 12, style: .continuous)
@@ -680,26 +700,22 @@ private struct FrequencyOptionButtonStyle: ButtonStyle {
680700
}
681701
}
682702
)
683-
.overlay(
684-
RoundedRectangle(cornerRadius: 12, style: .continuous)
685-
.stroke(Color.white.opacity(isSelected ? 0.3 : 0.12), lineWidth: isSelected ? 1.1 : 1)
686-
)
687703
.scaleEffect(configuration.isPressed ? 0.97 : 1)
688704
.animation(.easeInOut(duration: 0.12), value: configuration.isPressed)
689705
}
690706
}
691707

692708
private struct UpdatesPrimaryButtonStyle: ButtonStyle {
709+
var color: Color = .accentColor
693710
@Environment(\.isEnabled) private var isEnabled
694711

695712
func makeBody(configuration: Configuration) -> some View {
696713
let shape = RoundedRectangle(cornerRadius: 12, style: .continuous)
697-
let accent = Color.accentColor
698714
let activeOpacity: Double = configuration.isPressed ? 0.85 : 1.0
699715
let inactiveOpacity: Double = configuration.isPressed ? 0.65 : 0.78
700716
let gradientColors: [Color] = isEnabled
701-
? [accent.opacity(activeOpacity), accent.opacity(inactiveOpacity)]
702-
: [accent.opacity(0.42), accent.opacity(0.32)]
717+
? [color.opacity(activeOpacity), color.opacity(inactiveOpacity)]
718+
: [color.opacity(0.42), color.opacity(0.32)]
703719

704720
return configuration.label
705721
.foregroundColor(isEnabled ? Color.white : Color.white.opacity(0.7))
@@ -717,9 +733,9 @@ private struct UpdatesPrimaryButtonStyle: ButtonStyle {
717733
}
718734
)
719735
.overlay(
720-
shape.stroke(accent.opacity(isEnabled ? 0.55 : 0.32), lineWidth: 1)
736+
shape.stroke(color.opacity(isEnabled ? 0.55 : 0.32), lineWidth: 1)
721737
)
722-
.shadow(color: accent.opacity(isEnabled ? 0.45 : 0.2), radius: configuration.isPressed ? 4 : 8, y: configuration.isPressed ? 2 : 6)
738+
.shadow(color: color.opacity(isEnabled ? 0.45 : 0.2), radius: configuration.isPressed ? 4 : 8, y: configuration.isPressed ? 2 : 6)
723739
.scaleEffect(configuration.isPressed ? 0.97 : 1)
724740
.animation(.easeInOut(duration: 0.12), value: configuration.isPressed)
725741
}

0 commit comments

Comments
 (0)