From a135c8a59078246780d01d6860c90607ad8c168f Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 26 Oct 2025 12:50:54 +0530 Subject: [PATCH] Added text based huds progress --- DynamicIsland/Localizable.xcstrings | 22 ++- .../components/ColorCodedProgressBar.swift | 186 ++++++++--------- .../Live activities/InlineHUD.swift | 59 +++--- .../SystemEventIndicatorModifier.swift | 187 +++++++++++++++--- .../components/Settings/SettingsView.swift | 19 +- DynamicIsland/models/Constants.swift | 1 + 6 files changed, 317 insertions(+), 157 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index a9f0a7d..8aa023a 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -4609,8 +4609,9 @@ "Color Picker Panel:" : { }, - "Color-coded displays are only available in Gradient mode. Switch Progressbar style to Gradient to enable this feature." : { - + "Color-coded fills and smooth gradients are unavailable in Segmented mode. Switch to Hierarchical or Gradient to adjust these options." : { + "comment" : "An explanatory text that appears when the user tries to enable color-coded color gradients in Segmented progress bars. It instructs the user to switch to Hierarchical or Gradient progress bars to enable these features.", + "isCommentAutoGenerated" : true }, "Color-Coded Progress Bars" : { "comment" : "A section header that lists the options for color-coded progress bars.", @@ -5800,8 +5801,8 @@ } } }, - "Discrete: Battery/Volume uses 3 distinct colors only\nBattery: Green (high), Yellow (medium), Red (low)\nVolume: Green (quiet), Yellow (medium), Red (loud)" : { - "comment" : "A description of the \"Discrete\" color-coded progress bar style, including the colors used for battery and volume levels.", + "Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%)." : { + "comment" : "A description of how the color-coded progress bars work when not using smooth transitions.", "isCommentAutoGenerated" : true }, "Disk Read" : { @@ -10761,6 +10762,7 @@ } }, "Mic %@" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -10860,6 +10862,14 @@ } } }, + "Mic muted" : { + "comment" : "A description of the state when the microphone is muted.", + "isCommentAutoGenerated" : true + }, + "Mic unmuted" : { + "comment" : "A description of the state when the microphone is unmuted.", + "isCommentAutoGenerated" : true + }, "Microphone" : { "comment" : "A label displayed next to the microphone icon in the privacy indicator detail view.", "isCommentAutoGenerated" : true @@ -15307,8 +15317,8 @@ } } }, - "Smooth: Battery/Volume gradually transitions through all colors\nBattery: Green (high) → Yellow-Green → Yellow → Orange → Red (low)\nVolume: Green (quiet) → Yellow → Orange → Red (loud)" : { - "comment" : "A detailed description of the \"Smooth\" color-coded gradient display feature.", + "Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill." : { + "comment" : "A description of how the color-coding feature works.", "isCommentAutoGenerated" : true }, "Sneak Peek shows the media title and artist under the notch for a few seconds." : { diff --git a/DynamicIsland/components/ColorCodedProgressBar.swift b/DynamicIsland/components/ColorCodedProgressBar.swift index 6698aae..21c608e 100644 --- a/DynamicIsland/components/ColorCodedProgressBar.swift +++ b/DynamicIsland/components/ColorCodedProgressBar.swift @@ -9,16 +9,31 @@ import SwiftUI import Defaults +enum ColorCodingMode { + case volume + case battery + + func intensity(for value: CGFloat) -> CGFloat { + let clamped = min(max(value, 0), 1) + switch self { + case .volume: + return clamped + case .battery: + return 1 - clamped + } + } +} + struct ColorCodedProgressBar: View { let value: CGFloat // 0.0 to 1.0 - let reversed: Bool // If true: green at low, red at high (for volume) + let mode: ColorCodingMode let width: CGFloat let height: CGFloat let smoothGradient: Bool // If true: smooth gradient, if false: discrete colors - init(value: CGFloat, reversed: Bool = false, width: CGFloat = 100, height: CGFloat = 4, smoothGradient: Bool = true) { + init(value: CGFloat, mode: ColorCodingMode = .battery, width: CGFloat = 100, height: CGFloat = 4, smoothGradient: Bool = true) { self.value = min(max(value, 0), 1) // Clamp between 0 and 1 - self.reversed = reversed + self.mode = mode self.width = width self.height = height self.smoothGradient = smoothGradient @@ -34,100 +49,19 @@ struct ColorCodedProgressBar: View { // Filled track with color gradient or solid color RoundedRectangle(cornerRadius: height / 2) - .fill(smoothGradient ? - AnyShapeStyle(LinearGradient( - gradient: gradientForValue, - startPoint: .leading, - endPoint: .trailing - )) : - AnyShapeStyle(colorForPercentage(value, reversed: reversed)) - ) + .fill(fillStyle) .frame(width: width * value, height: height) } } .frame(width: width, height: height) } - /// Generates a gradient based on the current value - private var gradientForValue: Gradient { - let color = colorForPercentage(value, reversed: reversed) - - // Create a gradient that transitions smoothly - let startColor = colorForPercentage(max(value - 0.1, 0), reversed: reversed) - let endColor = color - - return Gradient(colors: [startColor, endColor]) - } - - /// Calculates discrete color for a given percentage (no smooth transitions) - /// - Parameters: - /// - percentage: Value from 0.0 to 1.0 - /// - reversed: If true, reverses the color scale (green at low, red at high) - /// - Returns: Discrete color (red, yellow, or green only) - private func discreteColorForPercentage(_ percentage: CGFloat, reversed: Bool) -> Color { - let normalized = reversed ? (1.0 - percentage) : percentage - - switch normalized { - case 0.0..<0.33: - // Red zone (critical/low) - return Color(red: 1.0, green: 0.23, blue: 0.19) - case 0.33..<0.67: - // Yellow zone (medium) - return Color(red: 1.0, green: 0.8, blue: 0.0) - default: - // Green zone (healthy/high) - return Color(red: 0.2, green: 0.78, blue: 0.35) - } - } - - /// Calculates color for a given percentage with smooth transitions - /// - Parameters: - /// - percentage: Value from 0.0 to 1.0 - /// - reversed: If true, reverses the color scale (green at low, red at high) - /// - Returns: Color with smooth gradient transitions - private func colorForPercentage(_ percentage: CGFloat, reversed: Bool) -> Color { - let normalized = reversed ? (1.0 - percentage) : percentage - - switch normalized { - case 0.0..<0.2: - // Pure Red zone (critical/low) - return Color(red: 1.0, green: 0.23, blue: 0.19) - - case 0.2..<0.35: - // Red → Orange transition - let t = (normalized - 0.2) / 0.15 - let red: CGFloat = 1.0 - let green: CGFloat = 0.23 + (0.37 * t) // 0.23 → 0.6 - let blue: CGFloat = 0.19 - return Color(red: red, green: green, blue: blue) - - case 0.35..<0.5: - // Orange → Yellow transition - let t = (normalized - 0.35) / 0.15 - let red: CGFloat = 1.0 - let green: CGFloat = 0.6 + (0.4 * t) // 0.6 → 1.0 - let blue: CGFloat = 0.19 - return Color(red: red, green: green, blue: blue) - - case 0.5..<0.65: - // Yellow → Yellow-Green transition - let t = (normalized - 0.5) / 0.15 - let red: CGFloat = 1.0 - (0.4 * t) // 1.0 → 0.6 - let green: CGFloat = 1.0 - let blue: CGFloat = 0.19 + (0.4 * t) // 0.19 → 0.59 - return Color(red: red, green: green, blue: blue) - - case 0.65..<0.8: - // Yellow-Green → Green transition - let t = (normalized - 0.65) / 0.15 - let red: CGFloat = 0.6 - (0.4 * t) // 0.6 → 0.2 - let green: CGFloat = 1.0 - (0.22 * t) // 1.0 → 0.78 - let blue: CGFloat = 0.59 - (0.24 * t) // 0.59 → 0.35 - return Color(red: red, green: green, blue: blue) - - default: - // Pure Green zone (healthy/high) - return Color(red: 0.2, green: 0.78, blue: 0.35) + private var fillStyle: AnyShapeStyle { + let intensity = mode.intensity(for: value) + if smoothGradient { + return AnyShapeStyle(ColorCodedPalette.gradient(for: intensity)) + } else { + return AnyShapeStyle(ColorCodedPalette.discreteColor(for: intensity)) } } } @@ -137,12 +71,78 @@ struct ColorCodedProgressBar: View { extension ColorCodedProgressBar { /// Creates a battery-style progress bar (green at high, red at low) static func battery(value: CGFloat, width: CGFloat = 100, height: CGFloat = 4, smoothGradient: Bool = true) -> some View { - ColorCodedProgressBar(value: value, reversed: false, width: width, height: height, smoothGradient: smoothGradient) + ColorCodedProgressBar(value: value, mode: .battery, width: width, height: height, smoothGradient: smoothGradient) } /// Creates a volume-style progress bar (red at high, green at low) static func volume(value: CGFloat, width: CGFloat = 100, height: CGFloat = 4, smoothGradient: Bool = true) -> some View { - ColorCodedProgressBar(value: value, reversed: true, width: width, height: height, smoothGradient: smoothGradient) + ColorCodedProgressBar(value: value, mode: .volume, width: width, height: height, smoothGradient: smoothGradient) + } + + /// Shared helper for other progress bar implementations that need the same palette. + static func shapeStyle(for value: CGFloat, mode: ColorCodingMode, smoothGradient: Bool) -> AnyShapeStyle { + let intensity = mode.intensity(for: min(max(value, 0), 1)) + if smoothGradient { + return AnyShapeStyle(ColorCodedPalette.gradient(for: intensity)) + } + return AnyShapeStyle(ColorCodedPalette.discreteColor(for: intensity)) + } +} + +// MARK: - Palette Helpers + +private enum ColorCodedPalette { + private static let greenComponents: (CGFloat, CGFloat, CGFloat) = (0.2, 0.78, 0.35) + private static let yellowComponents: (CGFloat, CGFloat, CGFloat) = (0.93, 0.75, 0.2) + private static let redComponents: (CGFloat, CGFloat, CGFloat) = (0.95, 0.36, 0.3) + + static func discreteColor(for intensity: CGFloat) -> Color { + let clamped = min(max(intensity, 0), 1) + switch clamped { + case ..<0.6: + return color(from: greenComponents) + case ..<0.85: + return color(from: yellowComponents) + default: + return color(from: redComponents) + } + } + + static func gradient(for intensity: CGFloat) -> LinearGradient { + let endColor = smoothColor(for: intensity) + let startColor = smoothColor(for: max(intensity - 0.15, 0)) + return LinearGradient(colors: [startColor, endColor], startPoint: .leading, endPoint: .trailing) + } + + private static func smoothColor(for intensity: CGFloat) -> Color { + let clamped = min(max(intensity, 0), 1) + switch clamped { + case ..<0.6: + // Stay green within the comfort zone + return color(from: greenComponents) + case ..<0.85: + let t = (clamped - 0.6) / 0.25 + return blend(from: greenComponents, to: yellowComponents, fraction: t) + default: + let t = (clamped - 0.85) / 0.15 + return blend(from: yellowComponents, to: redComponents, fraction: t) + } + } + + private static func blend(from start: (CGFloat, CGFloat, CGFloat), to end: (CGFloat, CGFloat, CGFloat), fraction: CGFloat) -> Color { + let t = min(max(fraction, 0), 1) + let red = lerp(start.0, end.0, t) + let green = lerp(start.1, end.1, t) + let blue = lerp(start.2, end.2, t) + return color(from: (red, green, blue)) + } + + private static func lerp(_ a: CGFloat, _ b: CGFloat, _ t: CGFloat) -> CGFloat { + a + (b - a) * t + } + + private static func color(from components: (CGFloat, CGFloat, CGFloat)) -> Color { + Color(red: Double(components.0), green: Double(components.1), blue: Double(components.2)) } } diff --git a/DynamicIsland/components/Live activities/InlineHUD.swift b/DynamicIsland/components/Live activities/InlineHUD.swift index 13d6c6d..523c79b 100644 --- a/DynamicIsland/components/Live activities/InlineHUD.swift +++ b/DynamicIsland/components/Live activities/InlineHUD.swift @@ -20,6 +20,7 @@ struct InlineHUD: View { @Default(.useColorCodedVolumeDisplay) var useColorCodedVolumeDisplay @Default(.useSmoothColorGradient) var useSmoothColorGradient @Default(.progressBarStyle) var progressBarStyle + @Default(.showProgressPercentages) var showProgressPercentages @ObservedObject var bluetoothManager = BluetoothAudioManager.shared @State private var displayName: String = "" @@ -130,14 +131,15 @@ struct InlineHUD: View { // Bluetooth device battery display HStack(spacing: 4) { if value > 0 { - // Show color-coded battery bar if battery info available - // Color-coding disabled for segmented or hierarchical mode - if useColorCodedBatteryDisplay && progressBarStyle == .gradient { - ColorCodedProgressBar.battery(value: value, width: 60, height: 4, smoothGradient: useSmoothColorGradient) - } else { - DraggableProgressBar(value: .constant(value)) - .frame(width: 60) + Group { + if useColorCodedBatteryDisplay && progressBarStyle != .segmented { + DraggableProgressBar(value: .constant(value), colorMode: .battery) + } else { + DraggableProgressBar(value: .constant(value)) + } } + .frame(width: 60) + .allowsHitTesting(false) Text("\(Int(value * 100))%") .font(.caption) .fontWeight(.medium) @@ -155,26 +157,35 @@ struct InlineHUD: View { .frame(maxWidth: .infinity, alignment: .trailing) } else { // Volume and brightness displays - HStack { - // Color-coding only available in gradient mode, not hierarchical or segmented - if type == .volume && useColorCodedVolumeDisplay && progressBarStyle == .gradient { - // Color-coded volume (reversed: red at high, green at low) - ColorCodedProgressBar.volume(value: value, width: 70, height: 4, smoothGradient: useSmoothColorGradient) + Group { + if type == .volume { + Group { + if value.isZero { + Text("muted") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.gray) + .lineLimit(1) + .allowsTightening(true) + .multilineTextAlignment(.trailing) + .contentTransition(.numericText()) + } else { + HStack(spacing: 6) { + DraggableProgressBar(value: $value, colorMode: .volume) + PercentageLabel(value: value, isVisible: showProgressPercentages) + } + .transition(.opacity.combined(with: .scale)) + } + } + .animation(.smooth(duration: 0.2), value: value.isZero) } else { - // Default draggable progress bar - DraggableProgressBar(value: $value) - } - - if (type == .volume && value.isZero) { - Text("muted") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.gray) - .lineLimit(1) - .allowsTightening(true) - .multilineTextAlignment(.trailing) + HStack(spacing: 6) { + DraggableProgressBar(value: $value) + PercentageLabel(value: value, isVisible: showProgressPercentages) + } } } + .frame(maxWidth: .infinity, alignment: .trailing) } } .padding(.trailing, 4) diff --git a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift index 911293b..97cceab 100644 --- a/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift +++ b/DynamicIsland/components/Live activities/SystemEventIndicatorModifier.swift @@ -22,6 +22,7 @@ struct SystemEventIndicatorModifier: View { @Binding var icon: String let showSlider: Bool = false var sendEventBack: (CGFloat) -> Void + @Default(.showProgressPercentages) private var showProgressPercentages var body: some View { HStack(spacing: 14) { @@ -58,13 +59,21 @@ struct SystemEventIndicatorModifier: View { default: EmptyView() } - if (eventType != .mic) { - DraggableProgressBar(value: $value) - } else { - Text("Mic \(value > 0 ? "unmuted" : "muted")") + switch eventType { + case .mic: + Text(value > 0 ? "Mic unmuted" : "Mic muted") .foregroundStyle(.gray) .lineLimit(1) .allowsTightening(true) + .contentTransition(.numericText()) + case .volume: + VolumeProgressSection(value: $value, showPercentages: showProgressPercentages) + case .brightness: + ProgressSection(value: $value, showPercentages: showProgressPercentages) + case .backlight: + ProgressSection(value: $value, showPercentages: showProgressPercentages) + default: + ProgressSection(value: $value, showPercentages: showProgressPercentages) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -88,18 +97,106 @@ struct SystemEventIndicatorModifier: View { } } +struct ProgressSection: View { + @Binding var value: CGFloat + var showPercentages: Bool + var colorMode: ProgressColorMode? = nil + + var body: some View { + HStack(spacing: 8) { + DraggableProgressBar(value: $value, colorMode: colorMode) + PercentageLabel(value: value, isVisible: showPercentages) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct VolumeProgressSection: View { + @Binding var value: CGFloat + var showPercentages: Bool + + var body: some View { + Group { + if value.isZero { + Text("muted") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.gray) + .contentTransition(.numericText()) + } else { + ProgressSection(value: $value, showPercentages: showPercentages, colorMode: .volume) + .transition(.opacity.combined(with: .scale)) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .animation(.smooth(duration: 0.2), value: value.isZero) + } +} + +struct PercentageLabel: View { + let value: CGFloat + let isVisible: Bool + @Default(.progressBarStyle) private var progressBarStyle + + var body: some View { + Group { + if shouldShow { + Text(formattedValue) + .font(.caption) + .fontWeight(.semibold) + .monospacedDigit() + .foregroundStyle(.white.opacity(0.9)) + .contentTransition(.numericText()) + .frame(width: 28, alignment: .trailing) + } + } + } + + private var shouldShow: Bool { + isVisible && progressBarStyle != .segmented + } + + private var formattedValue: String { + let raw = max(0, min(100, Int(round(value * 100)))) + return String(format: "%3d", raw) + } +} + +enum ProgressColorMode { + case volume + case battery +} + +private extension ProgressColorMode { + var colorCodingMode: ColorCodingMode { + switch self { + case .volume: + return .volume + case .battery: + return .battery + } + } +} + struct DraggableProgressBar: View { @EnvironmentObject var vm: DynamicIslandViewModel @Binding var value: CGFloat + var colorMode: ProgressColorMode? = nil @State private var isDragging = false - @State private var dragOffset: CGFloat = 0 + @Default(.progressBarStyle) private var progressBarStyle + @Default(.useColorCodedVolumeDisplay) private var useColorCodedVolumeDisplay + @Default(.useColorCodedBatteryDisplay) private var useColorCodedBatteryDisplay + @Default(.useSmoothColorGradient) private var useSmoothColorGradient + @Default(.systemEventIndicatorUseAccent) private var useAccentColor + @Default(.systemEventIndicatorShadow) private var useShadow + @Default(.inlineHUD) private var inlineHUD var body: some View { VStack { GeometryReader { geo in Group { - if Defaults[.progressBarStyle] == .segmented { + if progressBarStyle == .segmented { // Segmented progress bar - completely different layout SegmentedProgressContent(value: value, geometry: geo) } else { @@ -107,23 +204,8 @@ struct DraggableProgressBar: View { ZStack(alignment: .leading) { Capsule() .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) + progressFill(width: geo.size.width) + .opacity(value.isZero ? 0 : 1) } } } @@ -142,7 +224,7 @@ struct DraggableProgressBar: View { } ) } - .frame(height: Defaults[.inlineHUD] ? isDragging ? 8 : 5 : isDragging ? 9 : 6) + .frame(height: inlineHUD ? (isDragging ? 8 : 5) : (isDragging ? 9 : 6)) } } @@ -152,6 +234,59 @@ struct DraggableProgressBar: View { value = max(0, min(newValue, 1)) } + + private func progressFill(width: CGFloat) -> some View { + let clampedWidth = max(0, min(width * value, width)) + let accentColor = Defaults[.accentColor] + let shadowColor: Color = { + guard useShadow else { return .clear } + if useAccentColor { + return accentColor.ensureMinimumBrightness(factor: 0.7) + } + return .white + }() + + return Group { + if shouldUseColorCoding, let mode = colorMode?.colorCodingMode { + Capsule() + .fill(ColorCodedProgressBar.shapeStyle(for: value, mode: mode, smoothGradient: useSmoothColorGradient)) + .frame(width: clampedWidth) + .shadow(color: shadowColor, radius: useShadow ? 8 : 0, x: 3) + } else { + switch progressBarStyle { + case .gradient: + Capsule() + .fill(LinearGradient( + colors: useAccentColor + ? [accentColor, accentColor.ensureMinimumBrightness(factor: 0.2)] + : [.white, .white.opacity(0.2)], + startPoint: .trailing, + endPoint: .leading + )) + .frame(width: clampedWidth) + .shadow(color: shadowColor, radius: useShadow ? 8 : 0, x: 3) + case .hierarchical: + Capsule() + .fill(useAccentColor ? accentColor : .white) + .frame(width: clampedWidth) + .shadow(color: shadowColor, radius: useShadow ? 8 : 0, x: 3) + case .segmented: + EmptyView() + } + } + } + } + + private var shouldUseColorCoding: Bool { + guard let colorMode else { return false } + guard progressBarStyle != .segmented else { return false } + switch colorMode { + case .volume: + return useColorCodedVolumeDisplay + case .battery: + return useColorCodedBatteryDisplay + } + } } struct SegmentedProgressContent: View { @@ -164,8 +299,8 @@ struct SegmentedProgressContent: View { 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 computed = (geometry.size.width - CGFloat(segmentCount - 1) * spacing) / CGFloat(segmentCount) + let barWidth = max(3.0, computed) let activeCount = Int(round(value * CGFloat(segmentCount))) HStack(spacing: spacing) { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index dc29bb0..0d1802b 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -675,27 +675,30 @@ struct HUD: View { } Section { + let colorCodingDisabled = progressBarStyle == .segmented Defaults.Toggle("Color-coded battery display", key: .useColorCodedBatteryDisplay) - .disabled(progressBarStyle != .gradient) + .disabled(colorCodingDisabled) Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay) - .disabled(progressBarStyle != .gradient) - - if progressBarStyle == .gradient && (Defaults[.useColorCodedBatteryDisplay] || Defaults[.useColorCodedVolumeDisplay]) { + .disabled(colorCodingDisabled) + + if !colorCodingDisabled && (Defaults[.useColorCodedBatteryDisplay] || Defaults[.useColorCodedVolumeDisplay]) { Defaults.Toggle("Smooth color transitions", key: .useSmoothColorGradient) } + + Defaults.Toggle("Show percentages beside progress bars", key: .showProgressPercentages) } header: { Text("Color-Coded Progress Bars") } footer: { - if progressBarStyle != .gradient { - Text("Color-coded displays are only available in Gradient mode. Switch Progressbar style to Gradient to enable this feature.") + if progressBarStyle == .segmented { + Text("Color-coded fills and smooth gradients are unavailable in Segmented mode. Switch to Hierarchical or Gradient to adjust these options.") .foregroundStyle(.secondary) .font(.caption) } else if Defaults[.useSmoothColorGradient] { - Text("Smooth: Battery/Volume gradually transitions through all colors\nBattery: Green (high) → Yellow-Green → Yellow → Orange → Red (low)\nVolume: Green (quiet) → Yellow → Orange → Red (loud)") + Text("Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill.") .foregroundStyle(.secondary) .font(.caption) } else { - Text("Discrete: Battery/Volume uses 3 distinct colors only\nBattery: Green (high), Yellow (medium), Red (low)\nVolume: Green (quiet), Yellow (medium), Red (loud)") + Text("Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%).") .foregroundStyle(.secondary) .font(.caption) } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 503bf36..d7c7edc 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -362,6 +362,7 @@ extension Defaults.Keys { static let enableGradient = Key("enableGradient", default: false) static let systemEventIndicatorShadow = Key("systemEventIndicatorShadow", default: false) static let systemEventIndicatorUseAccent = Key("systemEventIndicatorUseAccent", default: false) + static let showProgressPercentages = Key("showProgressPercentages", default: false) // MARK: Shelf static let dynamicShelf = Key("dynamicShelf", default: true)