mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge remote-tracking branch 'origin/main' into pr-361
# Conflicts: # DynamicIsland/components/Settings/SettingsView.swift
This commit is contained in:
commit
73d076636f
14 changed files with 6786 additions and 929 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
import Cocoa
|
||||
|
||||
struct CalendarModel: Identifiable, Hashable {
|
||||
let accountName: String
|
||||
let id: String
|
||||
let title: String
|
||||
let color: NSColor
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue