Atoll/DynamicIsland/components/Live activities/DynamicIslandBattery.swift
Hariharan Mudaliar 13a2ff0e20 Working
2025-07-13 22:16:02 +05:30

256 lines
7.9 KiB
Swift

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] {
Text("\(Int32(levelBattery))%")
.font(.callout)
.foregroundStyle(.white)
}
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)
}