import SwiftUI import Defaults /// A view that displays the battery status with an icon and charging indicator. struct BatteryView: View { var levelBattery: Float var isPluggedIn: Bool var isCharging: Bool var isInLowPowerMode: Bool var batteryWidth: CGFloat = 26 var isForNotification: Bool var animationStyle: DynamicIslandAnimations = DynamicIslandAnimations() var icon: String = "battery.0" /// Determines the icon to display when charging. var iconStatus: String { if isCharging { return "bolt" } else if isPluggedIn { return "plug" } else { return "" } } /// Determines the color of the battery based on its status. var batteryColor: Color { if isInLowPowerMode { return .yellow } else if levelBattery <= 20 && !isCharging && !isPluggedIn { return .red } else if isCharging || isPluggedIn || levelBattery == 100 { return .green } else { return .white } } var body: some View { ZStack(alignment: .leading) { Image(systemName: icon) .resizable() .fontWeight(.thin) .aspectRatio(contentMode: .fit) .foregroundColor(.white.opacity(0.5)) .frame( width: batteryWidth + 1 ) RoundedRectangle(cornerRadius: 2.5) .fill(batteryColor) .frame( width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))), height: (batteryWidth - 2.75) - 18 ) .padding(.leading, 2) if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) { ZStack { Image(iconStatus) .resizable() .aspectRatio(contentMode: .fit) .foregroundColor(.white) .frame( width: 17, height: 17 ) } .frame(width: batteryWidth, height: batteryWidth) } } } } /// A view that displays detailed battery information and settings. struct BatteryMenuView: View { var isPluggedIn: Bool var isCharging: Bool var levelBattery: Float var maxCapacity: Float var timeToFullCharge: Int var isInLowPowerMode: Bool var onDismiss: () -> Void @Environment(\.openURL) private var openURL var body: some View { VStack(alignment: .leading, spacing: 16) { HStack { Text("Battery Status") .font(.headline) .fontWeight(.semibold) Spacer() Text("\(Int(levelBattery))%") .font(.headline) .fontWeight(.semibold) } VStack(alignment: .leading, spacing: 8) { Text("Max Capacity: \(Int(maxCapacity))%") .font(.subheadline) .fontWeight(.regular) if isInLowPowerMode { Label("Low Power Mode", systemImage: "bolt.circle") .font(.subheadline) .fontWeight(.regular) } if isCharging { Label("Charging", systemImage: "bolt.fill") .font(.subheadline) .fontWeight(.regular) } if isPluggedIn { Label("Plugged In", systemImage: "powerplug.fill") .font(.subheadline) .fontWeight(.regular) } if timeToFullCharge > 0 { Label("Time to Full Charge: \(timeToFullCharge) min", systemImage: "clock") .font(.subheadline) .fontWeight(.regular) } if !isCharging && isPluggedIn && levelBattery >= 80 { Label("Charging on Hold: Desktop Mode", systemImage: "desktopcomputer") .font(.subheadline) .fontWeight(.regular) } } .padding(.vertical, 8) Divider().background(Color.white) Button(action: openBatteryPreferences) { Label("Battery Settings", systemImage: "gearshape") .fontWeight(.regular) } .frame(maxWidth: .infinity) .buttonStyle(.plain) .padding(.vertical, 8) } .padding() .frame(width: 280) .foregroundColor(.white) } private func openBatteryPreferences() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.battery") { openURL(url) onDismiss() } } } /// A view that displays the battery status and allows interaction to show detailed information. struct DynamicIslandBatteryView: View { @State var batteryWidth: CGFloat = 26 var isCharging: Bool = false var isInLowPowerMode: Bool = false var isPluggedIn: Bool = false var levelBattery: Float = 0 var maxCapacity: Float = 0 var timeToFullCharge: Int = 0 @State var isForNotification: Bool = false @State private var showPopupMenu: Bool = false @State private var isPressed: Bool = false @State private var isHoveringPopover: Bool = false @EnvironmentObject var vm: DynamicIslandViewModel var body: some View { HStack { if Defaults[.showBatteryPercentage] { ZStack(alignment: .trailing) { Text("100%") .font(.callout) .hidden() Text("\(Int32(levelBattery))%") .font(.callout) .foregroundStyle(.white) .lineLimit(1) } .fixedSize(horizontal: true, vertical: false) } BatteryView( levelBattery: levelBattery, isPluggedIn: isPluggedIn, isCharging: isCharging, isInLowPowerMode: isInLowPowerMode, batteryWidth: batteryWidth, isForNotification: isForNotification ) } .scaleEffect(isPressed ? 0.95 : 1.0) .animation(.easeInOut(duration: 0.2), value: isPressed) .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in withAnimation { isPressed = true } } .onEnded { _ in withAnimation { isPressed = false showPopupMenu.toggle() } } ) .popover( isPresented: $showPopupMenu, arrowEdge: .bottom) { BatteryMenuView( isPluggedIn: isPluggedIn, isCharging: isCharging, levelBattery: levelBattery, maxCapacity: maxCapacity, timeToFullCharge: timeToFullCharge, isInLowPowerMode: isInLowPowerMode, onDismiss: { showPopupMenu = false } ) .onHover { hovering in isHoveringPopover = hovering } } .onChange(of: showPopupMenu) { _, _ in updateBatteryPopoverActiveState() } .onChange(of: isHoveringPopover) { _, _ in updateBatteryPopoverActiveState() } } private func updateBatteryPopoverActiveState() { vm.isBatteryPopoverActive = showPopupMenu && isHoveringPopover } } #Preview { DynamicIslandBatteryView( batteryWidth: 30, isCharging: false, isInLowPowerMode: false, isPluggedIn: true, levelBattery: 80, maxCapacity: 100, timeToFullCharge: 10, isForNotification: false ).frame(width: 200, height: 200) }