Merge pull request #25 from Ebullioscopic/feature/hud

Segmented HUD Inline and Default - for more Apple like feel
This commit is contained in:
Hariharan Mudaliar 2025-09-10 23:05:07 +05:30 • committed by GitHub
commit 9343add5be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 20 deletions

4
.gitignore vendored
View file

@ -66,4 +66,6 @@ rough_note.md
Solid
SettingsView.swift.md
logic.md
TheBoringWorker-HUD
TheBoringWorker-HUD
boringnotch + meteo + airpods + traduzione modificabile
segmentedhud.swift.ref

View file

@ -378,6 +378,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
coordinator.setupWorkersNotificationObservers()
// Migrate legacy progress bar settings
Defaults.Keys.migrateProgressBarStyle()
// Setup SystemHUD Manager
SystemHUDManager.shared.setup(coordinator: coordinator)

View file

@ -1086,6 +1086,9 @@
},
"Seconds" : {
},
"Segmented" : {
},
"Select a color to view details" : {

View file

@ -98,23 +98,34 @@ struct DraggableProgressBar: View {
var body: some View {
VStack {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule()
.fill(.tertiary)
Group {
if Defaults[.enableGradient] {
Group {
if Defaults[.progressBarStyle] == .segmented {
// Segmented progress bar - completely different layout
SegmentedProgressContent(value: value, geometry: geo)
} else {
// Traditional capsule-based progress bar
ZStack(alignment: .leading) {
Capsule()
.fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading))
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
} else {
Capsule()
.fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white)
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
.fill(.tertiary)
Group {
switch Defaults[.progressBarStyle] {
case .gradient:
Capsule()
.fill(LinearGradient(colors: Defaults[.systemEventIndicatorUseAccent] ? [Defaults[.accentColor], Defaults[.accentColor].ensureMinimumBrightness(factor: 0.2)] : [.white, .white.opacity(0.2)], startPoint: .trailing, endPoint: .leading))
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
case .hierarchical:
Capsule()
.fill(Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor] : .white)
.frame(width: max(0, min(geo.size.width * value, geo.size.width)))
.shadow(color: Defaults[.systemEventIndicatorShadow] ? Defaults[.systemEventIndicatorUseAccent] ? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7) : .white : .clear, radius: 8, x: 3)
case .segmented:
EmptyView() // This case won't be reached due to the outer if condition
}
}
.opacity(value.isZero ? 0 : 1)
}
}
.opacity(value.isZero ? 0 : 1)
}
.gesture(
DragGesture(minimumDistance: 0)
@ -142,3 +153,105 @@ struct DraggableProgressBar: View {
value = max(0, min(newValue, 1))
}
}
struct SegmentedProgressContent: View {
let value: CGFloat
let geometry: GeometryProxy
private let segmentCount = 16
@State private var glowIndex: Int? = nil
@State private var lastValue: CGFloat = 0
var body: some View {
let spacing: CGFloat = 1.5
let computed = (geometry.size.width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount)
let barWidth = max(3.0, min(6.0, computed))
let activeCount = Int(round(value * CGFloat(segmentCount)))
HStack(spacing: spacing) {
ForEach(0..<segmentCount, id: \.self) { index in
RoundedRectangle(cornerRadius: 2)
.fill(segmentColor(isActive: index < activeCount))
.shadow(
color: Defaults[.systemEventIndicatorShadow]
? glowShadowColor(for: index < activeCount, index: index)
: .clear,
radius: Defaults[.systemEventIndicatorShadow]
? (glowIndex == index ? 12 : (index < activeCount ? 4 : 0))
: 0,
x: 0, y: 0
)
.frame(width: barWidth)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: activeCount)
.scaleEffect(glowIndex == index ? 1.15 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.6), value: glowIndex == index)
}
}
.frame(maxWidth: .infinity, alignment: .center)
.opacity(value.isZero ? 0 : 1)
.onChange(of: value) { old, newVal in
handleGlow(oldValue: old, newValue: newVal)
}
}
private func segmentColor(isActive: Bool) -> Color {
if isActive {
if Defaults[.systemEventIndicatorUseAccent] {
return Defaults[.accentColor]
}
return .white
} else {
return .white.opacity(0.15)
}
}
private func glowShadowColor(for isActive: Bool, index: Int) -> Color {
if isActive {
if let glowIndex = glowIndex, index == glowIndex {
return Defaults[.systemEventIndicatorUseAccent]
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.8)
: .white
} else {
return Defaults[.systemEventIndicatorUseAccent]
? Defaults[.accentColor].ensureMinimumBrightness(factor: 0.7)
: .white
}
}
return .clear
}
private func handleGlow(oldValue: CGFloat, newValue: CGFloat) {
defer { lastValue = newValue }
let oldIndex = Int(round(oldValue * CGFloat(segmentCount)))
let newIndex = Int(round(newValue * CGFloat(segmentCount)))
guard oldIndex != newIndex else { return }
if newIndex > oldIndex {
animateWave(from: oldIndex, to: newIndex, step: 1)
} else {
animateWave(from: oldIndex - 1, to: newIndex - 1, step: -1)
}
}
private func animateWave(from start: Int, to end: Int, step: Int) {
let clampedStart = max(0, min(segmentCount - 1, start))
let clampedEnd = max(-1, min(segmentCount - 1, end))
let range = stride(from: clampedStart, through: clampedEnd, by: step)
var delay: Double = 0
for i in range {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(.spring(response: 0.2, dampingFraction: 0.7)) {
glowIndex = i
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay + 0.2) {
if glowIndex == i {
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
glowIndex = nil
}
}
}
delay += 0.015
}
}
}

View file

@ -419,7 +419,7 @@ struct Downloads: View {
struct HUD: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@Default(.inlineHUD) var inlineHUD
@Default(.enableGradient) var enableGradient
@Default(.progressBarStyle) var progressBarStyle
@Default(.enableSystemHUD) var enableSystemHUD
@Default(.enableVolumeHUD) var enableVolumeHUD
@Default(.enableBrightnessHUD) var enableBrightnessHUD
@ -491,15 +491,17 @@ struct HUD: View {
if Defaults[.inlineHUD] {
withAnimation {
Defaults[.systemEventIndicatorShadow] = false
Defaults[.enableGradient] = false
Defaults[.progressBarStyle] = .hierarchical
}
}
}
Picker("Progressbar style", selection: $enableGradient) {
Picker("Progressbar style", selection: $progressBarStyle) {
Text("Hierarchical")
.tag(false)
.tag(ProgressBarStyle.hierarchical)
Text("Gradient")
.tag(true)
.tag(ProgressBarStyle.gradient)
Text("Segmented")
.tag(ProgressBarStyle.segmented)
}
Defaults.Toggle("Enable glowing effect", key: .systemEventIndicatorShadow)
Defaults.Toggle("Use accent color", key: .systemEventIndicatorUseAccent)

View file

@ -97,6 +97,14 @@ enum SneakPeekStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
var id: String { self.rawValue }
}
enum ProgressBarStyle: String, CaseIterable, Identifiable, Defaults.Serializable {
case hierarchical = "Hierarchical"
case gradient = "Gradient"
case segmented = "Segmented"
var id: String { self.rawValue }
}
extension Defaults.Keys {
// MARK: General
static let menubarIcon = Key<Bool>("menubarIcon", default: true)
@ -171,6 +179,8 @@ extension Defaults.Keys {
// MARK: HUD
static let inlineHUD = Key<Bool>("inlineHUD", default: false)
static let progressBarStyle = Key<ProgressBarStyle>("progressBarStyle", default: .hierarchical)
// Legacy support - keeping for backward compatibility
static let enableGradient = Key<Bool>("enableGradient", default: false)
static let systemEventIndicatorShadow = Key<Bool>("systemEventIndicatorShadow", default: false)
static let systemEventIndicatorUseAccent = Key<Bool>("systemEventIndicatorUseAccent", default: false)
@ -236,4 +246,15 @@ extension Defaults.Keys {
return .nowPlaying
}
}
// Migration helper to convert from legacy enableGradient Boolean to new ProgressBarStyle enum
static func migrateProgressBarStyle() {
// Check if migration is needed by seeing if the old Boolean was set to gradient
let wasGradientEnabled = Defaults[.enableGradient]
// Only migrate if we're still using the default hierarchical value but gradient was enabled
if wasGradientEnabled && Defaults[.progressBarStyle] == .hierarchical {
Defaults[.progressBarStyle] = .gradient
}
}
}