Merge remote-tracking branch 'origin/main' into pr-361

# Conflicts:
#	DynamicIsland/components/Settings/SettingsView.swift
This commit is contained in:
Hariharan Mudaliar 2026-03-28 11:49:27 +05:30
commit 73d076636f
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
14 changed files with 6786 additions and 929 deletions

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Foundation
import AppKit
import Combine
import SwiftUI
import Foundation
private struct ITunesSearchResponse: Decodable {
let results: [ITunesTrack]
}
private struct ITunesTrack: Decodable {
let trackName: String?
let collectionName: String?
let artworkUrl100: String?
}
class AppleMusicController: MediaControllerProtocol {
// MARK: - Properties
@ -30,17 +40,23 @@ class AppleMusicController: MediaControllerProtocol {
bundleIdentifier: "com.apple.Music",
playbackRate: 1
)
var playbackStatePublisher: AnyPublisher<PlaybackState, Never> {
$playbackState.eraseToAnyPublisher()
}
var isWorking: Bool {
return true // AppleMusic controller always works
}
/// Minimum byte count for artwork data to be considered valid. Anything
/// smaller is likely an empty descriptor or error string, not image data.
private static let minimumArtworkSize = 16
private var notificationTask: Task<Void, Never>?
private var lastCatalogArtworkKey: String?
private var cachedCatalogArtwork: Data?
// MARK: - Initialization
init() {
setupPlaybackStateChangeObserver()
@ -122,7 +138,7 @@ class AppleMusicController: MediaControllerProtocol {
guard let descriptor = try? await fetchPlaybackInfoAsync() else { return }
guard descriptor.numberOfItems >= 8 else { return }
var updatedState = self.playbackState
updatedState.isPlaying = descriptor.atIndex(1)?.booleanValue ?? false
updatedState.title = descriptor.atIndex(2)?.stringValue ?? "Unknown"
updatedState.artist = descriptor.atIndex(3)?.stringValue ?? "Unknown"
@ -132,91 +148,108 @@ class AppleMusicController: MediaControllerProtocol {
updatedState.isShuffled = descriptor.atIndex(7)?.booleanValue ?? false
let repeatModeValue = descriptor.atIndex(8)?.int32Value ?? 0
updatedState.repeatMode = RepeatMode(rawValue: Int(repeatModeValue)) ?? .off
updatedState.artwork = descriptor.atIndex(9)?.data as Data?
// AppleScript returns artwork data for library tracks. For streamed
// content not in the library it returns an empty descriptor, so we
// fall back to the iTunes Search API to fetch artwork by metadata.
if let artworkData = descriptor.atIndex(9)?.data as Data?,
artworkData.count > Self.minimumArtworkSize {
updatedState.artwork = artworkData
} else {
updatedState.artwork = await fetchArtworkFromCatalog(
title: updatedState.title, artist: updatedState.artist, album: updatedState.album
)
}
updatedState.lastUpdated = Date()
self.playbackState = updatedState
}
// MARK: - Private Methods
private func fetchArtworkFromCatalog(title: String, artist: String, album: String) async -> Data? {
let key = "\(title)|\(artist)|\(album)"
if key == lastCatalogArtworkKey, let cached = cachedCatalogArtwork {
return cached
}
// Search with title + artist only; including album in the query can
// confuse the free-text search when names overlap. We validate the
// album from the structured response fields instead.
let query = "\(title) \(artist)"
guard !query.trimmingCharacters(in: .whitespaces).isEmpty,
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "https://itunes.apple.com/search?term=\(encoded)&media=music&entity=song&limit=10")
else { return nil }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(ITunesSearchResponse.self, from: data)
guard !response.results.isEmpty else { return nil }
let normalizedAlbum = album.lowercased()
let normalizedTitle = title.lowercased()
let match = response.results.first(where: {
$0.trackName?.lowercased() == normalizedTitle &&
$0.collectionName?.lowercased() == normalizedAlbum
}) ?? response.results.first(where: {
$0.collectionName?.lowercased() == normalizedAlbum
}) ?? response.results.first(where: {
$0.trackName?.lowercased() == normalizedTitle
}) ?? response.results.first
guard let artworkURLString = match?.artworkUrl100 else { return nil }
let highResURL = artworkURLString.replacingOccurrences(of: "100x100", with: "600x600")
guard let imageURL = URL(string: highResURL) else { return nil }
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
lastCatalogArtworkKey = key
cachedCatalogArtwork = imageData
return imageData
} catch {
return nil
}
}
private func executeCommand(_ command: String) async {
let script = "tell application \"Music\" to \(command)"
try? await AppleScriptHelper.executeVoid(script)
}
private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? {
if #available(macOS 26.0, *) {
let script = """
tell application "Music"
let script = """
tell application "Music"
try
set playerState to player state is playing
set currentTrackName to name of current track
set currentTrackArtist to artist of current track
set currentTrackAlbum to album of current track
set trackPosition to player position
set trackDuration to duration of current track
set shuffleState to shuffle enabled
set repeatState to song repeat
if repeatState is off then
set repeatValue to 1
else if repeatState is one then
set repeatValue to 2
else if repeatState is all then
set repeatValue to 3
end if
set artData to ""
try
set playerState to player state is playing
set currentTrackName to name of current track
set currentTrackArtist to artist of current track
set currentTrackAlbum to album of current track
set trackPosition to player position
set trackDuration to duration of current track
set shuffleState to shuffle enabled
set repeatState to song repeat
if repeatState is off then
set repeatValue to 1
else if repeatState is one then
set repeatValue to 2
else if repeatState is all then
set repeatValue to 3
end if
set artData to ""
try
set libraryTrack to first track of playlist "Library" whose name is currentTrackName and artist is currentTrackArtist
set artData to data of artwork 1 of libraryTrack
on error
try
set artData to data of artwork 1 of current track
on error
set artData to ""
end try
end try
return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData}
on error
return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, ""}
set artData to raw data of artwork 1 of current track
end try
end tell
"""
return try await AppleScriptHelper.execute(script)
} else {
let script = """
tell application "Music"
set isRunning to true
try
set playerState to player state is playing
set currentTrackName to name of current track
set currentTrackArtist to artist of current track
set currentTrackAlbum to album of current track
set trackPosition to player position
set trackDuration to duration of current track
set shuffleState to shuffle enabled
set repeatState to song repeat
if repeatState is off then
set repeatValue to 1
else if repeatState is one then
set repeatValue to 2
else if repeatState is all then
set repeatValue to 3
end if
try
set artData to data of artwork 1 of current track
on error
set artData to ""
end try
return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData}
on error
return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, ""}
end try
end tell
"""
return try await AppleScriptHelper.execute(script)
}
return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData}
on error
return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, ""}
end try
end tell
"""
return try await AppleScriptHelper.execute(script)
}
}

View file

@ -146,6 +146,7 @@ class CalendarService: CalendarServiceProviding {
extension CalendarModel {
init(from calendar: EKCalendar) {
self.init(
accountName: calendar.accountTitle,
id: calendar.calendarIdentifier,
title: calendar.title,
color: calendar.color,
@ -267,7 +268,7 @@ extension Priority {
// MARK: - Helper Extensions
private extension EKCalendar {
extension EKCalendar {
var accountTitle: String {
switch source.sourceType {
case .local, .subscribed, .birthdays:

View file

@ -615,11 +615,11 @@ private struct DurationInputRow: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
DurationField(label: "HH", value: $hours, range: 0...23, width: fieldWidth)
DurationField(label: String(localized: "HH"), value: $hours, range: 0...23, width: fieldWidth)
colon
DurationField(label: "MM", value: $minutes, range: 0...59, width: fieldWidth)
DurationField(label: String(localized: "MM"), value: $minutes, range: 0...59, width: fieldWidth)
colon
DurationField(label: "SS", value: $seconds, range: 0...59, width: fieldWidth)
DurationField(label: String(localized: "SS"), value: $seconds, range: 0...59, width: fieldWidth)
}
}

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ struct CPUStatsDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "CPU Overview") {
StatsCard(title: String(localized: "CPU Overview")) {
CPUUsageDashboard(
breakdown: statsManager.cpuBreakdown,
loadAverage: statsManager.cpuLoadAverage,
@ -43,12 +43,12 @@ struct CPUStatsDetailView: View {
)
}
StatsCard(title: "Top Processes") {
StatsCard(title: String(localized: "Top Processes")) {
CPUProcessList(processes: topProcesses, accentColor: userColor)
}
if !statsManager.cpuCoreUsage.isEmpty {
StatsCard(title: "Per-Core Usage") {
StatsCard(title: String(localized: "Per-Core Usage")) {
CPUCoreUsageGrid(cores: statsManager.cpuCoreUsage, accentColor: userColor)
}
}

View file

@ -88,12 +88,12 @@ struct CPUUsageDashboard: View {
Text("System")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
DetailRow(color: nil, label: String(localized: "Logical cores"), value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: String(localized: "Uptime"), value: StatsFormatting.abbreviatedDuration(uptime))
Divider().padding(.vertical, 4)
DetailRow(color: nil, label: "Load 1m", value: String(format: "%.2f", loadAverage.oneMinute))
DetailRow(color: nil, label: "Load 5m", value: String(format: "%.2f", loadAverage.fiveMinutes))
DetailRow(color: nil, label: "Load 15m", value: String(format: "%.2f", loadAverage.fifteenMinutes))
DetailRow(color: nil, label: String(localized: "Load 1m"), value: String(format: "%.2f", loadAverage.oneMinute))
DetailRow(color: nil, label: String(localized: "Load 5m"), value: String(format: "%.2f", loadAverage.fiveMinutes))
DetailRow(color: nil, label: String(localized: "Load 15m"), value: String(format: "%.2f", loadAverage.fifteenMinutes))
}
.frame(maxWidth: .infinity, alignment: .leading)
@ -205,7 +205,7 @@ private struct CPUFrequencyGauge: View {
var body: some View {
CircularGaugeView(
title: "Frequency",
title: String(localized: "Frequency"),
value: normalizedValue,
tint: tint,
centerPrimaryText: centerPrimary,
@ -264,7 +264,7 @@ private struct CPUSegmentDonut: View {
VStack(spacing: 4) {
Text(StatsFormatting.percentage(breakdown.activeUsage))
.font(.system(size: 30, weight: .bold, design: .rounded))
Text("Active")
Text(String(localized: "stats_resource_active", defaultValue: "Active"))
.font(.footnote)
.foregroundColor(.secondary)
}
@ -335,9 +335,9 @@ struct LoadAverageRow: View {
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 12) {
LoadAverageValue(label: "1m", value: loadAverage.oneMinute)
LoadAverageValue(label: "5m", value: loadAverage.fiveMinutes)
LoadAverageValue(label: "15m", value: loadAverage.fifteenMinutes)
LoadAverageValue(label: String(localized: "1m"), value: loadAverage.oneMinute)
LoadAverageValue(label: String(localized: "5m"), value: loadAverage.fiveMinutes)
LoadAverageValue(label: String(localized: "15m"), value: loadAverage.fifteenMinutes)
}
}
}
@ -455,10 +455,10 @@ struct CPUDetailsGrid: View {
Text("Usage Breakdown")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Total", value: StatsFormatting.percentage(breakdown.activeUsage))
DetailRow(color: systemColor, label: "System", value: StatsFormatting.percentage(breakdown.system))
DetailRow(color: userColor, label: "User", value: StatsFormatting.percentage(breakdown.user))
DetailRow(color: Color.gray.opacity(0.4), label: "Idle", value: StatsFormatting.percentage(breakdown.idle))
DetailRow(color: nil, label: String(localized: "Total"), value: StatsFormatting.percentage(breakdown.activeUsage))
DetailRow(color: systemColor, label: String(localized: "System"), value: StatsFormatting.percentage(breakdown.system))
DetailRow(color: userColor, label: String(localized: "User"), value: StatsFormatting.percentage(breakdown.user))
DetailRow(color: Color.gray.opacity(0.4), label: String(localized: "Idle"), value: StatsFormatting.percentage(breakdown.idle))
}
Divider().padding(.vertical, 4)
@ -467,8 +467,8 @@ struct CPUDetailsGrid: View {
Text("System")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
DetailRow(color: nil, label: String(localized: "Logical cores"), value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: String(localized: "Uptime"), value: StatsFormatting.abbreviatedDuration(uptime))
}
if showLoadAverage {

View file

@ -28,7 +28,7 @@ struct DiskStatsDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Disk Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Disk Overview"), padding: 16, background: cardBackground, cornerRadius: 12) {
DiskOverview(
readSpeed: statsManager.diskRead,
writeSpeed: statsManager.diskWrite,
@ -39,11 +39,11 @@ struct DiskStatsDetailView: View {
)
}
StatsCard(title: "Storage Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Storage Devices"), padding: 16, background: cardBackground, cornerRadius: 12) {
DiskDevicesCard(devices: statsManager.diskDevices, accentColor: readColor)
}
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Session Totals"), padding: 16, background: cardBackground, cornerRadius: 12) {
DiskTotalsView(totals: statsManager.diskTotals, readColor: readColor, writeColor: writeColor)
}
}
@ -99,8 +99,8 @@ private struct DiskTotalsView: View {
var body: some View {
VStack(spacing: 10) {
DetailRow(color: readColor.opacity(0.75), label: "Read", value: formattedBytes(totals.readMB))
DetailRow(color: writeColor.opacity(0.75), label: "Written", value: formattedBytes(totals.writtenMB))
DetailRow(color: readColor.opacity(0.75), label: String(localized: "Read"), value: formattedBytes(totals.readMB))
DetailRow(color: writeColor.opacity(0.75), label: String(localized: "Written"), value: formattedBytes(totals.writtenMB))
}
}

View file

@ -29,7 +29,7 @@ struct GPUStatsDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "GPU Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "GPU Overview"), padding: 16, background: cardBackground, cornerRadius: 12) {
GPUUsageDashboard(
usage: statsManager.gpuUsage,
breakdown: statsManager.gpuBreakdown,
@ -40,12 +40,12 @@ struct GPUStatsDetailView: View {
)
}
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Top Processes"), padding: 16, background: cardBackground, cornerRadius: 12) {
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
}
if !statsManager.gpuDevices.isEmpty {
StatsCard(title: "Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Devices"), padding: 16, background: cardBackground, cornerRadius: 12) {
GPUDeviceList(devices: statsManager.gpuDevices, accentColor: accentColor)
}
}
@ -119,10 +119,10 @@ private struct GPUUsageDashboard: View {
private var breakdownSection: some View {
VStack(alignment: .leading, spacing: 12) {
DetailRow(color: accentColor.opacity(0.9), label: "Render", value: StatsFormatting.percentage(breakdown.render))
DetailRow(color: accentColor.opacity(0.7), label: "Compute", value: StatsFormatting.percentage(breakdown.compute))
DetailRow(color: accentColor.opacity(0.55), label: "Video", value: StatsFormatting.percentage(breakdown.video))
DetailRow(color: accentColor.opacity(0.45), label: "Other", value: StatsFormatting.percentage(breakdown.other))
DetailRow(color: accentColor.opacity(0.9), label: String(localized: "Render"), value: StatsFormatting.percentage(breakdown.render))
DetailRow(color: accentColor.opacity(0.7), label: String(localized: "Compute"), value: StatsFormatting.percentage(breakdown.compute))
DetailRow(color: accentColor.opacity(0.55), label: String(localized: "Video"), value: StatsFormatting.percentage(breakdown.video))
DetailRow(color: accentColor.opacity(0.45), label: String(localized: "Other"), value: StatsFormatting.percentage(breakdown.other))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
@ -132,15 +132,15 @@ private struct GPUUsageDashboard: View {
Text("Averages")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Session Avg", value: StatsFormatting.percentage(averageUsage))
DetailRow(color: nil, label: "Last Update", value: formattedTimestamp(lastUpdated))
DetailRow(color: nil, label: String(localized: "Session Avg"), value: StatsFormatting.percentage(averageUsage))
DetailRow(color: nil, label: String("localized: Last Update"), value: formattedTimestamp(lastUpdated))
if let device = primaryDevice {
Divider().padding(.vertical, 4)
DetailRow(color: nil, label: "Active GPU", value: device.formattedVendorModel)
DetailRow(color: nil, label: "Status", value: device.isActive ? "Active" : "Idle")
DetailRow(color: nil, label: "Temperature", value: device.temperatureText)
DetailRow(color: nil, label: String(localized: "Active GPU"), value: device.formattedVendorModel)
DetailRow(color: nil, label: String(localized: "Status"), value: device.isActive ? String(localized: "Active") : String(localized: "Idle"))
DetailRow(color: nil, label: String(localized: "Temperature"), value: device.temperatureText)
if let cores = device.cores {
DetailRow(color: nil, label: "Cores", value: "\(cores)")
DetailRow(color: nil, label: String(localized: "Cores"), value: "\(cores)")
}
}
}
@ -170,9 +170,9 @@ private struct GPUEngineGauges: View {
.font(.caption)
.foregroundColor(.secondary)
HStack(alignment: .center, spacing: 18) {
EngineGaugeView(title: "Render", value: render, tint: accentColor, size: 72)
EngineGaugeView(title: String(localized: "Render"), value: render, tint: accentColor, size: 72)
GPUUsageRing(usage: usage, accentColor: accentColor, diameter: ringDiameter, lineWidth: ringLineWidth)
EngineGaugeView(title: "Tiler", value: tiler, tint: accentColor.opacity(0.75), size: 72)
EngineGaugeView(title: String(localized: "Tiler"), value: tiler, tint: accentColor.opacity(0.75), size: 72)
}
.frame(maxWidth: .infinity, alignment: .center)
}
@ -193,7 +193,7 @@ private struct GPUUsageRing: View {
VStack(spacing: 4) {
Text(StatsFormatting.percentage(usage))
.font(.system(size: 26, weight: .bold, design: .rounded))
Text("Active")
Text(String(localized: "stats_resource_active", defaultValue: "Active"))
.font(.footnote)
.foregroundColor(.secondary)
}
@ -272,7 +272,7 @@ private struct GPUDeviceRow: View {
VStack(alignment: .leading, spacing: 2) {
Text(device.formattedVendorModel)
.font(.system(size: 13, weight: .semibold))
Text(device.isActive ? "Active" : "Idle")
Text(device.isActive ? String(localized: "stats_resource_active", defaultValue: "Active") : String(localized: "Idle"))
.font(.caption2)
.foregroundColor(device.isActive ? .green : .secondary)
}
@ -288,25 +288,25 @@ private struct GPUDeviceRow: View {
}
VStack(spacing: 8) {
if let render = device.renderUtilization {
DetailRow(color: accentColor.opacity(0.9), label: "Renderer", value: StatsFormatting.percentage(render))
DetailRow(color: accentColor.opacity(0.9), label: String(localized: "Renderer"), value: StatsFormatting.percentage(render))
}
if let tiler = device.tilerUtilization {
DetailRow(color: accentColor.opacity(0.6), label: "Tiler", value: StatsFormatting.percentage(tiler))
DetailRow(color: accentColor.opacity(0.6), label: String(localized: "Tiler"), value: StatsFormatting.percentage(tiler))
}
if let temp = device.temperature {
DetailRow(color: nil, label: "Temperature", value: String(format: "%.0f°C", temp))
DetailRow(color: nil, label: String(localized: "Temperature"), value: String(format: "%.0f°C", temp))
}
if let fan = device.fanSpeed {
DetailRow(color: nil, label: "Fan Speed", value: "\(fan)%")
DetailRow(color: nil, label: String(localized: "Fan Speed"), value: "\(fan)%")
}
if let coreClock = device.coreClock {
DetailRow(color: nil, label: "Core Clock", value: "\(coreClock) MHz")
DetailRow(color: nil, label: String(localized: "Core Clock"), value: "\(coreClock) MHz")
}
if let memoryClock = device.memoryClock {
DetailRow(color: nil, label: "Memory Clock", value: "\(memoryClock) MHz")
DetailRow(color: nil, label: String(localized: "Memory Clock"), value: "\(memoryClock) MHz")
}
if let cores = device.cores {
DetailRow(color: nil, label: "Cores", value: "\(cores)")
DetailRow(color: nil, label: String(localized: "Cores"), value: "\(cores)")
}
}
}

View file

@ -29,15 +29,15 @@ struct MemoryStatsDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Memory Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Memory Overview"), padding: 16, background: cardBackground, cornerRadius: 12) {
MemoryUsageDashboard(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
}
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Top Processes"), padding: 16, background: cardBackground, cornerRadius: 12) {
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
}
StatsCard(title: "Pressure & Swap", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Pressure & Swap"), padding: 16, background: cardBackground, cornerRadius: 12) {
MemoryHealthView(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
}
}
@ -64,9 +64,9 @@ private struct MemoryUsageDashboard: View {
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))
DetailRow(color: accentColor.opacity(0.8), label: String(localized: "Used"), value: formattedBytes(breakdown.usedBytes))
DetailRow(color: freeColor.opacity(0.9), label: String(localized: "Free"), value: formattedBytes(breakdown.freeBytes))
DetailRow(color: nil, label: String(localized: "Total"), value: formattedBytes(breakdown.totalBytes))
}
.frame(maxWidth: .infinity, alignment: .leading)
@ -74,10 +74,10 @@ private struct MemoryUsageDashboard: View {
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))
MemoryBreakdownRow(label: String(localized: "App"), value: breakdown.appBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.85))
MemoryBreakdownRow(label: String(localized: "Cached"), value: breakdown.cacheBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.65))
MemoryBreakdownRow(label: String(localized: "Wired"), value: breakdown.wiredBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.45))
MemoryBreakdownRow(label: String(localized: "Compressed"), value: breakdown.compressedBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.35))
}
.frame(maxWidth: .infinity, alignment: .leading)
@ -170,11 +170,11 @@ private struct MemoryPressureIndicator: View {
private var status: (title: String, color: Color, description: String) {
switch pressure.level {
case .normal:
return ("Normal", Color.green, "System memory pressure is nominal.")
return (String(localized: "Normal"), Color.green, String(localized: "System memory pressure is nominal."))
case .warning:
return ("Warning", Color.orange, "Memory pressure is elevated. Closing apps may help.")
return (String(localized: "Warning"), Color.orange, String(localized: "Memory pressure is elevated. Closing apps may help."))
case .critical:
return ("Critical", Color.red, "Memory pressure is critical; the system may purge aggressively.")
return (String(localized: "Critical"), Color.red, String(localized: "Memory pressure is critical; the system may purge aggressively."))
}
}
@ -220,9 +220,9 @@ private struct SwapUsageView: View {
.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))
DetailRow(color: accentColor.opacity(0.85), label: String(localized: "Used"), value: StatsFormatting.bytes(swap.usedBytes))
DetailRow(color: accentColor.opacity(0.55), label: String(localized: "Free"), value: StatsFormatting.bytes(swap.freeBytes))
DetailRow(color: nil, label: String(localized: "Total"), value: StatsFormatting.bytes(swap.totalBytes))
SwapUsageBar(usedBytes: swap.usedBytes, totalBytes: swap.totalBytes, tint: accentColor)
.frame(height: 8)
} else {

View file

@ -28,7 +28,7 @@ struct NetworkStatsDetailView: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Network Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Network Overview"), padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkOverview(
download: statsManager.networkDownload,
upload: statsManager.networkUpload,
@ -41,7 +41,7 @@ struct NetworkStatsDetailView: View {
)
}
StatsCard(title: "Interfaces", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Interfaces"), padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkInterfacesCard(
interfaces: statsManager.networkInterfaces,
downloadColor: downloadColor,
@ -49,7 +49,7 @@ struct NetworkStatsDetailView: View {
)
}
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
StatsCard(title: String(localized: "Session Totals"), padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkTotalsView(totals: statsManager.networkTotals, downloadColor: downloadColor, uploadColor: uploadColor)
}
}
@ -101,8 +101,8 @@ private struct NetworkOverview: View {
Text(StatsFormatting.throughput(upload))
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary)
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgUpload))
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakUpload))
DetailRow(color: uploadColor.opacity(0.6), label: String(localized: "Average"), value: StatsFormatting.throughput(avgUpload))
DetailRow(color: uploadColor.opacity(0.45), label: String(localized: "Peak"), value: StatsFormatting.throughput(peakUpload))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
@ -116,8 +116,8 @@ private struct NetworkTotalsView: View {
var body: some View {
VStack(spacing: 10) {
DetailRow(color: downloadColor.opacity(0.75), label: "Downloaded", value: formattedBytes(totals.downloadedMB))
DetailRow(color: uploadColor.opacity(0.75), label: "Uploaded", value: formattedBytes(totals.uploadedMB))
DetailRow(color: downloadColor.opacity(0.75), label: String(localized: "Downloaded"), value: formattedBytes(totals.downloadedMB))
DetailRow(color: uploadColor.opacity(0.75), label: String(localized: "Uploaded"), value: formattedBytes(totals.uploadedMB))
}
}
@ -193,7 +193,7 @@ private struct NetworkInterfaceRow: View {
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.primary)
if interface.isActive {
Text("Active")
Text(String(localized: "stats_resource_active", defaultValue: "Active"))
.font(.caption2)
.foregroundColor(downloadColor)
.padding(.horizontal, 6)
@ -222,8 +222,8 @@ private struct NetworkInterfaceRow: View {
Spacer()
VStack(alignment: .trailing, spacing: 4) {
metricRow(label: "Down", value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
metricRow(label: "Up", value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
metricRow(label: String(localized: "Down"), value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
metricRow(label: String(localized: "Up"), value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
}
}
}

View file

@ -382,6 +382,36 @@ class CalendarManager: ObservableObject {
lastEventsFetchDate = Date()
}
func setCalendarsSelected(_ calendars: [CalendarModel], isSelected: Bool) async {
var selectionState = Defaults[.calendarSelectionState]
let ids = Set(calendars.map { $0.id })
switch selectionState {
case .all:
if !isSelected {
let identifiers = Set(allCalendars.map { $0.id }).subtracting(ids)
selectionState = .selected(identifiers)
}
case .selected(var identifiers):
if isSelected {
identifiers.formUnion(ids)
} else {
identifiers.subtract(ids)
}
if identifiers.isEmpty || identifiers.count == allCalendars.count {
selectionState = .all
} else {
selectionState = .selected(identifiers)
}
}
Defaults[.calendarSelectionState] = selectionState
updateSelectedCalendars()
await updateEvents(force: true)
await updateLockScreenEvents(force: true)
}
func setReminderCompleted(reminderID: String, completed: Bool) async {
await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed)
await updateEvents(force: true)

View file

@ -302,6 +302,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject {
private func applyCalendarPreviewEvents() {
let now = Date()
let reminderCalendar = CalendarModel(
accountName: "Preview",
id: "preview.reminders",
title: "Reminders",
color: .systemBlue,
@ -327,6 +328,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject {
)
let eventCalendar = CalendarModel(
accountName: "Preview",
id: "preview.calendar",
title: "Calendar",
color: .systemOrange,

View file

@ -23,6 +23,7 @@
import Cocoa
struct CalendarModel: Identifiable, Hashable {
let accountName: String
let id: String
let title: String
let color: NSColor