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
|
||||
}
|
||||
|
||||
if self.vm.notchState == .open && !self.vm.isBatteryPopoverActive {
|
||||
if self.vm.notchState == .open && !self.hasAnyActivePopovers() {
|
||||
self.vm.close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ struct NotchStatsView: View {
|
|||
@State private var isHoveringNetworkPopover = false
|
||||
@State private var isHoveringDiskPopover = false
|
||||
@State private var isResizingForStats = false
|
||||
@State private var statsHoverGraceActive = false
|
||||
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
|
||||
var availableGraphs: [GraphData] {
|
||||
|
|
@ -364,7 +366,13 @@ struct NotchStatsView: View {
|
|||
}
|
||||
}
|
||||
.onDisappear {
|
||||
statsHoverGraceWorkItem?.cancel()
|
||||
statsHoverGraceWorkItem = nil
|
||||
if statsHoverGraceActive {
|
||||
statsHoverGraceActive = false
|
||||
}
|
||||
// Keep monitoring running when tab is not visible
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||
|
|
@ -375,19 +383,44 @@ struct NotchStatsView: View {
|
|||
isResizingForStats = false
|
||||
}
|
||||
}
|
||||
.onChange(of: showingCPUPopover) { _, _ in
|
||||
.onChange(of: showingCPUPopover) { _, newValue in
|
||||
if newValue {
|
||||
activateStatsHoverGrace()
|
||||
} else {
|
||||
deactivateStatsHoverGrace()
|
||||
}
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingMemoryPopover) { _, _ in
|
||||
.onChange(of: showingMemoryPopover) { _, newValue in
|
||||
if newValue {
|
||||
activateStatsHoverGrace()
|
||||
} else {
|
||||
deactivateStatsHoverGrace()
|
||||
}
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingGPUPopover) { _, _ in
|
||||
.onChange(of: showingGPUPopover) { _, newValue in
|
||||
if newValue {
|
||||
activateStatsHoverGrace()
|
||||
} else {
|
||||
deactivateStatsHoverGrace()
|
||||
}
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingNetworkPopover) { _, _ in
|
||||
.onChange(of: showingNetworkPopover) { _, newValue in
|
||||
if newValue {
|
||||
activateStatsHoverGrace()
|
||||
} else {
|
||||
deactivateStatsHoverGrace()
|
||||
}
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingDiskPopover) { _, _ in
|
||||
.onChange(of: showingDiskPopover) { _, newValue in
|
||||
if newValue {
|
||||
activateStatsHoverGrace()
|
||||
} else {
|
||||
deactivateStatsHoverGrace()
|
||||
}
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: isHoveringCPUPopover) { _, _ in
|
||||
|
|
@ -406,26 +439,41 @@ struct NotchStatsView: View {
|
|||
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() {
|
||||
// Use the same logic as battery popover: active only when shown AND hovered
|
||||
// Also consider resize protection to prevent closing during layout changes
|
||||
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
|
||||
(showingMemoryPopover && isHoveringMemoryPopover) ||
|
||||
(showingGPUPopover && isHoveringGPUPopover) ||
|
||||
(showingNetworkPopover && isHoveringNetworkPopover) ||
|
||||
(showingDiskPopover && isHoveringDiskPopover) ||
|
||||
isResizingForStats
|
||||
let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
|
||||
let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive
|
||||
if vm.isStatsPopoverActive != newState {
|
||||
vm.isStatsPopoverActive = newState
|
||||
#if DEBUG
|
||||
print("📊 Stats popover state updated: \(newState)")
|
||||
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)")
|
||||
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)")
|
||||
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)")
|
||||
print(" Network: shown=\(showingNetworkPopover), hovering=\(isHoveringNetworkPopover)")
|
||||
print(" Disk: shown=\(showingDiskPopover), hovering=\(isHoveringDiskPopover)")
|
||||
print(" CPU open=\(showingCPUPopover)")
|
||||
print(" Memory open=\(showingMemoryPopover)")
|
||||
print(" GPU open=\(showingGPUPopover)")
|
||||
print(" Network open=\(showingNetworkPopover)")
|
||||
print(" Disk open=\(showingDiskPopover)")
|
||||
print(" Resizing: \(isResizingForStats)")
|
||||
print(" Hover grace: \(statsHoverGraceActive)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,11 +73,11 @@ private struct NetworkOverview: View {
|
|||
Text("Download")
|
||||
.font(.headline)
|
||||
.foregroundColor(downloadColor)
|
||||
Text(StatsFormatting.mbPerSecond(download))
|
||||
Text(StatsFormatting.throughput(download))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgDownload))
|
||||
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakDownload))
|
||||
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgDownload))
|
||||
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakDownload))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
|
@ -89,11 +89,11 @@ private struct NetworkOverview: View {
|
|||
Text("Upload")
|
||||
.font(.headline)
|
||||
.foregroundColor(uploadColor)
|
||||
Text(StatsFormatting.mbPerSecond(upload))
|
||||
Text(StatsFormatting.throughput(upload))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgUpload))
|
||||
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakUpload))
|
||||
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgUpload))
|
||||
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakUpload))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
|
@ -213,8 +213,8 @@ private struct NetworkInterfaceRow: View {
|
|||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
metricRow(label: "Down", value: StatsFormatting.mbPerSecond(interface.currentDownload), color: downloadColor)
|
||||
metricRow(label: "Up", value: StatsFormatting.mbPerSecond(interface.currentUpload), color: uploadColor)
|
||||
metricRow(label: "Down", value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
|
||||
metricRow(label: "Up", value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
// Adapted from boring.notch implementation
|
||||
|
||||
import SwiftUI
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
struct RankedProcessPopover: View {
|
||||
let rankingType: ProcessRankingType
|
||||
|
|
@ -44,8 +47,12 @@ struct RankedProcessPopover: View {
|
|||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.onHover { hovering in
|
||||
onHoverChange?(hovering)
|
||||
.overlay(alignment: .center) {
|
||||
PopoverHoverSensor { hovering in
|
||||
onHoverChange?(hovering)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,4 +71,51 @@ struct RankedProcessPopover: View {
|
|||
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)
|
||||
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 {
|
||||
String(format: "%.1f%%", value)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue