mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: separate dashboard and Stats settings
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
|
||||
9A0C82E124460F7200FAE3D4 /* StatsKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; };
|
||||
9A0C82E224460F7200FAE3D4 /* StatsKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9A0C82E624460F9A00FAE3D4 /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A654920244074B500E30B74 /* extensions.swift */; };
|
||||
@@ -369,6 +370,7 @@
|
||||
7A19DAE52552C326001B192F /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
97B5592A24FD84E000D3C4FF /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
98BF5451254DF04C004E9DF5 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
9A045EB62594F8D100ED58F2 /* Dashboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = "<group>"; };
|
||||
9A0C82D124460DFF00FAE3D4 /* updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = updater.swift; sourceTree = "<group>"; };
|
||||
9A0C82D324460E4400FAE3D4 /* launchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = launchAtLogin.swift; sourceTree = "<group>"; };
|
||||
9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StatsKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -713,6 +715,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9A81C74C24499C7000825D92 /* Settings.swift */,
|
||||
9A045EB62594F8D100ED58F2 /* Dashboard.swift */,
|
||||
9A81C74B24499C7000825D92 /* AppSettings.swift */,
|
||||
9A9EA9442476D34500E3B883 /* Update.swift */,
|
||||
);
|
||||
@@ -1427,6 +1430,7 @@
|
||||
9AABEB7E243FDEF100668CB0 /* main.swift in Sources */,
|
||||
9AABEB7A243FD26200668CB0 /* AppDelegate.swift in Sources */,
|
||||
9A9EA9452476D34500E3B883 /* Update.swift in Sources */,
|
||||
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */,
|
||||
9A81C74E24499C7000825D92 /* Settings.swift in Sources */,
|
||||
9A81C74D24499C7000825D92 /* AppSettings.swift in Sources */,
|
||||
9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */,
|
||||
|
||||
@@ -21,7 +21,6 @@ import Fans
|
||||
|
||||
var store: Store = Store()
|
||||
let updater = macAppUpdater(user: "exelban", repo: "stats")
|
||||
let systemKit: SystemKit = SystemKit()
|
||||
var smc: SMCService = SMCService()
|
||||
var modules: [Module] = [
|
||||
Battery(&store),
|
||||
|
||||
26
Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json
vendored
Normal file
26
Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "baseline_settings_white_24pt_1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_settings_white_24pt_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_settings_white_24pt_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_1x.png
vendored
Normal file
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_1x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 345 B |
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_2x.png
vendored
Normal file
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 554 B |
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_3x.png
vendored
Normal file
BIN
Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 788 B |
@@ -52,6 +52,10 @@
|
||||
"Show icon in dock" = "Show icon in dock";
|
||||
"Start at login" = "Start at login";
|
||||
|
||||
// Dashboard
|
||||
"Serial number" = "Serial number";
|
||||
"Uptime" = "Uptime";
|
||||
|
||||
// Update
|
||||
"The latest version of Stats installed" = "The latest version of Stats installed";
|
||||
"Downloading..." = "Downloading...";
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
"Update application" = "Aktualizuj aplikacje";
|
||||
"Check for updates" = "Sprawdzaj aktualizacje";
|
||||
"Check for update" = "Sprawdź aktualizacje";
|
||||
"Show icon in dock" = "Pokaż ikonę w docku";
|
||||
"Start at login" = "Uruchom przy logowaniu";
|
||||
"Show icon in dock" = "Pokazuj ikonę w docku";
|
||||
"Start at login" = "Uruchamiać przy logowaniu";
|
||||
|
||||
// Update
|
||||
"The latest version of Stats installed" = "Najnowsza wersja Stats zainstalowana";
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"Check for updates" = "Перевіряти оновленя";
|
||||
"Check for update" = "Перевірити оновленя";
|
||||
"Show icon in dock" = "Показувати іконку в dock";
|
||||
"Start at login" = "Запуск при логуванні";
|
||||
"Start at login" = "Запускати при логуванні";
|
||||
|
||||
// Update
|
||||
"The latest version of Stats installed" = "Встановлено останню версію";
|
||||
|
||||
@@ -13,11 +13,7 @@ import Cocoa
|
||||
import StatsKit
|
||||
import os.log
|
||||
|
||||
class ApplicationSettings: NSView {
|
||||
private let width: CGFloat = 540
|
||||
private let height: CGFloat = 480
|
||||
private let deviceInfoHeight: CGFloat = 300
|
||||
|
||||
class ApplicationSettings: NSScrollView {
|
||||
private var updateIntervalValue: AppUpdateInterval {
|
||||
get {
|
||||
return store.string(key: "update-interval", defaultValue: AppUpdateIntervals.atStart.rawValue)
|
||||
@@ -37,255 +33,64 @@ class ApplicationSettings: NSView {
|
||||
private let updateWindow: UpdateWindow = UpdateWindow()
|
||||
|
||||
init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: width, height: height))
|
||||
self.wantsLayer = true
|
||||
self.layer?.backgroundColor = .clear
|
||||
super.init(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 540,
|
||||
height: 480
|
||||
))
|
||||
|
||||
self.addDeviceInfo()
|
||||
self.addSettings()
|
||||
self.drawsBackground = false
|
||||
self.translatesAutoresizingMaskIntoConstraints = true
|
||||
self.borderType = .noBorder
|
||||
self.hasVerticalScroller = true
|
||||
self.hasHorizontalScroller = false
|
||||
self.autohidesScrollers = true
|
||||
self.horizontalScrollElasticity = .none
|
||||
|
||||
let versionsView = self.versions()
|
||||
let settingsView = self.settings()
|
||||
|
||||
let grid: NSGridView = NSGridView(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.frame.width,
|
||||
height: versionsView.frame.height + settingsView.frame.height
|
||||
))
|
||||
grid.rowSpacing = 0
|
||||
grid.yPlacement = .fill
|
||||
|
||||
let separator = NSBox()
|
||||
separator.boxType = .separator
|
||||
|
||||
grid.addRow(with: [versionsView])
|
||||
grid.addRow(with: [separator])
|
||||
grid.addRow(with: [settingsView])
|
||||
|
||||
grid.row(at: 0).height = versionsView.frame.height
|
||||
grid.row(at: 2).height = settingsView.frame.height
|
||||
|
||||
self.documentView = grid
|
||||
self.scroll(NSPoint(x: 0, y: grid.frame.size.height))
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func viewDidMoveToWindow() {
|
||||
if let button = self.updateButton, let version = updater.latest {
|
||||
if version.newest {
|
||||
button.title = LocalizedString("Update application")
|
||||
} else {
|
||||
button.title = LocalizedString("Check for update")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addSettings() {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 1, width: self.width-1, height: self.height - self.deviceInfoHeight))
|
||||
let rowHeight: CGFloat = 40
|
||||
let rowHorizontalPadding: CGFloat = 16
|
||||
private func versions() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 280))
|
||||
|
||||
let leftPanel: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width/2, height: view.frame.height))
|
||||
leftPanel.wantsLayer = true
|
||||
let h: CGFloat = 120+60+18
|
||||
let container: NSGridView = NSGridView(frame: NSRect(x: 0, y: (view.frame.height-h)/2, width: self.frame.width, height: h))
|
||||
container.rowSpacing = 0
|
||||
container.yPlacement = .center
|
||||
container.xPlacement = .center
|
||||
|
||||
var processorInfo = ""
|
||||
if systemKit.device.info?.cpu?.name != "" {
|
||||
processorInfo += "\(systemKit.device.info?.cpu?.name ?? LocalizedString("Unknown"))\n"
|
||||
}
|
||||
processorInfo += "\(systemKit.device.info?.cpu?.physicalCores ?? 0) cores (\(systemKit.device.info?.cpu?.logicalCores ?? 0) threads)"
|
||||
leftPanel.addSubview(makeInfoRow(
|
||||
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*3, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight+8),
|
||||
title: LocalizedString("Processor"),
|
||||
value: processorInfo
|
||||
))
|
||||
let iconView: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!)
|
||||
iconView.frame = NSRect(x: (view.frame.width - 50)/2, y: 0, width: 50, height: 50)
|
||||
|
||||
let sizeFormatter = ByteCountFormatter()
|
||||
sizeFormatter.allowedUnits = [.useGB]
|
||||
sizeFormatter.countStyle = .memory
|
||||
leftPanel.addSubview(makeInfoRow(
|
||||
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*2, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Memory"),
|
||||
value: "\(sizeFormatter.string(fromByteCount: Int64(systemKit.device.info?.ram?.total ?? 0)))"
|
||||
))
|
||||
|
||||
let gpus = systemKit.device.info?.gpu
|
||||
var gpu: String = LocalizedString("Unknown")
|
||||
if gpus != nil {
|
||||
if gpus?.count == 1 {
|
||||
gpu = gpus![0].name
|
||||
} else {
|
||||
gpu = ""
|
||||
gpus!.forEach{ gpu += "\($0.name)\n" }
|
||||
}
|
||||
}
|
||||
leftPanel.addSubview(makeInfoRow(
|
||||
frame: NSRect(x: rowHorizontalPadding, y: rowHeight*1, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Graphics"),
|
||||
value: gpu
|
||||
))
|
||||
|
||||
leftPanel.addSubview(makeInfoRow(
|
||||
frame: NSRect(x: rowHorizontalPadding, y: 0, width: leftPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Disk"),
|
||||
value: "\(systemKit.device.info?.disk?.model ?? systemKit.device.info?.disk?.name ?? LocalizedString("Unknown"))"
|
||||
))
|
||||
|
||||
let rightPanel: NSView = NSView(frame: NSRect(x: self.width/2, y: 0, width: view.frame.width/2, height: view.frame.height))
|
||||
|
||||
rightPanel.addSubview(makeSelectRow(
|
||||
frame: NSRect(x: rowHorizontalPadding*0.5, y: rowHeight*3, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Check for updates"),
|
||||
action: #selector(self.toggleUpdateInterval),
|
||||
items: AppUpdateIntervals.allCases.map{ $0.rawValue },
|
||||
selected: self.updateIntervalValue
|
||||
))
|
||||
|
||||
let temperature = SelectRow(
|
||||
frame: NSRect(
|
||||
x: rowHorizontalPadding*0.5,
|
||||
y: rowHeight*2,
|
||||
width: rightPanel.frame.width - (rowHorizontalPadding*1.5),
|
||||
height: rowHeight
|
||||
),
|
||||
title: LocalizedString("Temperature"),
|
||||
action: #selector(toggleTemperatureUnits),
|
||||
items: TemperatureUnits,
|
||||
selected: self.temperatureUnitsValue
|
||||
)
|
||||
temperature.subviews.forEach { (v: NSView) in
|
||||
if let view = v as? LabelField {
|
||||
view.textColor = .secondaryLabelColor
|
||||
}
|
||||
}
|
||||
rightPanel.addSubview(temperature)
|
||||
|
||||
rightPanel.addSubview(makeSettingRow(
|
||||
frame: NSRect(x: rowHorizontalPadding*0.5, y: rowHeight*1, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Show icon in dock"),
|
||||
action: #selector(self.toggleDock),
|
||||
state: store.bool(key: "dockIcon", defaultValue: false)
|
||||
))
|
||||
|
||||
rightPanel.addSubview(makeSettingRow(
|
||||
frame: NSRect(x: rowHorizontalPadding*0.5, y: 0, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight),
|
||||
title: LocalizedString("Start at login"),
|
||||
action: #selector(self.toggleLaunchAtLogin),
|
||||
state: LaunchAtLogin.isEnabled
|
||||
))
|
||||
|
||||
view.addSubview(leftPanel)
|
||||
view.addSubview(rightPanel)
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
func makeSelectRow(frame: NSRect, title: String, action: Selector, items: [String], selected: String) -> NSView {
|
||||
let row: NSView = NSView(frame: frame)
|
||||
|
||||
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (row.frame.height - 32)/2, width: row.frame.width - 52, height: 32), title)
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .secondaryLabelColor
|
||||
|
||||
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-28)/2, width: 50, height: 28))
|
||||
select.target = self
|
||||
select.action = action
|
||||
|
||||
let menu = NSMenu()
|
||||
items.forEach { (color: String) in
|
||||
if color.contains("separator") {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
} else {
|
||||
let interfaceMenu = NSMenuItem(title: color, action: nil, keyEquivalent: "")
|
||||
menu.addItem(interfaceMenu)
|
||||
if selected == color {
|
||||
interfaceMenu.state = .on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.menu = menu
|
||||
select.sizeToFit()
|
||||
|
||||
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
|
||||
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
|
||||
|
||||
row.addSubview(select)
|
||||
row.addSubview(rowTitle)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private func makeInfoRow(frame: NSRect, title: String, value: String) -> NSView {
|
||||
let row: NSView = NSView(frame: frame)
|
||||
let titleWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 10
|
||||
|
||||
let rowTitle: NSTextField = TextView(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: titleWidth, height: 17))
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .secondaryLabelColor
|
||||
rowTitle.stringValue = title
|
||||
|
||||
let rowValue: NSTextField = TextView(frame: NSRect(x: titleWidth, y: (row.frame.height - 16)/2, width: row.frame.width - titleWidth, height: 17))
|
||||
rowValue.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowValue.alignment = .right
|
||||
rowValue.stringValue = value
|
||||
rowValue.isSelectable = true
|
||||
|
||||
if value.contains("\n") {
|
||||
rowValue.frame = NSRect(x: titleWidth, y: 0, width: rowValue.frame.width, height: row.frame.height)
|
||||
}
|
||||
|
||||
row.addSubview(rowTitle)
|
||||
row.addSubview(rowValue)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private func makeSettingRow(frame: NSRect, title: String, action: Selector, state: Bool) -> NSView {
|
||||
let row: NSView = NSView(frame: frame)
|
||||
let state: NSControl.StateValue = state ? .on : .off
|
||||
|
||||
let rowTitle: NSTextField = TextView(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: row.frame.width - 52, height: 17))
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .secondaryLabelColor
|
||||
rowTitle.stringValue = title
|
||||
|
||||
var toggle: NSControl = NSControl()
|
||||
if #available(OSX 10.15, *) {
|
||||
let switchButton = NSSwitch(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height))
|
||||
switchButton.state = state
|
||||
switchButton.action = action
|
||||
switchButton.target = self
|
||||
|
||||
toggle = switchButton
|
||||
} else {
|
||||
let button: NSButton = NSButton(frame: NSRect(x: row.frame.width - 30, y: 0, width: 30, height: row.frame.height))
|
||||
button.setButtonType(.switch)
|
||||
button.state = state
|
||||
button.title = ""
|
||||
button.action = action
|
||||
button.isBordered = false
|
||||
button.isTransparent = true
|
||||
button.target = self
|
||||
|
||||
toggle = button
|
||||
}
|
||||
|
||||
row.addSubview(toggle)
|
||||
row.addSubview(rowTitle)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private func addDeviceInfo() {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.height - self.deviceInfoHeight, width: self.width, height: self.deviceInfoHeight))
|
||||
let leftPanel: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.width/2, height: self.deviceInfoHeight))
|
||||
|
||||
let deviceImageView: NSImageView = NSImageView(image: systemKit.device.model.icon)
|
||||
deviceImageView.frame = NSRect(x: (leftPanel.frame.width - 160)/2, y: ((self.deviceInfoHeight - 120)/2) + 22, width: 160, height: 120)
|
||||
|
||||
let deviceNameField: NSTextField = TextView(frame: NSRect(x: 0, y: 72, width: leftPanel.frame.width, height: 20))
|
||||
deviceNameField.alignment = .center
|
||||
deviceNameField.font = NSFont.systemFont(ofSize: 14, weight: .regular)
|
||||
deviceNameField.stringValue = systemKit.device.model.name
|
||||
deviceNameField.isSelectable = true
|
||||
deviceNameField.toolTip = systemKit.device.modelIdentifier
|
||||
|
||||
let osField: NSTextField = TextView(frame: NSRect(x: 0, y: 52, width: leftPanel.frame.width, height: 18))
|
||||
osField.alignment = .center
|
||||
osField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
osField.stringValue = "macOS \(systemKit.device.os?.name ?? LocalizedString("Unknown")) (\(systemKit.device.os?.version.getFullVersion() ?? ""))"
|
||||
osField.isSelectable = true
|
||||
|
||||
leftPanel.addSubview(deviceImageView)
|
||||
leftPanel.addSubview(deviceNameField)
|
||||
leftPanel.addSubview(osField)
|
||||
|
||||
let rightPanel: NSView = NSView(frame: NSRect(x: self.width/2, y: 0, width: self.width/2, height: self.deviceInfoHeight))
|
||||
|
||||
let iconView: NSImageView = NSImageView(frame: NSRect(x: (leftPanel.frame.width - 100)/2, y: ((self.deviceInfoHeight - 100)/2) + 32, width: 100, height: 100))
|
||||
iconView.image = NSImage(named: NSImage.Name("AppIcon"))!
|
||||
|
||||
let infoView: NSView = NSView(frame: NSRect(x: 0, y: 54, width: self.width/2, height: 42))
|
||||
|
||||
let statsName: NSTextField = TextView(frame: NSRect(x: 0, y: 20, width: leftPanel.frame.width, height: 22))
|
||||
let statsName: NSTextField = TextView(frame: NSRect(x: 0, y: 20, width: view.frame.width, height: 22))
|
||||
statsName.alignment = .center
|
||||
statsName.font = NSFont.systemFont(ofSize: 20, weight: .regular)
|
||||
statsName.stringValue = "Stats"
|
||||
@@ -294,31 +99,137 @@ class ApplicationSettings: NSView {
|
||||
let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
||||
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
|
||||
|
||||
let statsVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: leftPanel.frame.width, height: 16))
|
||||
let statsVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 16))
|
||||
statsVersion.alignment = .center
|
||||
statsVersion.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
statsVersion.stringValue = "\(LocalizedString("Version")) \(versionNumber)"
|
||||
statsVersion.isSelectable = true
|
||||
statsVersion.toolTip = "Build number: \(buildNumber)"
|
||||
|
||||
infoView.addSubview(statsName)
|
||||
infoView.addSubview(statsVersion)
|
||||
|
||||
let button: NSButton = NSButton(frame: NSRect(x: (rightPanel.frame.width - 160)/2, y: 20, width: 160, height: 28))
|
||||
let button: NSButton = NSButton(frame: NSRect(x: (view.frame.width - 160)/2, y: 0, width: 160, height: 30))
|
||||
button.title = LocalizedString("Check for update")
|
||||
button.bezelStyle = .rounded
|
||||
button.target = self
|
||||
button.action = #selector(updateAction)
|
||||
self.updateButton = button
|
||||
|
||||
rightPanel.addSubview(iconView)
|
||||
rightPanel.addSubview(infoView)
|
||||
rightPanel.addSubview(button)
|
||||
container.addRow(with: [iconView])
|
||||
container.addRow(with: [statsName])
|
||||
container.addRow(with: [statsVersion])
|
||||
container.addRow(with: [button])
|
||||
|
||||
view.addSubview(leftPanel)
|
||||
view.addSubview(rightPanel)
|
||||
container.column(at: 0).width = self.frame.width
|
||||
container.row(at: 1).height = 22
|
||||
container.row(at: 2).height = 20
|
||||
container.row(at: 3).height = 30
|
||||
|
||||
self.addSubview(view)
|
||||
view.addSubview(container)
|
||||
return view
|
||||
}
|
||||
|
||||
private func settings() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
||||
|
||||
let grid: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 0))
|
||||
grid.rowSpacing = 10
|
||||
grid.columnSpacing = 20
|
||||
grid.xPlacement = .trailing
|
||||
grid.rowAlignment = .firstBaseline
|
||||
grid.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let separator = NSBox()
|
||||
separator.boxType = .separator
|
||||
|
||||
grid.addRow(with: self.updates())
|
||||
grid.addRow(with: self.temperature())
|
||||
grid.addRow(with: self.dockIcon())
|
||||
grid.addRow(with: self.startAtLogin())
|
||||
|
||||
view.addSubview(grid)
|
||||
|
||||
var height: CGFloat = grid.rowSpacing*2
|
||||
for i in 0..<grid.numberOfRows {
|
||||
let row = grid.row(at: i)
|
||||
for a in 0..<row.numberOfCells {
|
||||
height += row.cell(at: a).contentView?.frame.height ?? 0
|
||||
}
|
||||
}
|
||||
view.setFrameSize(NSSize(width: view.frame.width, height: max(200, height)))
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
grid.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
grid.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
])
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
private func updates() -> [NSView] {
|
||||
return [
|
||||
self.titleView(LocalizedString("Check for updates")),
|
||||
SelectView(
|
||||
action: #selector(self.toggleUpdateInterval),
|
||||
items: AppUpdateIntervals.allCases.map{ KeyValue_t(key: $0.rawValue, value: $0.rawValue) },
|
||||
selected: self.updateIntervalValue
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func temperature() -> [NSView] {
|
||||
return [
|
||||
self.titleView(LocalizedString("Temperature")),
|
||||
SelectView(
|
||||
action: #selector(self.toggleTemperatureUnits),
|
||||
items: TemperatureUnits,
|
||||
selected: self.temperatureUnitsValue
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func dockIcon() -> [NSView] {
|
||||
return [
|
||||
self.titleView(LocalizedString("Show icon in dock")),
|
||||
self.toggleView(
|
||||
action: #selector(self.toggleDock),
|
||||
state: store.bool(key: "dockIcon", defaultValue: false)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func startAtLogin() -> [NSView] {
|
||||
return [
|
||||
self.titleView(LocalizedString("Start at login")),
|
||||
self.toggleView(
|
||||
action: #selector(self.toggleLaunchAtLogin),
|
||||
state: LaunchAtLogin.isEnabled
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - helpers
|
||||
|
||||
private func titleView(_ value: String) -> NSTextField {
|
||||
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 120, height: 17))
|
||||
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
field.textColor = .secondaryLabelColor
|
||||
field.stringValue = value
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
private func toggleView(action: Selector, state: Bool) -> NSView {
|
||||
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 10))
|
||||
button.setButtonType(.switch)
|
||||
button.state = state ? .on : .off
|
||||
button.title = ""
|
||||
button.action = action
|
||||
button.isBordered = false
|
||||
button.isTransparent = true
|
||||
button.target = self
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
@objc func updateAction(_ sender: NSObject) {
|
||||
@@ -354,33 +265,18 @@ class ApplicationSettings: NSView {
|
||||
self.temperatureUnitsValue = key
|
||||
}
|
||||
|
||||
@objc func toggleDock(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
@objc func toggleDock(_ sender: NSButton) {
|
||||
store.set(key: "dockIcon", value: sender.state == NSControl.StateValue.on)
|
||||
NSApp.setActivationPolicy(sender.state == .on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory)
|
||||
|
||||
if state != nil {
|
||||
store.set(key: "dockIcon", value: state! == NSControl.StateValue.on)
|
||||
}
|
||||
let dockIconStatus = state == NSControl.StateValue.on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
|
||||
NSApp.setActivationPolicy(dockIconStatus)
|
||||
if state == .off {
|
||||
if sender.state == .off {
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggleLaunchAtLogin(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
@objc func toggleLaunchAtLogin(_ sender: NSButton) {
|
||||
LaunchAtLogin.isEnabled = sender.state == .on
|
||||
|
||||
LaunchAtLogin.isEnabled = state! == NSControl.StateValue.on
|
||||
if !store.exist(key: "runAtLoginInitialized") {
|
||||
store.set(key: "runAtLoginInitialized", value: true)
|
||||
}
|
||||
|
||||
332
Stats/Views/Dashboard.swift
Normal file
332
Stats/Views/Dashboard.swift
Normal file
@@ -0,0 +1,332 @@
|
||||
//
|
||||
// Stats.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 24/12/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
import os.log
|
||||
|
||||
class Dashboard: NSScrollView {
|
||||
private var uptimeField: NSTextField? = nil
|
||||
|
||||
init() {
|
||||
super.init(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 540,
|
||||
height: 480
|
||||
))
|
||||
|
||||
self.drawsBackground = false
|
||||
self.translatesAutoresizingMaskIntoConstraints = true
|
||||
self.borderType = .noBorder
|
||||
self.hasVerticalScroller = true
|
||||
self.hasHorizontalScroller = false
|
||||
self.autohidesScrollers = true
|
||||
self.horizontalScrollElasticity = .none
|
||||
|
||||
let versionsView = self.versions()
|
||||
let specsView = self.specs()
|
||||
|
||||
let grid: NSGridView = NSGridView(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.frame.width,
|
||||
height: versionsView.frame.height + specsView.frame.height
|
||||
))
|
||||
grid.rowSpacing = 0
|
||||
grid.yPlacement = .fill
|
||||
|
||||
let separator = NSBox()
|
||||
separator.boxType = .separator
|
||||
|
||||
grid.addRow(with: [versionsView])
|
||||
grid.addRow(with: [separator])
|
||||
grid.addRow(with: [specsView])
|
||||
|
||||
grid.row(at: 0).height = versionsView.frame.height
|
||||
grid.row(at: 2).height = specsView.frame.height
|
||||
|
||||
self.documentView = grid
|
||||
self.scroll(NSPoint(x: 0, y: grid.frame.size.height))
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(windowOpens), name: .openModuleSettings, object: nil)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func versions() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 280))
|
||||
|
||||
let h: CGFloat = 120+60+18
|
||||
let container: NSGridView = NSGridView(frame: NSRect(x: 0, y: (view.frame.height-h)/2, width: self.frame.width, height: h))
|
||||
container.rowSpacing = 0
|
||||
container.yPlacement = .center
|
||||
container.xPlacement = .center
|
||||
|
||||
let deviceImageView: NSImageView = NSImageView(image: SystemKit.shared.device.model.icon)
|
||||
deviceImageView.frame = NSRect(x: (view.frame.width - 160)/2, y: 0, width: 160, height: 120)
|
||||
|
||||
let deviceNameField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
|
||||
deviceNameField.alignment = .center
|
||||
deviceNameField.font = NSFont.systemFont(ofSize: 14, weight: .regular)
|
||||
deviceNameField.stringValue = SystemKit.shared.device.model.name
|
||||
deviceNameField.isSelectable = true
|
||||
deviceNameField.toolTip = SystemKit.shared.device.modelIdentifier
|
||||
|
||||
let osField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 18))
|
||||
osField.alignment = .center
|
||||
osField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
||||
osField.stringValue = "macOS \(SystemKit.shared.device.os?.name ?? LocalizedString("Unknown")) (\(SystemKit.shared.device.os?.version.getFullVersion() ?? ""))"
|
||||
osField.isSelectable = true
|
||||
|
||||
container.addRow(with: [deviceImageView])
|
||||
container.addRow(with: [deviceNameField])
|
||||
container.addRow(with: [osField])
|
||||
|
||||
container.column(at: 0).width = self.frame.width
|
||||
container.row(at: 1).height = 22
|
||||
container.row(at: 2).height = 20
|
||||
|
||||
view.addSubview(container)
|
||||
return view
|
||||
}
|
||||
|
||||
private func specs() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
||||
let grid: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 0))
|
||||
grid.rowSpacing = 10
|
||||
grid.columnSpacing = 20
|
||||
grid.xPlacement = .trailing
|
||||
grid.rowAlignment = .firstBaseline
|
||||
grid.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let separator = NSBox()
|
||||
separator.boxType = .separator
|
||||
|
||||
grid.addRow(with: self.processor())
|
||||
grid.addRow(with: self.ram())
|
||||
grid.addRow(with: self.gpu())
|
||||
grid.addRow(with: self.disk())
|
||||
grid.addRow(with: self.serialNumber())
|
||||
|
||||
grid.addRow(with: [separator])
|
||||
grid.row(at: 5).mergeCells(in: NSRange(location: 0, length: 2))
|
||||
grid.row(at: 5).topPadding = 5
|
||||
grid.row(at: 5).bottomPadding = 5
|
||||
|
||||
grid.addRow(with: self.upTime())
|
||||
|
||||
view.addSubview(grid)
|
||||
|
||||
var height: CGFloat = grid.rowSpacing*2
|
||||
for i in 0..<grid.numberOfRows {
|
||||
let row = grid.row(at: i)
|
||||
for a in 0..<row.numberOfCells {
|
||||
height += row.cell(at: a).contentView?.frame.height ?? 0
|
||||
}
|
||||
}
|
||||
view.setFrameSize(NSSize(width: view.frame.width, height: height))
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
grid.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
grid.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
])
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
@objc private func windowOpens(_ notification: Notification) {
|
||||
guard notification.userInfo?["module"] as? String == "Stats" else {
|
||||
return
|
||||
}
|
||||
|
||||
let form = DateComponentsFormatter()
|
||||
form.maximumUnitCount = 2
|
||||
form.unitsStyle = .full
|
||||
form.allowedUnits = [.day, .hour, .minute]
|
||||
if let bootDate = SystemKit.shared.device.bootDate {
|
||||
if let duration = form.string(from: bootDate, to: Date()) {
|
||||
self.uptimeField?.stringValue = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
private func processor() -> [NSView] {
|
||||
var value = ""
|
||||
|
||||
if let cpu = SystemKit.shared.device.info.cpu, cpu.name != nil || cpu.physicalCores != nil || cpu.logicalCores != nil {
|
||||
if let name = cpu.name {
|
||||
value += name
|
||||
}
|
||||
|
||||
if cpu.physicalCores != nil || cpu.logicalCores != nil {
|
||||
if value.count != 0 {
|
||||
value += "\n"
|
||||
}
|
||||
|
||||
var mini = ""
|
||||
if let cores = cpu.physicalCores {
|
||||
mini += "\(cores) cores"
|
||||
}
|
||||
if let threads = cpu.logicalCores {
|
||||
if mini != "" {
|
||||
mini += ", "
|
||||
}
|
||||
mini += "\(threads) threads"
|
||||
}
|
||||
value += "\(mini)"
|
||||
}
|
||||
} else {
|
||||
value = LocalizedString("Unknown")
|
||||
}
|
||||
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Processor")):"),
|
||||
self.valueView(value),
|
||||
]
|
||||
}
|
||||
|
||||
private func ram() -> [NSView] {
|
||||
let sizeFormatter = ByteCountFormatter()
|
||||
sizeFormatter.allowedUnits = [.useGB]
|
||||
sizeFormatter.countStyle = .memory
|
||||
|
||||
var value = ""
|
||||
if let dimms = SystemKit.shared.device.info.ram?.dimms {
|
||||
for i in 0..<dimms.count {
|
||||
let dimm = dimms[i]
|
||||
var row = ""
|
||||
|
||||
if let size = dimm.size {
|
||||
row += size
|
||||
}
|
||||
|
||||
if let speed = dimm.speed {
|
||||
if row.count != 0 && row.last != " " {
|
||||
row += " "
|
||||
}
|
||||
row += speed
|
||||
}
|
||||
|
||||
if let type = dimm.type {
|
||||
if row.count != 0 && row.last != " " {
|
||||
row += " "
|
||||
}
|
||||
row += type
|
||||
}
|
||||
|
||||
if dimm.bank != nil || dimm.channel != nil {
|
||||
if row.count != 0 && row.last != " " {
|
||||
row += " "
|
||||
}
|
||||
|
||||
var mini = "("
|
||||
if let bank = dimm.bank {
|
||||
mini += "slot \(bank)"
|
||||
}
|
||||
if let ch = dimm.channel {
|
||||
mini += "\(mini == "(" ? "" : "/")ch \(ch)"
|
||||
}
|
||||
row += "\(mini))"
|
||||
}
|
||||
|
||||
value += "\(row)\(i == dimms.count-1 ? "" : "\n")"
|
||||
}
|
||||
} else {
|
||||
value = LocalizedString("Unknown")
|
||||
}
|
||||
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Memory")):"),
|
||||
self.valueView("\(value)"),
|
||||
]
|
||||
}
|
||||
|
||||
private func gpu() -> [NSView] {
|
||||
let gpus = SystemKit.shared.device.info.gpu
|
||||
var gpu: String = LocalizedString("Unknown")
|
||||
if gpus != nil {
|
||||
if gpus?.count == 1 {
|
||||
gpu = gpus![0].name
|
||||
} else {
|
||||
gpu = ""
|
||||
gpus!.forEach{ gpu += "\($0.name)\n" }
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Graphics")):"),
|
||||
self.valueView(gpu),
|
||||
]
|
||||
}
|
||||
|
||||
private func disk() -> [NSView] {
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Disk")):"),
|
||||
self.valueView("\(SystemKit.shared.device.info.disk?.model ?? SystemKit.shared.device.info.disk?.name ?? LocalizedString("Unknown"))"),
|
||||
]
|
||||
}
|
||||
|
||||
private func serialNumber() -> [NSView] {
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Serial number")):"),
|
||||
self.valueView("\(SystemKit.shared.device.serialNumber ?? LocalizedString("Unknown"))"),
|
||||
]
|
||||
}
|
||||
|
||||
private func upTime() -> [NSView] {
|
||||
let form = DateComponentsFormatter()
|
||||
form.maximumUnitCount = 2
|
||||
form.unitsStyle = .full
|
||||
form.allowedUnits = [.day, .hour, .minute]
|
||||
|
||||
var value = LocalizedString("Unknown")
|
||||
if let bootDate = SystemKit.shared.device.bootDate {
|
||||
if let duration = form.string(from: bootDate, to: Date()) {
|
||||
value = duration
|
||||
}
|
||||
}
|
||||
|
||||
let valueView = self.valueView(value)
|
||||
self.uptimeField = valueView
|
||||
|
||||
return [
|
||||
self.titleView("\(LocalizedString("Uptime")):"),
|
||||
valueView,
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func titleView(_ value: String) -> NSTextField {
|
||||
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 120, height: 17))
|
||||
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
||||
field.textColor = .labelColor
|
||||
field.stringValue = value
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
private func valueView(_ value: String) -> NSTextField {
|
||||
let field: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 17))
|
||||
field.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
field.alignment = .right
|
||||
field.stringValue = value
|
||||
field.isSelectable = true
|
||||
|
||||
return field
|
||||
}
|
||||
}
|
||||
@@ -114,13 +114,14 @@ private class SettingsView: NSView {
|
||||
private var navigationView: NSView = NSView()
|
||||
private var mainView: NSView = NSView()
|
||||
|
||||
private var applicationSettings: NSView = ApplicationSettings()
|
||||
private var dashboard: NSView = Dashboard()
|
||||
private var settings: NSView = ApplicationSettings()
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
|
||||
self.wantsLayer = true
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(menuCallback), name: .openSettingsView, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(menuCallback), name: .openModuleSettings, object: nil)
|
||||
|
||||
let sidebar = NSVisualEffectView(frame: NSMakeRect(0, 0, self.sidebarWidth, self.frame.height))
|
||||
sidebar.material = .sidebar
|
||||
@@ -135,12 +136,12 @@ private class SettingsView: NSView {
|
||||
)
|
||||
self.menuView.wantsLayer = true
|
||||
self.menuView.drawsBackground = false
|
||||
self.menuView.addSubview(MenuView(n: 0, icon: NSImage(named: NSImage.Name("apps"))!, title: "Stats"))
|
||||
self.menuView.addSubview(MenuView(n: 0, icon: NSImage(named: NSImage.Name("apps"))!, title: "Dashboard"))
|
||||
|
||||
self.navigationView.frame = NSRect(x: 0, y: 0, width: self.sidebarWidth, height: navigationHeight)
|
||||
self.navigationView.wantsLayer = true
|
||||
|
||||
self.navigationView.addSubview(self.makeButton(4, title: LocalizedString("Open Activity Monitor"), image: "chart", action: #selector(openActivityMonitor)))
|
||||
self.navigationView.addSubview(self.makeButton(4, title: LocalizedString("Open application settings"), image: "settings", action: #selector(openSettings)))
|
||||
self.navigationView.addSubview(self.makeButton(3, title: LocalizedString("Report a bug"), image: "bug", action: #selector(reportBug)))
|
||||
self.navigationView.addSubview(self.makeButton(2, title: LocalizedString("Support app"), image: "donate", action: #selector(donate)))
|
||||
self.navigationView.addSubview(self.makeButton(1, title: LocalizedString("Close application"), image: "power", action: #selector(closeApp)))
|
||||
@@ -160,7 +161,7 @@ private class SettingsView: NSView {
|
||||
self.addSubview(self.navigationView)
|
||||
self.addSubview(self.mainView)
|
||||
|
||||
self.openMenu("Stats")
|
||||
self.openMenu("Dashboard")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -197,18 +198,20 @@ private class SettingsView: NSView {
|
||||
self.menuView.addSubview(menu)
|
||||
}
|
||||
self.modules = list
|
||||
// self.openMenu("CPU")
|
||||
}
|
||||
|
||||
@objc private func menuCallback(_ notification: Notification) {
|
||||
if let title = notification.userInfo?["module"] as? String {
|
||||
var view: NSView = self.applicationSettings
|
||||
var view: NSView = NSView()
|
||||
|
||||
let detectedModule = self.modules?.pointee.first{ $0.config.name == title }
|
||||
if detectedModule != nil {
|
||||
if let v = detectedModule?.settings {
|
||||
if let detectedModule = self.modules?.pointee.first(where: { $0.config.name == title }) {
|
||||
if let v = detectedModule.settings {
|
||||
view = v
|
||||
}
|
||||
} else if title == "Dashboard" {
|
||||
view = self.dashboard
|
||||
} else if title == "settings" {
|
||||
view = self.settings
|
||||
}
|
||||
|
||||
self.mainView.subviews.forEach{ $0.removeFromSuperview() }
|
||||
@@ -248,14 +251,8 @@ private class SettingsView: NSView {
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func openActivityMonitor(_ sender: Any) {
|
||||
NSWorkspace.shared.launchApplication(
|
||||
withBundleIdentifier: "com.apple.ActivityMonitor",
|
||||
options: [.default],
|
||||
additionalEventParamDescriptor: nil,
|
||||
launchIdentifier: nil
|
||||
)
|
||||
self.window?.setIsVisible(false)
|
||||
@objc private func openSettings(_ sender: Any) {
|
||||
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": "settings"])
|
||||
}
|
||||
|
||||
@objc private func reportBug(_ sender: Any) {
|
||||
@@ -322,7 +319,7 @@ private class MenuView: NSView {
|
||||
}
|
||||
|
||||
public func activate() {
|
||||
NotificationCenter.default.post(name: .openSettingsView, object: nil, userInfo: ["module": self.title])
|
||||
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": self.title])
|
||||
self.layer?.backgroundColor = .init(gray: 0.1, alpha: 0.4)
|
||||
self.active = true
|
||||
}
|
||||
|
||||
@@ -37,18 +37,21 @@ public struct os_s {
|
||||
}
|
||||
|
||||
public struct cpu_s {
|
||||
public let physicalCores: Int8
|
||||
public let logicalCores: Int8
|
||||
public let name: String
|
||||
public var name: String? = nil
|
||||
public var physicalCores: Int8? = nil
|
||||
public var logicalCores: Int8? = nil
|
||||
}
|
||||
|
||||
public struct dimm_s {
|
||||
public var bank: Int? = nil
|
||||
public var channel: String? = nil
|
||||
public var type: String? = nil
|
||||
public var size: String? = nil
|
||||
public var speed: String? = nil
|
||||
}
|
||||
|
||||
public struct ram_s {
|
||||
public var active: Double
|
||||
public var inactive: Double
|
||||
public var wired: Double
|
||||
public var compressed: Double
|
||||
public var total: Double
|
||||
public var used: Double
|
||||
public var dimms: [dimm_s] = []
|
||||
}
|
||||
|
||||
public struct gpu_s {
|
||||
@@ -69,13 +72,18 @@ public struct info_s {
|
||||
}
|
||||
|
||||
public struct device_s {
|
||||
public var model: model_s = model_s(name: LocalizedString("Unknown"), year: 2020, type: .unknown)
|
||||
public var model: model_s = model_s(name: LocalizedString("Unknown"), year: Calendar.current.component(.year, from: Date()), type: .unknown)
|
||||
public var modelIdentifier: String? = nil
|
||||
public var serialNumber: String? = nil
|
||||
public var bootDate: Date? = nil
|
||||
|
||||
public var os: os_s? = nil
|
||||
public var info: info_s? = info_s()
|
||||
public var info: info_s = info_s()
|
||||
}
|
||||
|
||||
public class SystemKit {
|
||||
public static let shared = SystemKit()
|
||||
|
||||
public var device: device_s = device_s()
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SystemKit")
|
||||
|
||||
@@ -88,9 +96,15 @@ public class SystemKit {
|
||||
os_log(.error, log: self.log, "unknown device %s", modelName)
|
||||
}
|
||||
}
|
||||
if let id = self.modelID() {
|
||||
self.device.modelIdentifier = id
|
||||
|
||||
let (modelID, serialNumber) = self.modelAndSerialNumber()
|
||||
if modelID != nil {
|
||||
self.device.modelIdentifier = modelID
|
||||
}
|
||||
if serialNumber != nil {
|
||||
self.device.serialNumber = serialNumber
|
||||
}
|
||||
self.device.bootDate = self.bootDate()
|
||||
|
||||
let procInfo = ProcessInfo()
|
||||
let systemVersion = procInfo.operatingSystemVersion
|
||||
@@ -104,10 +118,10 @@ public class SystemKit {
|
||||
let version = "\(systemVersion.majorVersion).\(systemVersion.minorVersion)"
|
||||
self.device.os = os_s(name: osDict[version] ?? LocalizedString("Unknown"), version: systemVersion, build: build)
|
||||
|
||||
self.device.info?.cpu = self.getCPUInfo()
|
||||
self.device.info?.ram = self.getRamInfo()
|
||||
self.device.info?.gpu = self.getGPUInfo()
|
||||
self.device.info?.disk = self.getDiskInfo()
|
||||
self.device.info.cpu = self.getCPUInfo()
|
||||
self.device.info.ram = self.getRamInfo()
|
||||
self.device.info.gpu = self.getGPUInfo()
|
||||
self.device.info.disk = self.getDiskInfo()
|
||||
}
|
||||
|
||||
public func modelName() -> String? {
|
||||
@@ -128,18 +142,40 @@ public class SystemKit {
|
||||
return nil
|
||||
}
|
||||
|
||||
func modelID() -> String? {
|
||||
func modelAndSerialNumber() -> (String?, String?) {
|
||||
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
|
||||
|
||||
var modelIdentifier: String?
|
||||
if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data {
|
||||
modelIdentifier = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters)
|
||||
}
|
||||
|
||||
|
||||
var serialNumber: String?
|
||||
if let serialString = IORegistryEntryCreateCFProperty(service, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String {
|
||||
serialNumber = serialString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
IOObjectRelease(service)
|
||||
return modelIdentifier
|
||||
return (modelIdentifier, serialNumber)
|
||||
}
|
||||
|
||||
func bootDate() -> Date? {
|
||||
var mib = [CTL_KERN, KERN_BOOTTIME]
|
||||
var bootTime = timeval()
|
||||
var bootTimeSize = MemoryLayout<timeval>.size
|
||||
|
||||
let result = sysctl(&mib, UInt32(mib.count), &bootTime, &bootTimeSize, nil, 0)
|
||||
if result == KERN_SUCCESS {
|
||||
return Date(timeIntervalSince1970: Double(bootTime.tv_sec) + Double(bootTime.tv_usec) / 1_000_000.0)
|
||||
}
|
||||
|
||||
os_log(.error, log: self.log, "error get boot time: %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getCPUInfo() -> cpu_s? {
|
||||
var cpu = cpu_s()
|
||||
|
||||
var sizeOfName = 0
|
||||
sysctlbyname("machdep.cpu.brand_string", nil, &sizeOfName, nil, 0)
|
||||
var nameCharts = [CChar](repeating: 0, count: sizeOfName)
|
||||
@@ -150,6 +186,8 @@ public class SystemKit {
|
||||
name = name.replacingOccurrences(of: "(R)", with: "")
|
||||
name = name.replacingOccurrences(of: "CPU", with: "")
|
||||
name = name.replacingOccurrences(of: " @ ", with: "")
|
||||
|
||||
cpu.name = name
|
||||
}
|
||||
|
||||
var size = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
@@ -162,13 +200,16 @@ public class SystemKit {
|
||||
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &size)
|
||||
}
|
||||
|
||||
if result == KERN_SUCCESS {
|
||||
let data = hostInfo.move()
|
||||
return cpu_s(physicalCores: Int8(data.physical_cpu), logicalCores: Int8(data.logical_cpu), name: name)
|
||||
if result != KERN_SUCCESS {
|
||||
os_log(.error, log: self.log, "read cores number: %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return nil
|
||||
}
|
||||
|
||||
os_log(.error, log: self.log, "hostInfo.withMemoryRebound(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return nil
|
||||
let data = hostInfo.move()
|
||||
cpu.physicalCores = Int8(data.physical_cpu)
|
||||
cpu.logicalCores = Int8(data.logical_cpu)
|
||||
|
||||
return cpu
|
||||
}
|
||||
|
||||
private func getGPUInfo() -> [gpu_s]? {
|
||||
@@ -256,54 +297,71 @@ public class SystemKit {
|
||||
}
|
||||
|
||||
public func getRamInfo() -> ram_s? {
|
||||
var vmStats = host_basic_info()
|
||||
var count = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
var totalSize: Double = 0
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/sbin/system_profiler"
|
||||
task.arguments = ["SPMemoryDataType", "-json"]
|
||||
|
||||
var result: kern_return_t = withUnsafeMutablePointer(to: &vmStats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count)
|
||||
}
|
||||
}
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
if result == KERN_SUCCESS {
|
||||
totalSize = Double(vmStats.max_mem)
|
||||
} else {
|
||||
os_log(.error, log: self.log, "host_basic_info(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
os_log(.error, log: log, "system_profiler SPMemoryDataType: %s", "\(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
|
||||
var pageSize: vm_size_t = 0
|
||||
result = withUnsafeMutablePointer(to: &pageSize) { (size) -> kern_return_t in
|
||||
host_page_size(mach_host_self(), size)
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stats = vm_statistics64()
|
||||
count = UInt32(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
result = withUnsafeMutablePointer(to: &stats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
|
||||
let data = Data(output.utf8)
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
|
||||
var ram: ram_s = ram_s()
|
||||
|
||||
if let obj = json["SPMemoryDataType"] as? [[String:Any]], obj.count > 0 {
|
||||
if let items = obj[0]["_items"] as? [[String: Any]], items.count > 0 {
|
||||
for i in 0..<items.count {
|
||||
let item = items[i]
|
||||
|
||||
if item["dimm_size"] as? String == "empty" {
|
||||
continue
|
||||
}
|
||||
|
||||
var dimm: dimm_s = dimm_s()
|
||||
dimm.type = item["dimm_type"] as? String
|
||||
dimm.speed = item["dimm_speed"] as? String
|
||||
dimm.size = item["dimm_size"] as? String
|
||||
|
||||
if let nameValue = item["_name"] as? String {
|
||||
let arr = nameValue.split(separator: "/")
|
||||
if arr.indices.contains(0) {
|
||||
dimm.bank = Int(arr[0].filter("0123456789.".contains))
|
||||
}
|
||||
if arr.indices.contains(1) {
|
||||
dimm.channel = arr[1].split(separator: "-")[0].replacingOccurrences(of: "Channel", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
ram.dimms.append(dimm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ram
|
||||
}
|
||||
} catch let error as NSError {
|
||||
os_log(.error, log: self.log, "error to parse system_profiler SPMemoryDataType: %s", error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
|
||||
if result == KERN_SUCCESS {
|
||||
let active = Double(stats.active_count) * Double(vm_page_size)
|
||||
let inactive = Double(stats.inactive_count) * Double(vm_page_size)
|
||||
let wired = Double(stats.wire_count) * Double(vm_page_size)
|
||||
let compressed = Double(stats.compressor_page_count) * Double(vm_page_size)
|
||||
|
||||
return ram_s(
|
||||
active: active,
|
||||
inactive: inactive,
|
||||
wired: wired,
|
||||
compressed: compressed,
|
||||
total: totalSize,
|
||||
used: active + wired + compressed
|
||||
)
|
||||
}
|
||||
|
||||
os_log(.error, log: self.log, "host_statistics64(): %s", (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +348,20 @@ public extension NSView {
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .textColor
|
||||
|
||||
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-26)/2, width: 50, height: 26))
|
||||
let select: NSPopUpButton = SelectView(action: action, items: items, selected: selected)
|
||||
select.sizeToFit()
|
||||
|
||||
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
|
||||
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
|
||||
|
||||
row.addSubview(select)
|
||||
row.addSubview(rowTitle)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func SelectView(action: Selector, items: [KeyValue_t], selected: String) -> NSPopUpButton {
|
||||
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 50, height: 26))
|
||||
select.target = self
|
||||
select.action = action
|
||||
|
||||
@@ -365,24 +378,17 @@ public extension NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.menu = menu
|
||||
select.sizeToFit()
|
||||
|
||||
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
|
||||
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y))
|
||||
|
||||
row.addSubview(select)
|
||||
row.addSubview(rowTitle)
|
||||
|
||||
return row
|
||||
return select
|
||||
}
|
||||
}
|
||||
|
||||
public extension Notification.Name {
|
||||
static let toggleSettings = Notification.Name("toggleSettings")
|
||||
static let toggleModule = Notification.Name("toggleModule")
|
||||
static let openSettingsView = Notification.Name("openSettingsView")
|
||||
static let openModuleSettings = Notification.Name("openModuleSettings")
|
||||
static let settingsAppear = Notification.Name("settingsAppear")
|
||||
static let switchWidget = Notification.Name("switchWidget")
|
||||
static let checkForUpdates = Notification.Name("checkForUpdates")
|
||||
static let changeCronInterval = Notification.Name("changeCronInterval")
|
||||
|
||||
Reference in New Issue
Block a user