mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added portal for the Battery module
This commit is contained in:
@@ -46,10 +46,12 @@ struct Battery_Usage: value_t {
|
||||
}
|
||||
|
||||
public class Battery: Module {
|
||||
private let popupView: Popup
|
||||
private let settingsView: Settings
|
||||
private let portalView: Portal
|
||||
|
||||
private var usageReader: UsageReader? = nil
|
||||
private var processReader: ProcessReader? = nil
|
||||
private let popupView: Popup
|
||||
private var settingsView: Settings
|
||||
|
||||
private var lowLevelNotificationState: Bool = false
|
||||
private var highLevelNotificationState: Bool = false
|
||||
@@ -58,10 +60,12 @@ public class Battery: Module {
|
||||
public init() {
|
||||
self.settingsView = Settings("Battery")
|
||||
self.popupView = Popup("Battery")
|
||||
self.portalView = Portal("Battery")
|
||||
|
||||
super.init(
|
||||
popup: self.popupView,
|
||||
settings: self.settingsView
|
||||
settings: self.settingsView,
|
||||
portal: self.portalView
|
||||
)
|
||||
guard self.available else { return }
|
||||
|
||||
@@ -123,6 +127,7 @@ public class Battery: Module {
|
||||
self.checkLowNotification(value: value)
|
||||
self.checkHighNotification(value: value)
|
||||
self.popupView.usageCallback(value)
|
||||
self.portalView.loadCallback(value)
|
||||
|
||||
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: Widget) in
|
||||
switch w.item {
|
||||
|
||||
@@ -57,10 +57,10 @@ internal class Popup: NSView, Popup_p {
|
||||
private var processes: [ProcessView] = []
|
||||
private var processesInitialized: Bool = false
|
||||
|
||||
private var colorState: Bool = false
|
||||
|
||||
private var numberOfProcesses: Int {
|
||||
get {
|
||||
return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
|
||||
}
|
||||
return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
|
||||
}
|
||||
private var processesHeight: CGFloat {
|
||||
get {
|
||||
@@ -69,9 +69,7 @@ internal class Popup: NSView, Popup_p {
|
||||
}
|
||||
}
|
||||
private var timeFormat: String {
|
||||
get {
|
||||
return Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: "short")
|
||||
}
|
||||
Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: "short")
|
||||
}
|
||||
|
||||
public var sizeCallback: ((NSSize) -> Void)? = nil
|
||||
@@ -87,6 +85,8 @@ internal class Popup: NSView, Popup_p {
|
||||
))
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height + self.detailsHeight + self.processesHeight))
|
||||
|
||||
self.colorState = Store.shared.bool(key: "\(self.title)_color", defaultValue: self.colorState)
|
||||
|
||||
let gridView: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
|
||||
gridView.rowSpacing = 0
|
||||
gridView.yPlacement = .fill
|
||||
@@ -347,14 +347,32 @@ internal class Popup: NSView, Popup_p {
|
||||
// MARK: - Settings
|
||||
|
||||
public func settings() -> NSView? {
|
||||
return nil
|
||||
let view = SettingsContainerView()
|
||||
|
||||
view.addArrangedSubview(toggleSettingRow(
|
||||
title: localizedString("Colorize battery"),
|
||||
action: #selector(toggleColor),
|
||||
state: self.colorState
|
||||
))
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
@objc private func toggleColor(_ sender: NSControl) {
|
||||
self.colorState = controlState(sender)
|
||||
Store.shared.set(key: "\(self.title)_color", value: self.colorState)
|
||||
self.dashboardBatteryView?.display()
|
||||
}
|
||||
}
|
||||
|
||||
private class BatteryView: NSView {
|
||||
internal class BatteryView: NSView {
|
||||
private var percentage: Double = 0
|
||||
|
||||
public override init(frame: NSRect) {
|
||||
private var colorState: Bool {
|
||||
return Store.shared.bool(key: "Battery_color", defaultValue: false)
|
||||
}
|
||||
|
||||
public override init(frame: NSRect = NSRect.zero) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
@@ -365,27 +383,41 @@ private class BatteryView: NSView {
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let w: CGFloat = 130
|
||||
let h: CGFloat = 50
|
||||
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
||||
|
||||
let w: CGFloat = min(dirtyRect.width, 120)
|
||||
let h: CGFloat = min(dirtyRect.height, 50)
|
||||
let x: CGFloat = (dirtyRect.width - w)/2
|
||||
let y: CGFloat = (dirtyRect.size.height - h) / 2
|
||||
let radius: CGFloat = 3
|
||||
let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y, width: w, height: h), xRadius: radius, yRadius: radius)
|
||||
let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y+1, width: w-8, height: h-2), xRadius: 3, yRadius: 3)
|
||||
|
||||
NSColor.textColor.set()
|
||||
|
||||
let bPX: CGFloat = x+w+1
|
||||
let bPY: CGFloat = (dirtyRect.size.height / 2) - 4
|
||||
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 4, height: 8), xRadius: radius, yRadius: radius)
|
||||
batteryPoint.lineWidth = 1.1
|
||||
batteryPoint.stroke()
|
||||
let bPX: CGFloat = batteryFrame.bounds.origin.x + batteryFrame.bounds.width
|
||||
let bPY: CGFloat = batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2) - 4
|
||||
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX-2, y: bPY, width: 8, height: 8), xRadius: 4, yRadius: 4)
|
||||
batteryPoint.fill()
|
||||
|
||||
let batteryPointSeparator = NSBezierPath()
|
||||
batteryPointSeparator.move(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y))
|
||||
batteryPointSeparator.line(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y + batteryFrame.bounds.height))
|
||||
ctx.saveGState()
|
||||
ctx.setBlendMode(.destinationOut)
|
||||
NSColor.textColor.set()
|
||||
batteryPointSeparator.lineWidth = 4
|
||||
batteryPointSeparator.stroke()
|
||||
ctx.restoreGState()
|
||||
|
||||
batteryFrame.lineWidth = 1
|
||||
batteryFrame.stroke()
|
||||
|
||||
let maxWidth = w-2
|
||||
let inner = NSBezierPath(roundedRect: NSRect(x: x+2, y: y+1, width: maxWidth * CGFloat(self.percentage), height: h-2), xRadius: radius, yRadius: radius)
|
||||
self.percentage.batteryColor(color: true).set()
|
||||
let inner = NSBezierPath(roundedRect: NSRect(
|
||||
x: x+2,
|
||||
y: y+2,
|
||||
width: (w-10) * CGFloat(self.percentage),
|
||||
height: h-4
|
||||
), xRadius: 3, yRadius: 3)
|
||||
self.percentage.batteryColor(color: self.colorState).set()
|
||||
inner.lineWidth = 0
|
||||
inner.stroke()
|
||||
inner.close()
|
||||
|
||||
89
Modules/Battery/portal.swift
Normal file
89
Modules/Battery/portal.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// portal.swift
|
||||
// Battery
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 16/03/2023
|
||||
// Using Swift 5.0
|
||||
// Running on macOS 13.2
|
||||
//
|
||||
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Kit
|
||||
|
||||
internal class Portal: NSStackView, Portal_p {
|
||||
var name: String
|
||||
|
||||
private let batteryView: BatteryView = BatteryView()
|
||||
private var levelField: NSTextField = ValueField(frame: NSRect.zero, "")
|
||||
private var timeField: NSTextField = ValueField(frame: NSRect.zero, "")
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
private var timeFormat: String {
|
||||
Store.shared.string(key: "\(self.name)_timeFormat", defaultValue: "short")
|
||||
}
|
||||
|
||||
init(_ name: String) {
|
||||
self.name = name
|
||||
|
||||
super.init(frame: NSRect.zero)
|
||||
|
||||
self.wantsLayer = true
|
||||
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
|
||||
self.layer?.cornerRadius = 3
|
||||
|
||||
self.orientation = .vertical
|
||||
self.distribution = .fillEqually
|
||||
self.edgeInsets = NSEdgeInsets(
|
||||
top: Constants.Popup.margins,
|
||||
left: Constants.Popup.margins,
|
||||
bottom: Constants.Popup.margins,
|
||||
right: Constants.Popup.margins
|
||||
)
|
||||
self.spacing = 0
|
||||
|
||||
let box: NSStackView = NSStackView()
|
||||
box.heightAnchor.constraint(equalToConstant: 13).isActive = true
|
||||
box.orientation = .horizontal
|
||||
box.spacing = 0
|
||||
|
||||
self.levelField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
self.timeField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
box.addArrangedSubview(self.levelField)
|
||||
box.addArrangedSubview(NSView())
|
||||
box.addArrangedSubview(self.timeField)
|
||||
|
||||
self.addArrangedSubview(self.batteryView)
|
||||
self.addArrangedSubview(box)
|
||||
|
||||
self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func updateLayer() {
|
||||
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
|
||||
}
|
||||
|
||||
public func loadCallback(_ value: Battery_Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if (self.window?.isVisible ?? false) || !self.initialized {
|
||||
self.levelField.stringValue = "\(Int(abs(value.level) * 100))%"
|
||||
|
||||
var seconds: Double = 0
|
||||
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
|
||||
seconds = Double((value.powerSource == "Battery Power" ? value.timeToEmpty : value.timeToCharge)*60)
|
||||
}
|
||||
self.timeField.stringValue = seconds != 0 ? seconds.printSecondsToHoursMinutesSeconds(short: self.timeFormat == "short") : ""
|
||||
|
||||
self.batteryView.setValue(abs(value.level))
|
||||
self.initialized = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
5CFE494229265418000F2856 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494129265418000F2856 /* uninstall.sh */; };
|
||||
5CFE494429265421000F2856 /* changelog.py in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494329265421000F2856 /* changelog.py */; };
|
||||
5EE8037F29C36BDD0063D37D /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8037E29C36BDD0063D37D /* portal.swift */; };
|
||||
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
|
||||
9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; };
|
||||
9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
@@ -367,6 +368,7 @@
|
||||
5CFE493B292650F8000F2856 /* Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Launchd.plist; sourceTree = "<group>"; };
|
||||
5CFE494129265418000F2856 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = uninstall.sh; sourceTree = "<group>"; };
|
||||
5CFE494329265421000F2856 /* changelog.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = changelog.py; sourceTree = "<group>"; };
|
||||
5EE8037E29C36BDD0063D37D /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||
62BA5F74254810C8009D0AC2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
63A07F97275018DF00352C46 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7A19DAE52552C326001B192F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -916,6 +918,7 @@
|
||||
children = (
|
||||
9ABFF902248BEBD700C9041A /* main.swift */,
|
||||
9ABFF90F248BEE7200C9041A /* readers.swift */,
|
||||
5EE8037E29C36BDD0063D37D /* portal.swift */,
|
||||
9ABFF913248C30A800C9041A /* popup.swift */,
|
||||
9AD64FA124BF86C100419D59 /* settings.swift */,
|
||||
9ABFF8F9248BEBCB00C9041A /* Info.plist */,
|
||||
@@ -1720,6 +1723,7 @@
|
||||
files = (
|
||||
9ABFF910248BEE7200C9041A /* readers.swift in Sources */,
|
||||
9ABFF914248C30A800C9041A /* popup.swift in Sources */,
|
||||
5EE8037F29C36BDD0063D37D /* portal.swift in Sources */,
|
||||
9AD64FA224BF86C100419D59 /* settings.swift in Sources */,
|
||||
9ABFF903248BEBD700C9041A /* main.swift in Sources */,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user