diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 3d3b2826..b44d7e42 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -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 = ""; }; 97B5592A24FD84E000D3C4FF /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 98BF5451254DF04C004E9DF5 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + 9A045EB62594F8D100ED58F2 /* Dashboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = ""; }; 9A0C82D124460DFF00FAE3D4 /* updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = updater.swift; sourceTree = ""; }; 9A0C82D324460E4400FAE3D4 /* launchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = launchAtLogin.swift; sourceTree = ""; }; 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 */, diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index 9cd19153..bc3ccdcd 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -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), diff --git a/Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json b/Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 00000000..95df6a71 --- /dev/null +++ b/Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json @@ -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" + } +} diff --git a/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_1x.png b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_1x.png new file mode 100644 index 00000000..1198fc23 Binary files /dev/null and b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_1x.png differ diff --git a/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_2x.png b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_2x.png new file mode 100644 index 00000000..307389f3 Binary files /dev/null and b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_2x.png differ diff --git a/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_3x.png b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_3x.png new file mode 100644 index 00000000..45d855b4 Binary files /dev/null and b/Stats/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_white_24pt_3x.png differ diff --git a/Stats/Supporting Files/en.lproj/Localizable.strings b/Stats/Supporting Files/en.lproj/Localizable.strings index 5a3fe85e..15000b5e 100644 --- a/Stats/Supporting Files/en.lproj/Localizable.strings +++ b/Stats/Supporting Files/en.lproj/Localizable.strings @@ -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..."; diff --git a/Stats/Supporting Files/pl.lproj/Localizable.strings b/Stats/Supporting Files/pl.lproj/Localizable.strings index 850919c7..3f8234a7 100644 --- a/Stats/Supporting Files/pl.lproj/Localizable.strings +++ b/Stats/Supporting Files/pl.lproj/Localizable.strings @@ -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"; diff --git a/Stats/Supporting Files/uk.lproj/Localizable.strings b/Stats/Supporting Files/uk.lproj/Localizable.strings index 0f169b93..8f550495 100644 --- a/Stats/Supporting Files/uk.lproj/Localizable.strings +++ b/Stats/Supporting Files/uk.lproj/Localizable.strings @@ -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" = "Встановлено останню версію"; diff --git a/Stats/Views/AppSettings.swift b/Stats/Views/AppSettings.swift index 57645ade..fe28adba 100644 --- a/Stats/Views/AppSettings.swift +++ b/Stats/Views/AppSettings.swift @@ -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.. [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) } diff --git a/Stats/Views/Dashboard.swift b/Stats/Views/Dashboard.swift new file mode 100644 index 00000000..2cca7025 --- /dev/null +++ b/Stats/Views/Dashboard.swift @@ -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.. [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.. [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 + } +} diff --git a/Stats/Views/Settings.swift b/Stats/Views/Settings.swift index 394d5497..1f5c6008 100644 --- a/Stats/Views/Settings.swift +++ b/Stats/Views/Settings.swift @@ -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 } diff --git a/StatsKit/SystemKit.swift b/StatsKit/SystemKit.swift index 8dda4d61..15fbedf9 100644 --- a/StatsKit/SystemKit.swift +++ b/StatsKit/SystemKit.swift @@ -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.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.size / MemoryLayout.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.size / MemoryLayout.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.size / MemoryLayout.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.. 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")