mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
282 lines
11 KiB
Swift
282 lines
11 KiB
Swift
//
|
|
// MemoryStatsDetailView.swift
|
|
// DynamicIsland
|
|
//
|
|
// Detailed memory dashboard mirroring the Stats app layout.
|
|
//
|
|
// Created by GitHub Copilot on 19/10/2025.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct MemoryStatsDetailView: View {
|
|
@ObservedObject private var statsManager = StatsManager.shared
|
|
@State private var topProcesses: [ProcessStats] = []
|
|
|
|
private let accentColor = Color.green
|
|
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
|
|
private let processDisplayLimit = 8
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 16) {
|
|
StatsCard(title: "Memory Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
|
|
MemoryUsageDashboard(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
|
}
|
|
|
|
StatsCard(title: "Pressure & Swap", padding: 16, background: cardBackground, cornerRadius: 12) {
|
|
MemoryHealthView(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
|
}
|
|
|
|
StatsCard(title: "Details", padding: 16, background: cardBackground, cornerRadius: 12) {
|
|
MemoryDetailsGrid(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
|
}
|
|
|
|
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
|
|
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
|
|
}
|
|
}
|
|
.padding(16)
|
|
}
|
|
.frame(minWidth: 380, minHeight: 420)
|
|
.onAppear(perform: refreshProcesses)
|
|
.onReceive(statsManager.$lastUpdated) { _ in
|
|
refreshProcesses()
|
|
}
|
|
}
|
|
|
|
private func refreshProcesses() {
|
|
let processes = statsManager.getProcessesRankedByMemory()
|
|
topProcesses = Array(processes.prefix(processDisplayLimit))
|
|
}
|
|
}
|
|
|
|
private struct MemoryUsageDashboard: View {
|
|
let breakdown: MemoryBreakdown
|
|
let accentColor: Color
|
|
|
|
private let freeColor = Color.gray.opacity(0.28)
|
|
|
|
var body: some View {
|
|
let totalsSection = VStack(alignment: .leading, spacing: 12) {
|
|
DetailRow(color: accentColor.opacity(0.8), label: "Used", value: formattedBytes(breakdown.usedBytes))
|
|
DetailRow(color: freeColor.opacity(0.9), label: "Free", value: formattedBytes(breakdown.freeBytes))
|
|
DetailRow(color: nil, label: "Total", value: formattedBytes(breakdown.totalBytes))
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
let breakdownSection = VStack(alignment: .leading, spacing: 8) {
|
|
Text("Breakdown")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
MemoryBreakdownRow(label: "App", value: breakdown.appBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.85))
|
|
MemoryBreakdownRow(label: "Cached", value: breakdown.cacheBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.65))
|
|
MemoryBreakdownRow(label: "Wired", value: breakdown.wiredBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.45))
|
|
MemoryBreakdownRow(label: "Compressed", value: breakdown.compressedBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.35))
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
return ViewThatFits(in: .horizontal) {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
ZStack {
|
|
Circle()
|
|
.stroke(freeColor.opacity(0.35), lineWidth: 14)
|
|
.frame(width: 128, height: 128)
|
|
|
|
MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor)
|
|
.frame(width: 128, height: 128)
|
|
|
|
VStack(spacing: 4) {
|
|
Text(StatsFormatting.percentage(breakdown.usedPercentage))
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
Text("Used")
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
totalsSection
|
|
Divider().padding(.vertical, 4)
|
|
breakdownSection
|
|
}
|
|
HStack(alignment: .top, spacing: 28) {
|
|
ZStack {
|
|
Circle()
|
|
.stroke(freeColor.opacity(0.35), lineWidth: 14)
|
|
.frame(width: 128, height: 128)
|
|
|
|
MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor)
|
|
.frame(width: 128, height: 128)
|
|
|
|
VStack(spacing: 4) {
|
|
Text(StatsFormatting.percentage(breakdown.usedPercentage))
|
|
.font(.system(size: 30, weight: .bold, design: .rounded))
|
|
Text("Used")
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
totalsSection
|
|
breakdownSection
|
|
}
|
|
}
|
|
}
|
|
|
|
private func formattedBytes(_ value: UInt64) -> String {
|
|
StatsFormatting.bytes(value)
|
|
}
|
|
}
|
|
|
|
private struct MemoryUsageRing: View {
|
|
let breakdown: MemoryBreakdown
|
|
let usedColor: Color
|
|
let freeColor: Color
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
let usedFraction = CGFloat(min(max(breakdown.usedPercentage / 100, 0), 1))
|
|
if usedFraction > 0 {
|
|
RingArc(start: 0, end: usedFraction, color: usedColor, lineWidth: 14)
|
|
}
|
|
if usedFraction < 1 {
|
|
RingArc(start: usedFraction, end: 1, color: freeColor.opacity(0.65), lineWidth: 14)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MemoryBreakdownRow: View {
|
|
let label: String
|
|
let value: UInt64
|
|
let total: UInt64
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
let percent = total > 0 ? Double(value) / Double(total) * 100 : 0
|
|
return DetailRow(color: color, label: label, value: "\(StatsFormatting.bytes(value)) · \(StatsFormatting.percentage(percent))")
|
|
}
|
|
}
|
|
|
|
private struct MemoryDetailsGrid: View {
|
|
let breakdown: MemoryBreakdown
|
|
let accentColor: Color
|
|
|
|
var body: some View {
|
|
VStack(spacing: 10) {
|
|
DetailRow(color: accentColor.opacity(0.85), label: "App Memory", value: StatsFormatting.bytes(breakdown.appBytes))
|
|
DetailRow(color: accentColor.opacity(0.7), label: "Cached Files", value: StatsFormatting.bytes(breakdown.cacheBytes))
|
|
DetailRow(color: accentColor.opacity(0.6), label: "Active", value: StatsFormatting.bytes(breakdown.activeBytes))
|
|
DetailRow(color: accentColor.opacity(0.45), label: "Inactive", value: StatsFormatting.bytes(breakdown.inactiveBytes))
|
|
DetailRow(color: accentColor.opacity(0.35), label: "Wired", value: StatsFormatting.bytes(breakdown.wiredBytes))
|
|
DetailRow(color: accentColor.opacity(0.25), label: "Compressed", value: StatsFormatting.bytes(breakdown.compressedBytes))
|
|
DetailRow(color: accentColor.opacity(0.18), label: "Free", value: StatsFormatting.bytes(breakdown.freeBytes))
|
|
Divider().padding(.vertical, 4)
|
|
DetailRow(color: nil, label: "Total Memory", value: StatsFormatting.bytes(breakdown.totalBytes))
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MemoryHealthView: View {
|
|
let breakdown: MemoryBreakdown
|
|
let accentColor: Color
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
MemoryPressureIndicator(pressure: breakdown.pressure)
|
|
Divider().padding(.vertical, 4)
|
|
SwapUsageView(swap: breakdown.swap, accentColor: accentColor)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MemoryPressureIndicator: View {
|
|
let pressure: MemoryPressure
|
|
|
|
private var status: (title: String, color: Color, description: String) {
|
|
switch pressure.level {
|
|
case .normal:
|
|
return ("Normal", Color.green, "System memory pressure is nominal.")
|
|
case .warning:
|
|
return ("Warning", Color.orange, "Memory pressure is elevated. Closing apps may help.")
|
|
case .critical:
|
|
return ("Critical", Color.red, "Memory pressure is critical; the system may purge aggressively.")
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Pressure")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
HStack {
|
|
Capsule()
|
|
.fill(status.color.opacity(0.16))
|
|
.overlay(
|
|
HStack(spacing: 8) {
|
|
Circle()
|
|
.fill(status.color)
|
|
.frame(width: 10, height: 10)
|
|
Text(status.title)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundColor(status.color)
|
|
}
|
|
.padding(.horizontal, 12)
|
|
)
|
|
.frame(height: 28)
|
|
Spacer()
|
|
}
|
|
|
|
Text(status.description)
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SwapUsageView: View {
|
|
let swap: MemorySwap
|
|
let accentColor: Color
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text("Swap")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
if swap.totalBytes > 0 {
|
|
DetailRow(color: accentColor.opacity(0.85), label: "Used", value: StatsFormatting.bytes(swap.usedBytes))
|
|
DetailRow(color: accentColor.opacity(0.55), label: "Free", value: StatsFormatting.bytes(swap.freeBytes))
|
|
DetailRow(color: nil, label: "Total", value: StatsFormatting.bytes(swap.totalBytes))
|
|
SwapUsageBar(usedBytes: swap.usedBytes, totalBytes: swap.totalBytes, tint: accentColor)
|
|
.frame(height: 8)
|
|
} else {
|
|
Text("Swap is disabled on this system.")
|
|
.font(.footnote)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SwapUsageBar: View {
|
|
let usedBytes: UInt64
|
|
let totalBytes: UInt64
|
|
let tint: Color
|
|
|
|
var body: some View {
|
|
GeometryReader { proxy in
|
|
let width = proxy.size.width
|
|
let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0
|
|
ZStack(alignment: .leading) {
|
|
Capsule()
|
|
.fill(Color.primary.opacity(0.08))
|
|
Capsule()
|
|
.fill(tint.opacity(0.8))
|
|
.frame(width: width * progress)
|
|
}
|
|
}
|
|
}
|
|
}
|