mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
BugFix: remove the accidental stats in gitignore
This commit is contained in:
parent
742e34fbdc
commit
05ec90e840
13 changed files with 1628 additions and 1 deletions
131
DynamicIsland/components/Privacy/PrivacyLiveActivity.swift
Normal file
131
DynamicIsland/components/Privacy/PrivacyLiveActivity.swift
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// PrivacyLiveActivity.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for camera and microphone privacy indicators
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
struct PrivacyLiveActivity: View {
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
||||
@State private var isHovering: Bool = false
|
||||
@State private var gestureProgress: CGFloat = 0
|
||||
@State private var isExpanded: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left side - Recording pulsator (if recording is active)
|
||||
Color.clear
|
||||
.background {
|
||||
if recordingManager.isRecording && isExpanded {
|
||||
HStack {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.red.opacity(0.15))
|
||||
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 10, height: 10)
|
||||
.modifier(PulsingModifier())
|
||||
}
|
||||
.frame(width: vm.effectiveClosedNotchHeight - 12, height: vm.effectiveClosedNotchHeight - 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(width: recordingManager.isRecording && isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||
|
||||
// Center - Black fill
|
||||
Rectangle()
|
||||
.fill(.black)
|
||||
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0))
|
||||
|
||||
// Right side - Privacy indicators (camera and/or microphone)
|
||||
Color.clear
|
||||
.background {
|
||||
if isExpanded {
|
||||
HStack(spacing: 8) {
|
||||
// Microphone indicator (shows first if both active)
|
||||
if privacyManager.indicatorLayout.showsMicrophoneIndicator {
|
||||
PrivacyIcon(type: .microphone)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Camera indicator
|
||||
if privacyManager.indicatorLayout.showsCameraIndicator {
|
||||
PrivacyIcon(type: .camera)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 8)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||
}
|
||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0))
|
||||
.onAppear {
|
||||
withAnimation(.smooth(duration: 0.4)) {
|
||||
isExpanded = true
|
||||
}
|
||||
}
|
||||
.onChange(of: privacyManager.hasAnyIndicator) { _, newValue in
|
||||
if !newValue {
|
||||
withAnimation(.smooth(duration: 0.4)) {
|
||||
isExpanded = false
|
||||
}
|
||||
} else {
|
||||
withAnimation(.smooth(duration: 0.4)) {
|
||||
isExpanded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Individual privacy icon component
|
||||
struct PrivacyIcon: View {
|
||||
let type: PrivacyIndicatorType
|
||||
@State private var isPulsing = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Glow effect
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
Color.orange.opacity(0.4),
|
||||
Color.orange.opacity(0.2),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 10
|
||||
)
|
||||
)
|
||||
.frame(width: 20, height: 20)
|
||||
.opacity(isPulsing ? 0.8 : 0.5)
|
||||
|
||||
// Icon background
|
||||
Circle()
|
||||
.fill(Color.orange.opacity(0.2))
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
// Icon
|
||||
Image(systemName: type.icon)
|
||||
.font(.system(size: 8, weight: .semibold))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
|
||||
isPulsing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
268
DynamicIsland/components/PrivacyIndicatorIcon.swift
Normal file
268
DynamicIsland/components/PrivacyIndicatorIcon.swift
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
//
|
||||
// PrivacyIndicatorIcon.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created for privacy indicator feature
|
||||
// Visual component for camera and microphone privacy indicators
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Privacy Indicator Type
|
||||
enum PrivacyIndicatorType {
|
||||
case camera
|
||||
case microphone
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .camera:
|
||||
return "video.fill"
|
||||
case .microphone:
|
||||
return "mic.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .camera:
|
||||
return "Camera Active"
|
||||
case .microphone:
|
||||
return "Microphone Active"
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityLabel: String {
|
||||
switch self {
|
||||
case .camera:
|
||||
return "Camera is being used by an application"
|
||||
case .microphone:
|
||||
return "Microphone is being used by an application"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Privacy Indicator Icon View
|
||||
struct PrivacyIndicatorIcon: View {
|
||||
// MARK: - Properties
|
||||
let type: PrivacyIndicatorType
|
||||
let showLabel: Bool
|
||||
|
||||
@State private var isAnimating: Bool = false
|
||||
|
||||
// MARK: - Initialization
|
||||
init(type: PrivacyIndicatorType, showLabel: Bool = false) {
|
||||
self.type = type
|
||||
self.showLabel = showLabel
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
// Icon with glow effect
|
||||
ZStack {
|
||||
// Background glow
|
||||
Circle()
|
||||
.fill(
|
||||
RadialGradient(
|
||||
colors: [
|
||||
Color.orange.opacity(0.4),
|
||||
Color.orange.opacity(0.2),
|
||||
Color.clear
|
||||
],
|
||||
center: .center,
|
||||
startRadius: 0,
|
||||
endRadius: 12
|
||||
)
|
||||
)
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(isAnimating ? 0.8 : 0.5)
|
||||
|
||||
// Icon background circle
|
||||
Circle()
|
||||
.fill(Color.orange.opacity(0.2))
|
||||
.frame(width: 18, height: 18)
|
||||
|
||||
// Icon
|
||||
Image(systemName: type.icon)
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
// Optional label
|
||||
if showLabel {
|
||||
Text(type.label)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(.primary.opacity(0.8))
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(type.accessibilityLabel)
|
||||
.onAppear {
|
||||
startPulseAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animation
|
||||
private func startPulseAnimation() {
|
||||
withAnimation(
|
||||
.easeInOut(duration: 1.5)
|
||||
.repeatForever(autoreverses: true)
|
||||
) {
|
||||
isAnimating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Privacy Indicator Container
|
||||
/// Container view that shows both camera and microphone indicators based on layout
|
||||
struct PrivacyIndicatorContainer: View {
|
||||
// MARK: - Environment
|
||||
@EnvironmentObject var privacyManager: PrivacyIndicatorManager
|
||||
|
||||
// MARK: - Properties
|
||||
let showLabels: Bool
|
||||
let spacing: CGFloat
|
||||
|
||||
// MARK: - Initialization
|
||||
init(showLabels: Bool = false, spacing: CGFloat = 8) {
|
||||
self.showLabels = showLabels
|
||||
self.spacing = spacing
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
HStack(spacing: spacing) {
|
||||
// Microphone indicator (on the left when both are shown)
|
||||
if privacyManager.indicatorLayout.showsMicrophoneIndicator {
|
||||
PrivacyIndicatorIcon(type: .microphone, showLabel: showLabels)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Camera indicator (on the right when both are shown)
|
||||
if privacyManager.indicatorLayout.showsCameraIndicator {
|
||||
PrivacyIndicatorIcon(type: .camera, showLabel: showLabels)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.animation(.smooth(duration: 0.3), value: privacyManager.indicatorLayout)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Privacy Indicator Detail View
|
||||
/// Detailed view showing privacy indicator status (for settings/tips)
|
||||
struct PrivacyIndicatorDetailView: View {
|
||||
@EnvironmentObject var privacyManager: PrivacyIndicatorManager
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Privacy Indicators")
|
||||
.font(.headline)
|
||||
|
||||
// Camera status
|
||||
HStack(spacing: 12) {
|
||||
PrivacyIndicatorIcon(type: .camera)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Camera")
|
||||
.font(.subheadline.weight(.medium))
|
||||
|
||||
Text(privacyManager.cameraActive ? "Currently active" : "Not active")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Circle()
|
||||
.fill(privacyManager.cameraActive ? Color.green : Color.gray.opacity(0.3))
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color.primary.opacity(0.05))
|
||||
.cornerRadius(10)
|
||||
|
||||
// Microphone status
|
||||
HStack(spacing: 12) {
|
||||
PrivacyIndicatorIcon(type: .microphone)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Microphone")
|
||||
.font(.subheadline.weight(.medium))
|
||||
|
||||
Text(privacyManager.microphoneActive ? "Currently active" : "Not active")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Circle()
|
||||
.fill(privacyManager.microphoneActive ? Color.green : Color.gray.opacity(0.3))
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color.primary.opacity(0.05))
|
||||
.cornerRadius(10)
|
||||
|
||||
// Info text
|
||||
Text("These indicators appear in the notch when apps are using your camera or microphone, matching macOS's native privacy indicators.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
#Preview("Single Indicators") {
|
||||
VStack(spacing: 20) {
|
||||
PrivacyIndicatorIcon(type: .camera)
|
||||
PrivacyIndicatorIcon(type: .microphone)
|
||||
PrivacyIndicatorIcon(type: .camera, showLabel: true)
|
||||
PrivacyIndicatorIcon(type: .microphone, showLabel: true)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
}
|
||||
|
||||
#Preview("Container") {
|
||||
let manager = PrivacyIndicatorManager.shared
|
||||
|
||||
VStack(spacing: 20) {
|
||||
Text("Both Active")
|
||||
PrivacyIndicatorContainer()
|
||||
.onAppear {
|
||||
manager.cameraActive = true
|
||||
manager.microphoneActive = true
|
||||
}
|
||||
|
||||
Text("Camera Only")
|
||||
PrivacyIndicatorContainer()
|
||||
.onAppear {
|
||||
manager.cameraActive = true
|
||||
manager.microphoneActive = false
|
||||
}
|
||||
|
||||
Text("Microphone Only")
|
||||
PrivacyIndicatorContainer()
|
||||
.onAppear {
|
||||
manager.cameraActive = false
|
||||
manager.microphoneActive = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
.environmentObject(manager)
|
||||
}
|
||||
|
||||
#Preview("Detail View") {
|
||||
let manager = PrivacyIndicatorManager.shared
|
||||
|
||||
PrivacyIndicatorDetailView()
|
||||
.padding()
|
||||
.environmentObject(manager)
|
||||
.onAppear {
|
||||
manager.cameraActive = true
|
||||
manager.microphoneActive = false
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ struct RecordingLiveActivity: View {
|
|||
}
|
||||
|
||||
// Pulsing animation modifier for recording indicator
|
||||
// Note: Also used by PrivacyLiveActivity
|
||||
struct PulsingModifier: ViewModifier {
|
||||
@State private var isPulsing = false
|
||||
|
||||
|
|
|
|||
61
DynamicIsland/components/Stats/ProcessStats.swift
Normal file
61
DynamicIsland/components/Stats/ProcessStats.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// ProcessStats.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Process information structure for stats monitoring
|
||||
// Adapted from boring.notch implementation
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
// Process information structure
|
||||
struct ProcessStats: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let pid: Int32
|
||||
let name: String
|
||||
let cpuUsage: Double
|
||||
let memoryUsage: UInt64 // in bytes
|
||||
let icon: NSImage?
|
||||
|
||||
var memoryUsageString: String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useMB, .useGB]
|
||||
formatter.countStyle = .memory
|
||||
return formatter.string(fromByteCount: Int64(memoryUsage))
|
||||
}
|
||||
|
||||
var cpuUsageString: String {
|
||||
return String(format: "%.1f%%", cpuUsage)
|
||||
}
|
||||
}
|
||||
|
||||
enum ProcessRankingType {
|
||||
case cpu
|
||||
case memory
|
||||
case gpu
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .cpu: return "CPU Usage"
|
||||
case .memory: return "Memory Usage"
|
||||
case .gpu: return "GPU Usage"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .cpu: return "cpu"
|
||||
case .memory: return "memorychip"
|
||||
case .gpu: return "display"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .cpu: return .blue
|
||||
case .memory: return .green
|
||||
case .gpu: return .purple
|
||||
}
|
||||
}
|
||||
}
|
||||
196
DynamicIsland/components/Stats/RankedProcessPopover.swift
Normal file
196
DynamicIsland/components/Stats/RankedProcessPopover.swift
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// RankedProcessPopover.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Popover component for displaying ranked processes by different metrics
|
||||
// Adapted from boring.notch implementation
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RankedProcessPopover: View {
|
||||
let rankingType: ProcessRankingType
|
||||
@ObservedObject var statsManager = StatsManager.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var isHovering = false
|
||||
|
||||
// Callback to notify parent about hover state
|
||||
var onHoverChange: ((Bool) -> Void)?
|
||||
|
||||
private var rankedProcesses: [ProcessStats] {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return statsManager.getProcessesRankedByCPU()
|
||||
case .memory:
|
||||
return statsManager.getProcessesRankedByMemory()
|
||||
case .gpu:
|
||||
return statsManager.getProcessesRankedByGPU()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Header
|
||||
HStack {
|
||||
Image(systemName: rankingType.icon)
|
||||
.font(.title3)
|
||||
.foregroundColor(rankingType.color)
|
||||
|
||||
Text("Top Processes by \(rankingType.title)")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Divider()
|
||||
|
||||
// Process list
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 6) {
|
||||
ForEach(Array(rankedProcesses.prefix(15).enumerated()), id: \.element.id) { index, process in
|
||||
RankedProcessRow(
|
||||
process: process,
|
||||
rank: index + 1,
|
||||
rankingType: rankingType
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
|
||||
// Footer with refresh button
|
||||
HStack {
|
||||
Text("\(rankedProcesses.count) processes")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Refresh") {
|
||||
// Trigger stats update
|
||||
if statsManager.isMonitoring {
|
||||
// Stats will auto-refresh
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(rankingType.color)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 320)
|
||||
.frame(minHeight: 200)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
onHoverChange?(hovering)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RankedProcessRow: View {
|
||||
let process: ProcessStats
|
||||
let rank: Int
|
||||
let rankingType: ProcessRankingType
|
||||
@State private var isHovered = false
|
||||
|
||||
private var primaryValue: String {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return process.cpuUsageString
|
||||
case .memory:
|
||||
return process.memoryUsageString
|
||||
case .gpu:
|
||||
return process.cpuUsageString // Using CPU as proxy for GPU
|
||||
}
|
||||
}
|
||||
|
||||
private var secondaryValue: String {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return process.memoryUsageString
|
||||
case .memory:
|
||||
return process.cpuUsageString
|
||||
case .gpu:
|
||||
return process.memoryUsageString
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
// Rank
|
||||
Text("\(rank)")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(rankingType.color)
|
||||
.frame(width: 24, alignment: .center)
|
||||
|
||||
// App icon
|
||||
Group {
|
||||
if let icon = process.icon {
|
||||
Image(nsImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
Image(systemName: "app")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
// Process info
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(process.name)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text(primaryValue)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(rankingType.color)
|
||||
|
||||
Text(secondaryValue)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// PID
|
||||
Text("PID: \(process.pid)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.7)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isHovered ? Color(NSColor.selectedControlColor).opacity(0.2) : Color.clear)
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(.easeInOut(duration: 0.15)) {
|
||||
isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RankedProcessPopover(rankingType: .cpu)
|
||||
.background(Color.black)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue