BugFix: remove the accidental stats in gitignore

This commit is contained in:
Hariharan Mudaliar 2025-10-15 22:31:28 +05:30
commit 05ec90e840
13 changed files with 1628 additions and 1 deletions

View 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
}
}
}
}

View 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
}
}

View file

@ -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

View 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
}
}
}

View 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)
}