Merge pull request #18 from Ebullioscopic/feature/stats

Feature/stats
This commit is contained in:
Hariharan Mudaliar 2025-08-13 23:45:43 +05:30 • committed by GitHub
commit be02c1e9e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 135 additions and 34 deletions

View file

@ -125,7 +125,15 @@ struct ContentView: View {
// Only close if mouse leaves and the notch is open and no popovers are active // Only close if mouse leaves and the notch is open and no popovers are active
if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive { if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive {
vm.close() // Add slight delay for stats tab to prevent accidental closure during layout changes
let delay: Double = (coordinator.currentView == .stats) ? 0.2 : 0.0
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// Double-check conditions after delay
if !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive {
vm.close()
}
}
} }
} }
.onTapGesture { .onTapGesture {

View file

@ -327,7 +327,7 @@
"CPU Usage" : { "CPU Usage" : {
}, },
"Current custom timer: %@" : { "Current custom timer:" : {
}, },
"Current History" : { "Current History" : {
@ -1219,7 +1219,7 @@
"unmuted" : { "unmuted" : {
}, },
"Update" : { "Update Timer" : {
}, },
"Upgrade to Pro" : { "Upgrade to Pro" : {

View file

@ -75,13 +75,48 @@ class ClipboardPanel: NSPanel {
let screenFrame = screen.visibleFrame let screenFrame = screen.visibleFrame
let panelFrame = frame let panelFrame = frame
// Position at top center of screen (near where the notch would be) // Check if we have a saved position
if let savedPosition = getSavedPosition() {
// Validate saved position is still on screen
let savedFrame = NSRect(origin: savedPosition, size: panelFrame.size)
if screenFrame.intersects(savedFrame) {
setFrameOrigin(savedPosition)
return
}
}
// Default to center of screen (not top center)
let xPosition = (screenFrame.width - panelFrame.width) / 2 + screenFrame.minX let xPosition = (screenFrame.width - panelFrame.width) / 2 + screenFrame.minX
let yPosition = screenFrame.maxY - panelFrame.height - 10 // 10px from top let yPosition = (screenFrame.height - panelFrame.height) / 2 + screenFrame.minY
setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
} }
private func getSavedPosition() -> NSPoint? {
let defaults = UserDefaults.standard
let x = defaults.double(forKey: "clipboardPanelPositionX")
let y = defaults.double(forKey: "clipboardPanelPositionY")
// Check if we have valid saved coordinates (not default 0.0)
if x != 0.0 || y != 0.0 {
return NSPoint(x: x, y: y)
}
return nil
}
private func saveCurrentPosition() {
let currentOrigin = frame.origin
let defaults = UserDefaults.standard
defaults.set(currentOrigin.x, forKey: "clipboardPanelPositionX")
defaults.set(currentOrigin.y, forKey: "clipboardPanelPositionY")
}
override func setFrameOrigin(_ point: NSPoint) {
super.setFrameOrigin(point)
// Save position whenever it changes (user dragging)
saveCurrentPosition()
}
func positionNearMouse() { func positionNearMouse() {
let mouseLocation = NSEvent.mouseLocation let mouseLocation = NSEvent.mouseLocation
let panelFrame = frame let panelFrame = frame

View file

@ -118,25 +118,71 @@ struct NotchStatsView: View {
return graphs return graphs
} }
// Simplified and more consistent layout system // Smart grid layout system for different graph counts
@ViewBuilder @ViewBuilder
var statsGridLayout: some View { var statsGridLayout: some View {
let graphCount = availableGraphs.count let graphCount = availableGraphs.count
// Use LazyVGrid for all cases - more predictable layout behavior if graphCount <= 3 {
LazyVGrid( // 1-3 graphs: Single row with equal spacing
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: min(graphCount, 3)), LazyVGrid(
spacing: 12 columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount),
) { spacing: 12
ForEach(0..<graphCount, id: \.self) { index in ) {
UnifiedStatsCard(graphData: availableGraphs[index]) ForEach(0..<graphCount, id: \.self) { index in
.transition(.asymmetric( UnifiedStatsCard(graphData: availableGraphs[index])
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)), .transition(.asymmetric(
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)) insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
)) removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
}
}
} else if graphCount == 4 {
// 4 graphs: 2x2 grid
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
spacing: 12
) {
ForEach(0..<graphCount, id: \.self) { index in
UnifiedStatsCard(graphData: availableGraphs[index])
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
}
}
} else {
// 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
VStack(spacing: 12) {
// First row: 3 graphs
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
spacing: 8
) {
ForEach(0..<3, id: \.self) { index in
UnifiedStatsCard(graphData: availableGraphs[index])
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
}
}
// Second row: 2 graphs (half-width each)
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
spacing: 8
) {
ForEach(3..<graphCount, id: \.self) { index in
UnifiedStatsCard(graphData: availableGraphs[index])
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
}
}
} }
} }
.animation(.easeInOut(duration: 0.4), value: graphCount)
} }
// Helper function to create graph views using unified component // Helper function to create graph views using unified component

View file

@ -1324,8 +1324,8 @@ struct TimerSettings: View {
Text("Custom Timer Duration") Text("Custom Timer Duration")
.font(.headline) .font(.headline)
HStack(spacing: 16) { HStack(spacing: 20) {
VStack { VStack(alignment: .center, spacing: 8) {
Text("Minutes") Text("Minutes")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -1336,10 +1336,10 @@ struct TimerSettings: View {
} }
} }
.pickerStyle(.menu) .pickerStyle(.menu)
.frame(width: 80, height: 40) .frame(width: 100)
} }
VStack { VStack(alignment: .center, spacing: 8) {
Text("Seconds") Text("Seconds")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -1350,20 +1350,32 @@ struct TimerSettings: View {
} }
} }
.pickerStyle(.menu) .pickerStyle(.menu)
.frame(width: 80, height: 40) .frame(width: 100)
} }
} }
.frame(maxWidth: .infinity)
HStack { VStack(alignment: .leading, spacing: 8) {
Text("Current custom timer: \(customTimerDisplayText)") HStack {
.foregroundStyle(.secondary) Text("Current custom timer:")
Spacer() .foregroundStyle(.secondary)
Button("Update") { Text(customTimerDisplayText)
customTimerDuration = Double(customMinutes * 60 + customSeconds) .foregroundStyle(.primary)
.fontWeight(.medium)
Spacer()
}
HStack {
Spacer()
Button("Update Timer") {
customTimerDuration = Double(customMinutes * 60 + customSeconds)
}
.disabled(customMinutes == 0 && customSeconds == 0)
.buttonStyle(.borderedProminent)
} }
.disabled(customMinutes == 0 && customSeconds == 0)
} }
} }
.padding(.vertical, 8)
} header: { } header: {
Text("Custom Timer") Text("Custom Timer")
} footer: { } footer: {

View file

@ -120,8 +120,8 @@ class StatsManager: ObservableObject {
var downloadSpeed: Double = 0.0 var downloadSpeed: Double = 0.0
var uploadSpeed: Double = 0.0 var uploadSpeed: Double = 0.0
// Only calculate speeds if we have a reasonable time interval and previous data // Only calculate speeds if we have a reasonable time interval and this isn't the first run
if timeInterval > 0.1 && previousNetworkStats.bytesIn > 0 && previousNetworkStats.bytesOut > 0 { if timeInterval > 0.1 && (previousNetworkStats.bytesIn > 0 || previousNetworkStats.bytesOut > 0) {
let bytesDownloaded = currentNetworkStats.bytesIn > previousNetworkStats.bytesIn ? let bytesDownloaded = currentNetworkStats.bytesIn > previousNetworkStats.bytesIn ?
currentNetworkStats.bytesIn - previousNetworkStats.bytesIn : 0 currentNetworkStats.bytesIn - previousNetworkStats.bytesIn : 0
let bytesUploaded = currentNetworkStats.bytesOut > previousNetworkStats.bytesOut ? let bytesUploaded = currentNetworkStats.bytesOut > previousNetworkStats.bytesOut ?
@ -136,8 +136,8 @@ class StatsManager: ObservableObject {
var readSpeed: Double = 0.0 var readSpeed: Double = 0.0
var writeSpeed: Double = 0.0 var writeSpeed: Double = 0.0
// Only calculate speeds if we have a reasonable time interval and previous data // Only calculate speeds if we have a reasonable time interval and this isn't the first run
if timeInterval > 0.1 && previousDiskStats.bytesRead > 0 && previousDiskStats.bytesWritten > 0 { if timeInterval > 0.1 && (previousDiskStats.bytesRead > 0 || previousDiskStats.bytesWritten > 0) {
let bytesRead = currentDiskStats.bytesRead > previousDiskStats.bytesRead ? let bytesRead = currentDiskStats.bytesRead > previousDiskStats.bytesRead ?
currentDiskStats.bytesRead - previousDiskStats.bytesRead : 0 currentDiskStats.bytesRead - previousDiskStats.bytesRead : 0
let bytesWritten = currentDiskStats.bytesWritten > previousDiskStats.bytesWritten ? let bytesWritten = currentDiskStats.bytesWritten > previousDiskStats.bytesWritten ?