mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
commit
be02c1e9e1
6 changed files with 135 additions and 34 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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" : {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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 ?
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue