mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Working Color, Bluetooth and not working DND
This commit is contained in:
parent
dc774a073b
commit
c00b6db984
12 changed files with 13150 additions and 11440 deletions
179
DynamicIsland/components/ColorCodedProgressBar.swift
Normal file
179
DynamicIsland/components/ColorCodedProgressBar.swift
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//
|
||||
// ColorCodedProgressBar.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for color-coded battery and volume displays
|
||||
// Provides smooth gradient transitions from red to green (or reversed)
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
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 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) {
|
||||
self.value = min(max(value, 0), 1) // Clamp between 0 and 1
|
||||
self.reversed = reversed
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.smoothGradient = smoothGradient
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
RoundedRectangle(cornerRadius: height / 2)
|
||||
.fill(Color.white.opacity(0.2))
|
||||
.frame(width: width, height: height)
|
||||
|
||||
// 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))
|
||||
)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Extensions
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Battery Mode") {
|
||||
VStack(spacing: 20) {
|
||||
ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { value in
|
||||
HStack {
|
||||
Text("\(Int(value * 100))%")
|
||||
.frame(width: 50, alignment: .trailing)
|
||||
.foregroundColor(.white)
|
||||
ColorCodedProgressBar.battery(value: value, width: 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
}
|
||||
|
||||
#Preview("Volume Mode (Reversed)") {
|
||||
VStack(spacing: 20) {
|
||||
ForEach([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], id: \.self) { value in
|
||||
HStack {
|
||||
Text("\(Int(value * 100))%")
|
||||
.frame(width: 50, alignment: .trailing)
|
||||
.foregroundColor(.white)
|
||||
ColorCodedProgressBar.volume(value: value, width: 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
struct InlineHUD: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
|
|
@ -14,6 +15,15 @@ struct InlineHUD: View {
|
|||
@Binding var icon: String
|
||||
@Binding var hoverAnimation: Bool
|
||||
@Binding var gestureProgress: CGFloat
|
||||
|
||||
@Default(.useColorCodedBatteryDisplay) var useColorCodedBatteryDisplay
|
||||
@Default(.useColorCodedVolumeDisplay) var useColorCodedVolumeDisplay
|
||||
@Default(.useSmoothColorGradient) var useSmoothColorGradient
|
||||
@Default(.progressBarStyle) var progressBarStyle
|
||||
@ObservedObject var bluetoothManager = BluetoothAudioManager.shared
|
||||
|
||||
@State private var displayName: String = ""
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack(spacing: 5) {
|
||||
|
|
@ -21,7 +31,9 @@ struct InlineHUD: View {
|
|||
switch (type) {
|
||||
case .volume:
|
||||
if icon.isEmpty {
|
||||
Image(systemName: SpeakerSymbol(value))
|
||||
// Show headphone icon if Bluetooth audio is connected, otherwise speaker
|
||||
let baseIcon = bluetoothManager.isBluetoothAudioConnected ? "headphones" : SpeakerSymbol(value)
|
||||
Image(systemName: baseIcon)
|
||||
.contentTransition(.interpolate)
|
||||
.symbolVariant(value > 0 ? .none : .slash)
|
||||
.frame(width: 20, height: 15, alignment: .leading)
|
||||
|
|
@ -51,6 +63,11 @@ struct InlineHUD: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
.contentTransition(.interpolate)
|
||||
.frame(width: 20, height: 15, alignment: .center)
|
||||
case .bluetoothAudio:
|
||||
Image(systemName: icon.isEmpty ? "bluetooth" : icon)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.contentTransition(.interpolate)
|
||||
.frame(width: 20, height: 15, alignment: .center)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
|
@ -58,12 +75,33 @@ struct InlineHUD: View {
|
|||
.foregroundStyle(.white)
|
||||
.symbolVariant(.fill)
|
||||
|
||||
Text(Type2Name(type))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
.allowsTightening(true)
|
||||
.contentTransition(.numericText())
|
||||
// Use marquee text for device names to handle long names
|
||||
if type == .bluetoothAudio {
|
||||
MarqueeText(
|
||||
$displayName,
|
||||
font: .system(size: 13, weight: .medium),
|
||||
nsFont: .body,
|
||||
textColor: .white,
|
||||
minDuration: 0.5,
|
||||
frameWidth: 85 - (hoverAnimation ? 0 : 12) + gestureProgress / 2
|
||||
)
|
||||
.onAppear {
|
||||
displayName = Type2Name(type)
|
||||
}
|
||||
.onChange(of: type) { _, _ in
|
||||
displayName = Type2Name(type)
|
||||
}
|
||||
.onChange(of: bluetoothManager.lastConnectedDevice?.name) { _, _ in
|
||||
displayName = Type2Name(type)
|
||||
}
|
||||
} else {
|
||||
Text(Type2Name(type))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
.allowsTightening(true)
|
||||
.contentTransition(.numericText())
|
||||
}
|
||||
}
|
||||
.frame(width: 100 - (hoverAnimation ? 0 : 12) + gestureProgress / 2, height: vm.notchSize.height - (hoverAnimation ? 0 : 12), alignment: .leading)
|
||||
|
||||
|
|
@ -88,9 +126,45 @@ struct InlineHUD: View {
|
|||
.multilineTextAlignment(.trailing)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.contentTransition(.interpolate)
|
||||
} else if (type == .bluetoothAudio) {
|
||||
// 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)
|
||||
}
|
||||
Text("\(Int(value * 100))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(1)
|
||||
} else {
|
||||
// No battery info available
|
||||
Text("Connected")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.gray)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
} else {
|
||||
// Volume and brightness displays
|
||||
HStack {
|
||||
DraggableProgressBar(value: $value)
|
||||
// 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)
|
||||
} else {
|
||||
// Default draggable progress bar
|
||||
DraggableProgressBar(value: $value)
|
||||
}
|
||||
|
||||
if (type == .volume && value.isZero) {
|
||||
Text("muted")
|
||||
.font(.caption)
|
||||
|
|
@ -145,6 +219,8 @@ struct InlineHUD: View {
|
|||
return "Backlight"
|
||||
case .mic:
|
||||
return "Mic"
|
||||
case .bluetoothAudio:
|
||||
return BluetoothAudioManager.shared.lastConnectedDevice?.name ?? "Bluetooth"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,11 +75,21 @@ struct MarqueeText: View {
|
|||
self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize)
|
||||
self.animate = false
|
||||
self.offset = 0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01){
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02){
|
||||
if needsScrolling {
|
||||
self.animate = true
|
||||
self.offset = -(textSize.width + 10)
|
||||
|
||||
self.offset = -(textSize.width + 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: text) { _, _ in
|
||||
// Reset animation when text changes
|
||||
self.animate = false
|
||||
self.offset = 0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02){
|
||||
if needsScrolling {
|
||||
self.animate = true
|
||||
self.offset = -(textSize.width + 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -543,6 +543,43 @@ struct HUD: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections)
|
||||
} header: {
|
||||
Text("Bluetooth Audio Devices")
|
||||
} footer: {
|
||||
Text("Displays a HUD notification when Bluetooth audio devices (headphones, AirPods, speakers) connect, showing device name and battery level.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Color-coded battery display", key: .useColorCodedBatteryDisplay)
|
||||
.disabled(progressBarStyle != .gradient)
|
||||
Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay)
|
||||
.disabled(progressBarStyle != .gradient)
|
||||
|
||||
if progressBarStyle == .gradient && (Defaults[.useColorCodedBatteryDisplay] || Defaults[.useColorCodedVolumeDisplay]) {
|
||||
Defaults.Toggle("Smooth color transitions", key: .useSmoothColorGradient)
|
||||
}
|
||||
} 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.")
|
||||
.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)")
|
||||
.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)")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("HUD style", selection: $inlineHUD) {
|
||||
Text("Default")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue