mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
282 lines
8.9 KiB
Swift
282 lines
8.9 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 {
|
|
|
|
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)
|
|
}
|