mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working stuff
This commit is contained in:
parent
630431a008
commit
7a1945dd21
5 changed files with 162 additions and 30 deletions
|
|
@ -694,7 +694,7 @@ struct ContentView: View {
|
||||||
self.isHovering = false
|
self.isHovering = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.vm.notchState == .open && !self.vm.isBatteryPopoverActive {
|
if self.vm.notchState == .open && !self.hasAnyActivePopovers() {
|
||||||
self.vm.close()
|
self.vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ struct NotchStatsView: View {
|
||||||
@State private var isHoveringNetworkPopover = false
|
@State private var isHoveringNetworkPopover = false
|
||||||
@State private var isHoveringDiskPopover = false
|
@State private var isHoveringDiskPopover = false
|
||||||
@State private var isResizingForStats = false
|
@State private var isResizingForStats = false
|
||||||
|
@State private var statsHoverGraceActive = false
|
||||||
|
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
|
||||||
var availableGraphs: [GraphData] {
|
var availableGraphs: [GraphData] {
|
||||||
|
|
@ -364,7 +366,13 @@ struct NotchStatsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
statsHoverGraceWorkItem?.cancel()
|
||||||
|
statsHoverGraceWorkItem = nil
|
||||||
|
if statsHoverGraceActive {
|
||||||
|
statsHoverGraceActive = false
|
||||||
|
}
|
||||||
// Keep monitoring running when tab is not visible
|
// Keep monitoring running when tab is not visible
|
||||||
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
||||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||||
|
|
@ -375,19 +383,44 @@ struct NotchStatsView: View {
|
||||||
isResizingForStats = false
|
isResizingForStats = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: showingCPUPopover) { _, _ in
|
.onChange(of: showingCPUPopover) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
activateStatsHoverGrace()
|
||||||
|
} else {
|
||||||
|
deactivateStatsHoverGrace()
|
||||||
|
}
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingMemoryPopover) { _, _ in
|
.onChange(of: showingMemoryPopover) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
activateStatsHoverGrace()
|
||||||
|
} else {
|
||||||
|
deactivateStatsHoverGrace()
|
||||||
|
}
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingGPUPopover) { _, _ in
|
.onChange(of: showingGPUPopover) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
activateStatsHoverGrace()
|
||||||
|
} else {
|
||||||
|
deactivateStatsHoverGrace()
|
||||||
|
}
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingNetworkPopover) { _, _ in
|
.onChange(of: showingNetworkPopover) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
activateStatsHoverGrace()
|
||||||
|
} else {
|
||||||
|
deactivateStatsHoverGrace()
|
||||||
|
}
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: showingDiskPopover) { _, _ in
|
.onChange(of: showingDiskPopover) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
activateStatsHoverGrace()
|
||||||
|
} else {
|
||||||
|
deactivateStatsHoverGrace()
|
||||||
|
}
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
.onChange(of: isHoveringCPUPopover) { _, _ in
|
.onChange(of: isHoveringCPUPopover) { _, _ in
|
||||||
|
|
@ -406,26 +439,41 @@ struct NotchStatsView: View {
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func activateStatsHoverGrace(duration: TimeInterval = 0.45) {
|
||||||
|
statsHoverGraceWorkItem?.cancel()
|
||||||
|
statsHoverGraceActive = true
|
||||||
|
let workItem = DispatchWorkItem {
|
||||||
|
statsHoverGraceActive = false
|
||||||
|
statsHoverGraceWorkItem = nil
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
statsHoverGraceWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deactivateStatsHoverGrace() {
|
||||||
|
statsHoverGraceWorkItem?.cancel()
|
||||||
|
statsHoverGraceWorkItem = nil
|
||||||
|
if statsHoverGraceActive {
|
||||||
|
statsHoverGraceActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateStatsPopoverState() {
|
private func updateStatsPopoverState() {
|
||||||
// Use the same logic as battery popover: active only when shown AND hovered
|
let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
|
||||||
// Also consider resize protection to prevent closing during layout changes
|
let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive
|
||||||
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
|
|
||||||
(showingMemoryPopover && isHoveringMemoryPopover) ||
|
|
||||||
(showingGPUPopover && isHoveringGPUPopover) ||
|
|
||||||
(showingNetworkPopover && isHoveringNetworkPopover) ||
|
|
||||||
(showingDiskPopover && isHoveringDiskPopover) ||
|
|
||||||
isResizingForStats
|
|
||||||
if vm.isStatsPopoverActive != newState {
|
if vm.isStatsPopoverActive != newState {
|
||||||
vm.isStatsPopoverActive = newState
|
vm.isStatsPopoverActive = newState
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
print("📊 Stats popover state updated: \(newState)")
|
print("📊 Stats popover state updated: \(newState)")
|
||||||
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)")
|
print(" CPU open=\(showingCPUPopover)")
|
||||||
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)")
|
print(" Memory open=\(showingMemoryPopover)")
|
||||||
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)")
|
print(" GPU open=\(showingGPUPopover)")
|
||||||
print(" Network: shown=\(showingNetworkPopover), hovering=\(isHoveringNetworkPopover)")
|
print(" Network open=\(showingNetworkPopover)")
|
||||||
print(" Disk: shown=\(showingDiskPopover), hovering=\(isHoveringDiskPopover)")
|
print(" Disk open=\(showingDiskPopover)")
|
||||||
print(" Resizing: \(isResizingForStats)")
|
print(" Resizing: \(isResizingForStats)")
|
||||||
|
print(" Hover grace: \(statsHoverGraceActive)")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,11 +73,11 @@ private struct NetworkOverview: View {
|
||||||
Text("Download")
|
Text("Download")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(downloadColor)
|
.foregroundColor(downloadColor)
|
||||||
Text(StatsFormatting.mbPerSecond(download))
|
Text(StatsFormatting.throughput(download))
|
||||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgDownload))
|
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgDownload))
|
||||||
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakDownload))
|
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakDownload))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
|
@ -89,11 +89,11 @@ private struct NetworkOverview: View {
|
||||||
Text("Upload")
|
Text("Upload")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(uploadColor)
|
.foregroundColor(uploadColor)
|
||||||
Text(StatsFormatting.mbPerSecond(upload))
|
Text(StatsFormatting.throughput(upload))
|
||||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgUpload))
|
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgUpload))
|
||||||
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakUpload))
|
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakUpload))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
@ -213,8 +213,8 @@ private struct NetworkInterfaceRow: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
VStack(alignment: .trailing, spacing: 4) {
|
||||||
metricRow(label: "Down", value: StatsFormatting.mbPerSecond(interface.currentDownload), color: downloadColor)
|
metricRow(label: "Down", value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
|
||||||
metricRow(label: "Up", value: StatsFormatting.mbPerSecond(interface.currentUpload), color: uploadColor)
|
metricRow(label: "Up", value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
// Adapted from boring.notch implementation
|
// Adapted from boring.notch implementation
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
struct RankedProcessPopover: View {
|
struct RankedProcessPopover: View {
|
||||||
let rankingType: ProcessRankingType
|
let rankingType: ProcessRankingType
|
||||||
|
|
@ -44,8 +47,12 @@ struct RankedProcessPopover: View {
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
.onHover { hovering in
|
.overlay(alignment: .center) {
|
||||||
onHoverChange?(hovering)
|
PopoverHoverSensor { hovering in
|
||||||
|
onHoverChange?(hovering)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,4 +71,51 @@ struct RankedProcessPopover: View {
|
||||||
DiskStatsDetailView()
|
DiskStatsDetailView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private struct PopoverHoverSensor: NSViewRepresentable {
|
||||||
|
var onHoverChange: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> TrackingView {
|
||||||
|
let view = TrackingView()
|
||||||
|
view.onHoverChange = onHoverChange
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: TrackingView, context: Context) {
|
||||||
|
nsView.onHoverChange = onHoverChange
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TrackingView: NSView {
|
||||||
|
var onHoverChange: ((Bool) -> Void)?
|
||||||
|
private var trackingArea: NSTrackingArea?
|
||||||
|
|
||||||
|
override func updateTrackingAreas() {
|
||||||
|
super.updateTrackingAreas()
|
||||||
|
if let trackingArea {
|
||||||
|
removeTrackingArea(trackingArea)
|
||||||
|
}
|
||||||
|
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways, .inVisibleRect]
|
||||||
|
let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
|
||||||
|
addTrackingArea(area)
|
||||||
|
trackingArea = area
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseEntered(with event: NSEvent) {
|
||||||
|
onHoverChange?(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseExited(with event: NSEvent) {
|
||||||
|
onHoverChange?(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
onHoverChange?(false)
|
||||||
|
if let trackingArea {
|
||||||
|
removeTrackingArea(trackingArea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -26,6 +26,36 @@ enum StatsFormatting {
|
||||||
let formatted = formatter.string(from: number) ?? String(format: "%.2f", value)
|
let formatted = formatter.string(from: number) ?? String(format: "%.2f", value)
|
||||||
return "\(formatted) MB/s"
|
return "\(formatted) MB/s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func throughput(_ valueInMegabytesPerSecond: Double) -> String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
|
||||||
|
if valueInMegabytesPerSecond >= 1 {
|
||||||
|
formatter.maximumFractionDigits = valueInMegabytesPerSecond >= 10 ? 1 : 2
|
||||||
|
let formatted = formatter.string(from: NSNumber(value: valueInMegabytesPerSecond))
|
||||||
|
?? String(format: "%.2f", valueInMegabytesPerSecond)
|
||||||
|
return "\(formatted) MB/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueInKilobytesPerSecond = valueInMegabytesPerSecond * 1024
|
||||||
|
if valueInKilobytesPerSecond >= 1 {
|
||||||
|
formatter.maximumFractionDigits = valueInKilobytesPerSecond >= 10 ? 0 : 1
|
||||||
|
let formatted = formatter.string(from: NSNumber(value: valueInKilobytesPerSecond))
|
||||||
|
?? String(format: valueInKilobytesPerSecond >= 10 ? "%.0f" : "%.1f", valueInKilobytesPerSecond)
|
||||||
|
return "\(formatted) KB/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueInBytesPerSecond = valueInMegabytesPerSecond * 1_048_576
|
||||||
|
if valueInBytesPerSecond >= 1 {
|
||||||
|
formatter.maximumFractionDigits = 0
|
||||||
|
let formatted = formatter.string(from: NSNumber(value: valueInBytesPerSecond))
|
||||||
|
?? String(format: "%.0f", valueInBytesPerSecond)
|
||||||
|
return "\(formatted) B/s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0 B/s"
|
||||||
|
}
|
||||||
|
|
||||||
static func percentage(_ value: Double) -> String {
|
static func percentage(_ value: Double) -> String {
|
||||||
String(format: "%.1f%%", value)
|
String(format: "%.1f%%", value)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue