mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
720 lines
23 KiB
Swift
720 lines
23 KiB
Swift
/*
|
|
* Atoll (DynamicIsland)
|
|
* Copyright (C) 2024-2026 Atoll Contributors
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import SwiftUI
|
|
import Defaults
|
|
|
|
/// A view that displays the battery status with an icon and charging indicator.
|
|
struct BatteryView: View {
|
|
@Default(.showPowerStatusIcons) var showPowerStatusIcons
|
|
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 || 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 {
|
|
|
|
@Default(.showBatteryPercentage) var showBatteryPercentage
|
|
@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 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
|
|
}
|
|
}
|
|
|
|
|
|
private struct BatteryTemporaryHUDMetrics {
|
|
let width: CGFloat
|
|
let height: CGFloat
|
|
let topRadius: CGFloat
|
|
let bottomRadius: CGFloat
|
|
}
|
|
|
|
private extension BatteryTemporaryHUDKind {
|
|
func metrics(
|
|
style: BatteryNotificationStyle,
|
|
closedNotchWidth: CGFloat,
|
|
baseHeight: CGFloat
|
|
) -> BatteryTemporaryHUDMetrics {
|
|
let compactBaseRadius = max(baseHeight / 2, 16)
|
|
let compactTopRadius = max(12, compactBaseRadius - 4)
|
|
|
|
switch (self, style) {
|
|
case (.charging, _), (.lowBattery, .compact), (.fullBattery, .compact):
|
|
return BatteryTemporaryHUDMetrics(
|
|
width: closedNotchWidth + 180,
|
|
height: baseHeight,
|
|
topRadius: compactTopRadius,
|
|
bottomRadius: compactBaseRadius
|
|
)
|
|
case (.lowBattery, .standard):
|
|
return BatteryTemporaryHUDMetrics(
|
|
width: closedNotchWidth + 150,
|
|
height: baseHeight + 75,
|
|
topRadius: 22,
|
|
bottomRadius: 40
|
|
)
|
|
case (.fullBattery, .standard):
|
|
return BatteryTemporaryHUDMetrics(
|
|
width: closedNotchWidth + 140,
|
|
height: baseHeight + 70,
|
|
topRadius: 18,
|
|
bottomRadius: 36
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BatteryCompactStatusRow: View {
|
|
let title: String
|
|
let batteryLevel: Int
|
|
let tint: Color
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Text(verbatim: title)
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.white.opacity(0.8))
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 6) {
|
|
Text("\(batteryLevel)%")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(tint)
|
|
|
|
HStack(spacing: 1.5) {
|
|
ZStack(alignment: .leading) {
|
|
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
.fill(tint.opacity(0.3))
|
|
|
|
GeometryReader { geo in
|
|
let clamped = max(0, min(batteryLevel, 100))
|
|
let width = CGFloat(clamped) / 100 * geo.size.width
|
|
Rectangle()
|
|
.fill(tint.gradient)
|
|
.frame(width: max(0, width))
|
|
}
|
|
}
|
|
.frame(width: 28, height: 16)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
|
|
|
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
|
|
.fill(batteryLevel == 100 ? tint.gradient : tint.opacity(0.3).gradient)
|
|
.frame(width: 2, height: 6)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
}
|
|
}
|
|
|
|
struct BatteryTemporaryActivityView: View {
|
|
let kind: BatteryTemporaryHUDKind
|
|
let batteryLevel: Int
|
|
let isLowPowerMode: Bool
|
|
let closedNotchWidth: CGFloat
|
|
let baseHeight: CGFloat
|
|
let isDynamicIslandMode: Bool
|
|
let topCornerRadius: CGFloat
|
|
@Default(.lowBatteryHUDStyle) var lowBatteryHUDStyle
|
|
@Default(.fullBatteryHUDStyle) var fullBatteryHUDStyle
|
|
var styleOverride: BatteryNotificationStyle? = nil
|
|
|
|
@State private var pulse = false
|
|
@State private var showBatteryIndicator = false
|
|
@State private var changeBatteryIndicator = true
|
|
|
|
private var style: BatteryNotificationStyle {
|
|
if kind == .charging {
|
|
return .compact
|
|
}
|
|
if let styleOverride {
|
|
return styleOverride
|
|
}
|
|
switch kind {
|
|
case .charging:
|
|
return .compact
|
|
case .lowBattery:
|
|
return lowBatteryHUDStyle
|
|
case .fullBattery:
|
|
return fullBatteryHUDStyle
|
|
}
|
|
}
|
|
|
|
private var metrics: BatteryTemporaryHUDMetrics {
|
|
kind.metrics(
|
|
style: style,
|
|
closedNotchWidth: closedNotchWidth,
|
|
baseHeight: baseHeight
|
|
)
|
|
}
|
|
|
|
private var batteryTint: Color {
|
|
switch kind {
|
|
case .charging:
|
|
if isLowPowerMode {
|
|
return .yellow
|
|
} else if batteryLevel <= 20 {
|
|
return .red
|
|
}
|
|
return .green
|
|
case .lowBattery:
|
|
return isLowPowerMode ? .yellow : .red
|
|
case .fullBattery:
|
|
return isLowPowerMode ? .yellow : .green
|
|
}
|
|
}
|
|
|
|
|
|
private var surfaceShape: AnyShape {
|
|
if isDynamicIslandMode {
|
|
return AnyShape(DynamicIslandPillShape(cornerRadius: dynamicIslandPillCornerRadiusInsets.opened))
|
|
} else {
|
|
return AnyShape(NotchShape(topCornerRadius: topCornerRadius, bottomCornerRadius: metrics.bottomRadius))
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottom) {
|
|
content
|
|
}
|
|
.frame(width: metrics.width, height: metrics.height, alignment: .bottom)
|
|
.clipShape(surfaceShape)
|
|
.onAppear(perform: prepareAnimations)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
if style == .compact {
|
|
BatteryCompactStatusRow(
|
|
title: compactTitle,
|
|
batteryLevel: batteryLevel,
|
|
tint: batteryTint
|
|
)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
} else {
|
|
VStack {
|
|
Spacer()
|
|
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: kind == .lowBattery ? 2 : 3) {
|
|
standardTitle
|
|
standardDescription
|
|
}
|
|
|
|
Spacer()
|
|
|
|
standardIndicator
|
|
}
|
|
.padding(.leading, kind == .lowBattery ? 40 : 35)
|
|
.padding(.trailing, kind == .lowBattery ? 45 : 40)
|
|
.padding(.bottom, 20)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var compactTitle: String {
|
|
switch kind {
|
|
case .charging:
|
|
return "Charging"
|
|
case .lowBattery:
|
|
return "Low Battery"
|
|
case .fullBattery:
|
|
return "Full Battery"
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var standardTitle: some View {
|
|
HStack(spacing: 5) {
|
|
Text(verbatim: kind == .lowBattery ? "Battery Low" : "Full Battery")
|
|
.font(.system(size: kind == .lowBattery ? 13 : 13, weight: .semibold))
|
|
.foregroundStyle(.white.opacity(0.82))
|
|
.lineLimit(1)
|
|
|
|
Text("\(batteryLevel)%")
|
|
.font(.system(size: kind == .lowBattery ? 12 : 13, weight: .semibold))
|
|
.foregroundStyle(batteryTint)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var standardDescription: some View {
|
|
switch kind {
|
|
case .charging:
|
|
EmptyView()
|
|
case .lowBattery:
|
|
if isLowPowerMode {
|
|
(
|
|
Text(verbatim: "Low Power Mode enabled")
|
|
.foregroundColor(.yellow)
|
|
.font(.system(size: 10, weight: .medium))
|
|
+
|
|
Text(verbatim: ", it is recommended to charge it.")
|
|
.foregroundColor(.gray.opacity(0.6))
|
|
.font(.system(size: 10, weight: .medium))
|
|
)
|
|
} else {
|
|
Text(verbatim: "Turn on Low Power Mode or it\nis recommended to charge it.")
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(.gray.opacity(0.6))
|
|
.lineLimit(2)
|
|
}
|
|
case .fullBattery:
|
|
Text(verbatim: "Your Mac is fully charged.")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.gray.opacity(0.6))
|
|
.fontWeight(.medium)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var standardIndicator: some View {
|
|
switch kind {
|
|
case .charging:
|
|
EmptyView()
|
|
case .lowBattery:
|
|
if isLowPowerMode {
|
|
yellowLowIndicator
|
|
} else {
|
|
redLowIndicator
|
|
}
|
|
case .fullBattery:
|
|
if showBatteryIndicator {
|
|
if isLowPowerMode {
|
|
yellowFullIndicator
|
|
.transition(.opacity.combined(with: .scale))
|
|
} else {
|
|
greenFullIndicator
|
|
.transition(.opacity.combined(with: .scale))
|
|
}
|
|
} else {
|
|
magSafeIndicator
|
|
.transition(.opacity.combined(with: .scale))
|
|
}
|
|
}
|
|
}
|
|
|
|
private var redLowIndicator: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 30)
|
|
.fill(.red.opacity(0.2))
|
|
.frame(width: 70, height: 40)
|
|
|
|
HStack(spacing: 2) {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.red.opacity(0.4))
|
|
.frame(width: 40, height: 24)
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.red.opacity(0.4))
|
|
.frame(width: 3, height: 8)
|
|
}
|
|
.padding(.trailing, 5)
|
|
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color.red.gradient)
|
|
.frame(width: 8, height: 14)
|
|
.opacity(pulse ? 1 : 0.3)
|
|
.offset(x: -15)
|
|
|
|
RoundedRectangle(cornerRadius: 30)
|
|
.stroke(Color.red.opacity(0.9), lineWidth: 1.5)
|
|
.frame(width: pulse ? 8 : 30, height: pulse ? 14 : 32)
|
|
.offset(x: -15)
|
|
.opacity(pulse ? 0.3 : 1)
|
|
}
|
|
}
|
|
|
|
private var yellowLowIndicator: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 30)
|
|
.fill(.yellow.opacity(0.2))
|
|
.frame(width: 70, height: 40)
|
|
|
|
HStack(spacing: 2) {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.yellow.opacity(0.4))
|
|
.frame(width: 40, height: 24)
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.yellow.opacity(0.4))
|
|
.frame(width: 3, height: 8)
|
|
}
|
|
.padding(.trailing, 5)
|
|
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(.yellow.gradient)
|
|
.frame(width: 8, height: 14)
|
|
.offset(x: -15)
|
|
}
|
|
}
|
|
|
|
private var greenFullIndicator: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 30)
|
|
.fill(.green.opacity(0.2))
|
|
.frame(width: 70, height: 40)
|
|
|
|
HStack(spacing: 2) {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.green.opacity(0.4))
|
|
.frame(width: 44, height: 24)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.green.gradient)
|
|
.frame(width: 34, height: 14)
|
|
.opacity(pulse ? 1 : 0.4)
|
|
)
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.green.opacity(0.4))
|
|
.frame(width: 3, height: 8)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var yellowFullIndicator: some View {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 30)
|
|
.fill(.yellow.opacity(0.2))
|
|
.frame(width: 70, height: 40)
|
|
|
|
HStack(spacing: 2) {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.yellow.opacity(0.4))
|
|
.frame(width: 44, height: 24)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.yellow.gradient)
|
|
.frame(width: 34, height: 14)
|
|
.opacity(pulse ? 1 : 0.4)
|
|
)
|
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(.yellow.opacity(0.4))
|
|
.frame(width: 3, height: 8)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var magSafeIndicator: some View {
|
|
HStack(spacing: 0) {
|
|
Rectangle()
|
|
.fill(.gray.opacity(0.15))
|
|
.frame(width: 30, height: 5)
|
|
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(.gray.opacity(0.2).gradient)
|
|
.frame(width: 30, height: 40)
|
|
|
|
Circle()
|
|
.fill(changeBatteryIndicator ? .orange : .green)
|
|
.shadow(color: changeBatteryIndicator ? .orange : .green, radius: 5)
|
|
.frame(width: 5, height: 5)
|
|
}
|
|
|
|
Rectangle()
|
|
.fill(.white.opacity(0.4))
|
|
.frame(width: 3, height: 32)
|
|
}
|
|
}
|
|
|
|
private func prepareAnimations() {
|
|
pulse = false
|
|
showBatteryIndicator = kind == .fullBattery && style == .standard
|
|
changeBatteryIndicator = true
|
|
|
|
guard style == .standard else { return }
|
|
|
|
switch kind {
|
|
case .charging:
|
|
break
|
|
case .lowBattery:
|
|
if !isLowPowerMode {
|
|
withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
}
|
|
case .fullBattery:
|
|
withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
|
|
pulse = true
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
withAnimation(.spring(duration: 0.4)) {
|
|
showBatteryIndicator = false
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
|
withAnimation(.spring(duration: 0.2)) {
|
|
changeBatteryIndicator = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
DynamicIslandBatteryView(
|
|
batteryWidth: 30,
|
|
isCharging: false,
|
|
isInLowPowerMode: false,
|
|
isPluggedIn: true,
|
|
levelBattery: 80,
|
|
maxCapacity: 100,
|
|
timeToFullCharge: 10,
|
|
isForNotification: false
|
|
).frame(width: 200, height: 200)
|
|
}
|