mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #127 from Ebullioscopic/revamp/essentials
Minor UI Fixes, BugFixes and Additions
This commit is contained in:
commit
6d6ffff5b0
27 changed files with 624 additions and 295 deletions
|
|
@ -126,6 +126,20 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
return cornerRadiusInsets
|
return cornerRadiusInsets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var currentShadowPadding: CGFloat {
|
||||||
|
notchShadowPaddingValue(isMinimalistic: enableMinimalisticUI)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentNotchShape: NotchShape {
|
||||||
|
let topRadius = (vm.notchState == .open && Defaults[.cornerRadiusScaling])
|
||||||
|
? activeCornerRadiusInsets.opened.top
|
||||||
|
: activeCornerRadiusInsets.closed.top
|
||||||
|
let bottomRadius = (vm.notchState == .open && Defaults[.cornerRadiusScaling])
|
||||||
|
? activeCornerRadiusInsets.opened.bottom
|
||||||
|
: activeCornerRadiusInsets.closed.bottom
|
||||||
|
return NotchShape(topCornerRadius: topRadius, bottomCornerRadius: bottomRadius)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let interactionsEnabled = !lockScreenManager.isLocked
|
let interactionsEnabled = !lockScreenManager.isLocked
|
||||||
|
|
@ -139,16 +153,20 @@ struct ContentView: View {
|
||||||
)
|
)
|
||||||
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
.padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.mask {
|
.clipShape(currentNotchShape)
|
||||||
((vm.notchState == .open) && Defaults[.cornerRadiusScaling])
|
.compositingGroup()
|
||||||
? NotchShape(topCornerRadius: activeCornerRadiusInsets.opened.top, bottomCornerRadius: activeCornerRadiusInsets.opened.bottom)
|
.shadow(
|
||||||
.drawingGroup()
|
color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow])
|
||||||
: NotchShape(topCornerRadius: activeCornerRadiusInsets.closed.top, bottomCornerRadius: activeCornerRadiusInsets.closed.bottom)
|
? .black.opacity(0.6)
|
||||||
.drawingGroup()
|
: .clear,
|
||||||
}
|
radius: Defaults[.cornerRadiusScaling] ? 10 : 5
|
||||||
.padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0)
|
)
|
||||||
? zeroHeightHoverPadding
|
.padding(.bottom,
|
||||||
: 0
|
currentShadowPadding + (
|
||||||
|
vm.notchState == .open && Defaults[.extendHoverArea]
|
||||||
|
? 0
|
||||||
|
: (vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
mainLayout
|
mainLayout
|
||||||
|
|
@ -280,7 +298,11 @@ struct ContentView: View {
|
||||||
// .keyboardShortcut("E", modifiers: .command)
|
// .keyboardShortcut("E", modifiers: .command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top)
|
.frame(
|
||||||
|
maxWidth: dynamicNotchSize.width,
|
||||||
|
maxHeight: dynamicNotchSize.height + currentShadowPadding,
|
||||||
|
alignment: .top
|
||||||
|
)
|
||||||
.animation(dynamicNotchResizeAnimation, value: dynamicNotchSize)
|
.animation(dynamicNotchResizeAnimation, value: dynamicNotchSize)
|
||||||
.animation(.easeInOut(duration: 0.4), value: coordinator.currentView)
|
.animation(.easeInOut(duration: 0.4), value: coordinator.currentView)
|
||||||
.environmentObject(privacyManager)
|
.environmentObject(privacyManager)
|
||||||
|
|
@ -291,7 +313,6 @@ struct ContentView: View {
|
||||||
vm.shouldRecheckHover.toggle()
|
vm.shouldRecheckHover.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5)
|
|
||||||
.background(dragDetector)
|
.background(dragDetector)
|
||||||
.environmentObject(vm)
|
.environmentObject(vm)
|
||||||
.environmentObject(webcamManager)
|
.environmentObject(webcamManager)
|
||||||
|
|
|
||||||
|
|
@ -276,33 +276,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight)
|
return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use minimalistic or normal size based on settings
|
// Use minimalistic or normal size based on settings, then adjust for stats layout
|
||||||
var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
let adjustedContentSize = statsAdjustedNotchSize(
|
||||||
// Only apply dynamic sizing when on stats tab and stats are enabled
|
from: baseSize,
|
||||||
guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else {
|
isStatsTabActive: coordinator.currentView == .stats,
|
||||||
return baseSize
|
secondRowProgress: coordinator.statsSecondRowExpansion
|
||||||
}
|
)
|
||||||
|
return addShadowPadding(
|
||||||
let enabledGraphsCount = [
|
to: adjustedContentSize,
|
||||||
Defaults[.showCpuGraph],
|
isMinimalistic: Defaults[.enableMinimalisticUI]
|
||||||
Defaults[.showMemoryGraph],
|
)
|
||||||
Defaults[.showGpuGraph],
|
|
||||||
Defaults[.showNetworkGraph],
|
|
||||||
Defaults[.showDiskGraph]
|
|
||||||
].filter { $0 }.count
|
|
||||||
|
|
||||||
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
|
||||||
var requiredHeight = baseSize.height
|
|
||||||
|
|
||||||
if enabledGraphsCount >= 4 {
|
|
||||||
// Two rows needed - add height for second row plus spacing
|
|
||||||
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
|
||||||
requiredHeight = baseSize.height + extraHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Width stays constant - no horizontal expansion
|
|
||||||
return CGSize(width: baseSize.width, height: requiredHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
|
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
|
||||||
|
|
@ -781,6 +765,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
window.isRestorable = false
|
window.isRestorable = false
|
||||||
window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow")
|
window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow")
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
|
||||||
|
|
||||||
onboardingWindowController = NSWindowController(window: window)
|
onboardingWindowController = NSWindowController(window: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,16 @@ struct ExpandedItem {
|
||||||
class DynamicIslandViewCoordinator: ObservableObject {
|
class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
static let shared = DynamicIslandViewCoordinator()
|
static let shared = DynamicIslandViewCoordinator()
|
||||||
|
|
||||||
@Published var currentView: NotchViews = .home
|
@Published var currentView: NotchViews = .home {
|
||||||
|
didSet {
|
||||||
|
handleStatsTabTransition(from: oldValue, to: currentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var statsSecondRowExpansion: CGFloat = 1
|
||||||
|
private var statsSecondRowWorkItem: DispatchWorkItem?
|
||||||
|
private let statsSecondRowRevealDelay: TimeInterval = 0.5
|
||||||
|
private let statsSecondRowAnimationDuration: TimeInterval = 0.3
|
||||||
|
|
||||||
|
|
||||||
@AppStorage("firstLaunch") var firstLaunch: Bool = true
|
@AppStorage("firstLaunch") var firstLaunch: Bool = true
|
||||||
|
|
@ -91,6 +100,27 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
private init() {
|
private init() {
|
||||||
selectedScreen = preferredScreen
|
selectedScreen = preferredScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
|
||||||
|
guard oldValue != newValue else { return }
|
||||||
|
statsSecondRowWorkItem?.cancel()
|
||||||
|
if newValue == .stats && Defaults[.enableStatsFeature] {
|
||||||
|
statsSecondRowExpansion = 0
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
withAnimation(.easeInOut(duration: self.statsSecondRowAnimationDuration)) {
|
||||||
|
self.statsSecondRowExpansion = 1
|
||||||
|
}
|
||||||
|
self.statsSecondRowWorkItem = nil
|
||||||
|
}
|
||||||
|
statsSecondRowWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + statsSecondRowRevealDelay, execute: workItem)
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
statsSecondRowExpansion = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") {
|
func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") {
|
||||||
let resolvedDuration: TimeInterval
|
let resolvedDuration: TimeInterval
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,16 @@
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
|
||||||
|
private func applyClipboardCornerMask(_ view: NSView, radius: CGFloat) {
|
||||||
|
view.wantsLayer = true
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
view.layer?.cornerRadius = radius
|
||||||
|
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
view.layer?.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ClipboardPanel: NSPanel {
|
class ClipboardPanel: NSPanel {
|
||||||
|
|
||||||
|
|
@ -51,10 +60,8 @@ class ClipboardPanel: NSPanel {
|
||||||
.stationary,
|
.stationary,
|
||||||
.fullScreenAuxiliary // Float above full-screen apps
|
.fullScreenAuxiliary // Float above full-screen apps
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screenshot protection
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenshotProtection()
|
|
||||||
setupScreenshotProtectionObserver()
|
|
||||||
|
|
||||||
// Accept mouse moved events for proper hover behavior
|
// Accept mouse moved events for proper hover behavior
|
||||||
acceptsMouseMovedEvents = true
|
acceptsMouseMovedEvents = true
|
||||||
|
|
@ -66,6 +73,7 @@ class ClipboardPanel: NSPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
|
applyClipboardCornerMask(hostingView, radius: 12)
|
||||||
self.contentView = hostingView
|
self.contentView = hostingView
|
||||||
|
|
||||||
// Set initial size
|
// Set initial size
|
||||||
|
|
@ -140,28 +148,6 @@ class ClipboardPanel: NSPanel {
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenshotProtectionObserver() {
|
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenshotProtection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenshotProtection() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Exclude from screenshots and screen recordings
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ClipboardPanel: Protected from screenshots and screen recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screenshot inclusion
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ClipboardPanel: Visible in screenshots and screen recordings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClipboardPanelView: View {
|
struct ClipboardPanelView: View {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,16 @@ import SwiftUI
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
|
private func applyColorPickerCornerMask(_ view: NSView, radius: CGFloat) {
|
||||||
|
view.wantsLayer = true
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
view.layer?.cornerRadius = radius
|
||||||
|
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
view.layer?.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ColorPickerPanelManager: ObservableObject {
|
class ColorPickerPanelManager: ObservableObject {
|
||||||
static let shared = ColorPickerPanelManager()
|
static let shared = ColorPickerPanelManager()
|
||||||
|
|
||||||
|
|
@ -93,9 +103,7 @@ class ColorPickerPanel: NSPanel {
|
||||||
.fullScreenAuxiliary // Float above full-screen apps
|
.fullScreenAuxiliary // Float above full-screen apps
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
// Accept mouse moved events for proper hover behavior
|
// Accept mouse moved events for proper hover behavior
|
||||||
acceptsMouseMovedEvents = true
|
acceptsMouseMovedEvents = true
|
||||||
|
|
@ -105,6 +113,7 @@ class ColorPickerPanel: NSPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hostingView = NSHostingView(rootView: panelView)
|
let hostingView = NSHostingView(rootView: panelView)
|
||||||
|
applyColorPickerCornerMask(hostingView, radius: 12)
|
||||||
contentView = hostingView
|
contentView = hostingView
|
||||||
|
|
||||||
// Set initial size
|
// Set initial size
|
||||||
|
|
@ -161,27 +170,8 @@ class ColorPickerPanel: NSPanel {
|
||||||
saveCurrentPosition()
|
saveCurrentPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
ScreenCaptureVisibilityManager.shared.unregister(self)
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ColorPickerPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ColorPickerPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ class DynamicIslandWindow: NSPanel {
|
||||||
isReleasedWhenClosed = false
|
isReleasedWhenClosed = false
|
||||||
level = .mainMenu + 3
|
level = .mainMenu + 3
|
||||||
hasShadow = false
|
hasShadow = false
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .entireInterface)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var canBecomeKey: Bool {
|
override var canBecomeKey: Bool {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ struct NotchStatsView: View {
|
||||||
@State private var statsHoverGraceActive = false
|
@State private var statsHoverGraceActive = false
|
||||||
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
|
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
|
||||||
var availableGraphs: [GraphData] {
|
var availableGraphs: [GraphData] {
|
||||||
var graphs: [GraphData] = []
|
var graphs: [GraphData] = []
|
||||||
|
|
@ -148,18 +149,35 @@ struct NotchStatsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if graphCount == 4 {
|
} else if graphCount == 4 {
|
||||||
// 4 graphs: 2x2 grid
|
// 4 graphs: second row scales from zero height on tab open
|
||||||
LazyVGrid(
|
VStack(spacing: 0) {
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
LazyVGrid(
|
||||||
spacing: 12
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
) {
|
spacing: 12
|
||||||
ForEach(0..<graphCount, id: \.self) { index in
|
) {
|
||||||
graphViewForIndex(index)
|
ForEach(0..<2, id: \.self) { index in
|
||||||
|
graphViewForIndex(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsCollapsibleRow(
|
||||||
|
expansion: coordinator.statsSecondRowExpansion,
|
||||||
|
height: statsSecondRowContentHeight + statsGridSpacingHeight
|
||||||
|
) {
|
||||||
|
LazyVGrid(
|
||||||
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
|
spacing: 12
|
||||||
|
) {
|
||||||
|
ForEach(2..<graphCount, id: \.self) { index in
|
||||||
|
graphViewForIndex(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, statsGridSpacingHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
|
// 5 graphs: First row 3 graphs, second row 2 graphs (half-width each)
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 0) {
|
||||||
// First row: 3 graphs
|
// First row: 3 graphs
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||||
|
|
@ -169,15 +187,20 @@ struct NotchStatsView: View {
|
||||||
graphViewForIndex(index)
|
graphViewForIndex(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second row: 2 graphs (half-width each)
|
// Second row: 2 graphs (half-width each)
|
||||||
LazyVGrid(
|
StatsCollapsibleRow(
|
||||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
expansion: coordinator.statsSecondRowExpansion,
|
||||||
spacing: 8
|
height: statsSecondRowContentHeight + statsGridSpacingHeight
|
||||||
) {
|
) {
|
||||||
ForEach(3..<graphCount, id: \.self) { index in
|
LazyVGrid(
|
||||||
graphViewForIndex(index)
|
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||||
|
spacing: 8
|
||||||
|
) {
|
||||||
|
ForEach(3..<graphCount, id: \.self) { index in
|
||||||
|
graphViewForIndex(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, statsGridSpacingHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -438,8 +461,53 @@ struct NotchStatsView: View {
|
||||||
.onChange(of: isHoveringDiskPopover) { _, _ in
|
.onChange(of: isHoveringDiskPopover) { _, _ in
|
||||||
updateStatsPopoverState()
|
updateStatsPopoverState()
|
||||||
}
|
}
|
||||||
|
.onChange(of: coordinator.statsSecondRowExpansion) { _, newValue in
|
||||||
|
let shouldHold = availableGraphs.count >= 4 && newValue < 0.999
|
||||||
|
if shouldHold {
|
||||||
|
isResizingForStats = true
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
isResizingForStats = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private struct StatsCollapsibleRow<Content: View>: View {
|
||||||
|
let expansion: CGFloat
|
||||||
|
let height: CGFloat
|
||||||
|
let content: () -> Content
|
||||||
|
private let minVisibleHeight: CGFloat = 0.5
|
||||||
|
|
||||||
|
init(expansion: CGFloat, height: CGFloat, @ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.expansion = expansion
|
||||||
|
self.height = height
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let clamped = max(0, min(expansion, 1))
|
||||||
|
let targetHeight = clamped == 0 ? 0 : max(height * clamped, minVisibleHeight)
|
||||||
|
|
||||||
|
return Group {
|
||||||
|
if targetHeight == 0 {
|
||||||
|
Color.clear.frame(height: 0)
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
.frame(height: height, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.mask(
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: targetHeight)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
)
|
||||||
|
.frame(height: targetHeight, alignment: .top)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.3), value: clamped)
|
||||||
|
}
|
||||||
|
}
|
||||||
private func activateStatsHoverGrace(duration: TimeInterval = 0.45) {
|
private func activateStatsHoverGrace(duration: TimeInterval = 0.45) {
|
||||||
statsHoverGraceWorkItem?.cancel()
|
statsHoverGraceWorkItem?.cancel()
|
||||||
statsHoverGraceActive = true
|
statsHoverGraceActive = true
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
|
private func applyChatPanelCornerMask(_ view: NSView, radius: CGFloat) {
|
||||||
|
view.wantsLayer = true
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
view.layer?.cornerRadius = radius
|
||||||
|
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
view.layer?.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Chat Messages Panel (Left Side)
|
// MARK: - Chat Messages Panel (Left Side)
|
||||||
class ChatMessagesPanel: NSPanel {
|
class ChatMessagesPanel: NSPanel {
|
||||||
|
|
||||||
|
|
@ -47,9 +57,7 @@ class ChatMessagesPanel: NSPanel {
|
||||||
.fullScreenAuxiliary
|
.fullScreenAuxiliary
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
acceptsMouseMovedEvents = true
|
acceptsMouseMovedEvents = true
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +65,7 @@ class ChatMessagesPanel: NSPanel {
|
||||||
private func setupContentView() {
|
private func setupContentView() {
|
||||||
let contentView = ChatMessagesView()
|
let contentView = ChatMessagesView()
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
|
applyChatPanelCornerMask(hostingView, radius: 16)
|
||||||
self.contentView = hostingView
|
self.contentView = hostingView
|
||||||
|
|
||||||
// Set size for chat messages panel (wider and taller)
|
// Set size for chat messages panel (wider and taller)
|
||||||
|
|
@ -78,27 +87,8 @@ class ChatMessagesPanel: NSPanel {
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
ScreenCaptureVisibilityManager.shared.unregister(self)
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ChatMessagesPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ChatMessagesPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,9 +142,7 @@ class ChatInputPanel: NSPanel {
|
||||||
.fullScreenAuxiliary
|
.fullScreenAuxiliary
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
acceptsMouseMovedEvents = true
|
acceptsMouseMovedEvents = true
|
||||||
}
|
}
|
||||||
|
|
@ -162,6 +150,7 @@ class ChatInputPanel: NSPanel {
|
||||||
private func setupContentView() {
|
private func setupContentView() {
|
||||||
let contentView = ChatInputView()
|
let contentView = ChatInputView()
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
|
applyChatPanelCornerMask(hostingView, radius: 16)
|
||||||
self.contentView = hostingView
|
self.contentView = hostingView
|
||||||
|
|
||||||
// Set compact size for single-line input panel
|
// Set compact size for single-line input panel
|
||||||
|
|
@ -183,27 +172,8 @@ class ChatInputPanel: NSPanel {
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
ScreenCaptureVisibilityManager.shared.unregister(self)
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ChatInputPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ChatInputPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -786,45 +756,23 @@ struct ScreenshotPopoverBackground: NSViewRepresentable {
|
||||||
func makeNSView(context: Context) -> NSView {
|
func makeNSView(context: Context) -> NSView {
|
||||||
let view = NSView()
|
let view = NSView()
|
||||||
|
|
||||||
// Apply screen capture protection like our other panels
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let window = view.window {
|
guard let window = view.window else { return }
|
||||||
self.updateScreenCaptureVisibility(window: window)
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
|
||||||
self.setupScreenCaptureObserver(window: window)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ nsView: NSView, context: Context) {
|
func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
// Update screen capture protection when view updates
|
|
||||||
if let window = nsView.window {
|
if let window = nsView.window {
|
||||||
updateScreenCaptureVisibility(window: window)
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility(window: NSWindow) {
|
static func dismantleNSView(_ nsView: NSView, coordinator: ()) {
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
if let window = nsView.window {
|
||||||
|
ScreenCaptureVisibilityManager.shared.unregister(window)
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
window.sharingType = .none
|
|
||||||
print("🙈 ScreenshotPopover: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
window.sharingType = .readOnly
|
|
||||||
print("👁️ ScreenshotPopover: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupScreenCaptureObserver(window: NSWindow) {
|
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak window] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let window = window else { return }
|
|
||||||
self.updateScreenCaptureVisibility(window: window)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,16 @@ import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
|
private func applyModelSelectionCornerMask(_ view: NSView, radius: CGFloat) {
|
||||||
|
view.wantsLayer = true
|
||||||
|
view.layer?.masksToBounds = true
|
||||||
|
view.layer?.cornerRadius = radius
|
||||||
|
view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
view.layer?.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Model Selection Panel
|
// MARK: - Model Selection Panel
|
||||||
class ModelSelectionPanel: NSPanel {
|
class ModelSelectionPanel: NSPanel {
|
||||||
|
|
||||||
|
|
@ -58,9 +68,7 @@ class ModelSelectionPanel: NSPanel {
|
||||||
.fullScreenAuxiliary
|
.fullScreenAuxiliary
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
acceptsMouseMovedEvents = true
|
acceptsMouseMovedEvents = true
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +76,7 @@ class ModelSelectionPanel: NSPanel {
|
||||||
private func setupContentView() {
|
private func setupContentView() {
|
||||||
let contentView = ModelSelectionView()
|
let contentView = ModelSelectionView()
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
let hostingView = NSHostingView(rootView: contentView)
|
||||||
|
applyModelSelectionCornerMask(hostingView, radius: 16)
|
||||||
self.contentView = hostingView
|
self.contentView = hostingView
|
||||||
|
|
||||||
// Set size for model selection panel
|
// Set size for model selection panel
|
||||||
|
|
@ -89,27 +98,8 @@ class ModelSelectionPanel: NSPanel {
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
ScreenCaptureVisibilityManager.shared.unregister(self)
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ModelSelectionPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ModelSelectionPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,7 @@ class ScreenAssistantPanel: NSPanel {
|
||||||
.fullScreenAuxiliary
|
.fullScreenAuxiliary
|
||||||
]
|
]
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
// Set minimal size since this is just a wrapper
|
// Set minimal size since this is just a wrapper
|
||||||
setContentSize(CGSize(width: 1, height: 1))
|
setContentSize(CGSize(width: 1, height: 1))
|
||||||
|
|
@ -77,27 +75,8 @@ class ScreenAssistantPanel: NSPanel {
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
ScreenCaptureVisibilityManager.shared.unregister(self)
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 ScreenAssistantPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ ScreenAssistantPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ struct GeneralSettings: View {
|
||||||
NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil)
|
NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil)
|
||||||
}
|
}
|
||||||
.disabled(showOnAllDisplays)
|
.disabled(showOnAllDisplays)
|
||||||
Defaults.Toggle("Hide panels from screenshots & screen recordings", key: .hidePanelsFromScreenCapture)
|
Defaults.Toggle("Hide Dynamic Island during screenshots & recordings", key: .hideDynamicIslandFromScreenCapture)
|
||||||
} header: {
|
} header: {
|
||||||
Text("System features")
|
Text("System features")
|
||||||
}
|
}
|
||||||
|
|
@ -356,6 +356,7 @@ struct GeneralSettings: View {
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity)
|
Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity)
|
||||||
|
Defaults.Toggle("Play lock/unlock sounds", key: .enableLockSounds)
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
||||||
ForEach(LockScreenGlassStyle.allCases) { style in
|
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
|
||||||
import Sparkle
|
import Sparkle
|
||||||
|
|
||||||
class SettingsWindowController: NSWindowController {
|
class SettingsWindowController: NSWindowController {
|
||||||
|
|
@ -65,9 +64,7 @@ class SettingsWindowController: NSWindowController {
|
||||||
// Handle window closing
|
// Handle window closing
|
||||||
window.delegate = self
|
window.delegate = self
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showWindow() {
|
func showWindow() {
|
||||||
|
|
@ -108,26 +105,9 @@ class SettingsWindowController: NSWindowController {
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
deinit {
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
if let window = window {
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
ScreenCaptureVisibilityManager.shared.unregister(window)
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
window?.sharingType = .none
|
|
||||||
print("🙈 SettingsWindow: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
window?.sharingType = .readOnly
|
|
||||||
print("👁️ SettingsWindow: Visible in screen capture and recordings")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift
Normal file
51
DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import AppKit
|
||||||
|
import Combine
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
enum ScreenCaptureScope: Int {
|
||||||
|
case panelsOnly
|
||||||
|
case entireInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ScreenCaptureVisibilityManager {
|
||||||
|
static let shared = ScreenCaptureVisibilityManager()
|
||||||
|
|
||||||
|
private let scopedWindows = NSMapTable<NSWindow, NSNumber>(keyOptions: .weakMemory, valueOptions: .strongMemory)
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
let interfacePublisher = Defaults.publisher(.hideDynamicIslandFromScreenCapture)
|
||||||
|
.map { _ in () }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
interfacePublisher
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] in
|
||||||
|
self?.updateAllWindows()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func register(_ window: NSWindow, scope: ScreenCaptureScope) {
|
||||||
|
scopedWindows.setObject(NSNumber(value: scope.rawValue), forKey: window)
|
||||||
|
applyVisibility(to: window, scope: scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unregister(_ window: NSWindow) {
|
||||||
|
scopedWindows.removeObject(forKey: window)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAllWindows() {
|
||||||
|
guard let windows = scopedWindows.keyEnumerator().allObjects as? [NSWindow] else { return }
|
||||||
|
for window in windows {
|
||||||
|
guard let raw = scopedWindows.object(forKey: window)?.intValue,
|
||||||
|
let scope = ScreenCaptureScope(rawValue: raw) else { continue }
|
||||||
|
applyVisibility(to: window, scope: scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyVisibility(to window: NSWindow, scope _: ScreenCaptureScope) {
|
||||||
|
let shouldHide = Defaults[.hideDynamicIslandFromScreenCapture]
|
||||||
|
window.sharingType = shouldHide ? .none : .readOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
DynamicIsland/lock.mp3
Normal file
BIN
DynamicIsland/lock.mp3
Normal file
Binary file not shown.
|
|
@ -40,6 +40,10 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
private var missingBatteryLog: Set<String> = []
|
private var missingBatteryLog: Set<String> = []
|
||||||
private var lastBatteryStatusUpdate: Date?
|
private var lastBatteryStatusUpdate: Date?
|
||||||
private let batteryStatusUpdateInterval: TimeInterval = 20
|
private let batteryStatusUpdateInterval: TimeInterval = 20
|
||||||
|
private let pmsetFetchQueue = DispatchQueue(label: "com.dynamicisland.bluetooth.pmset", qos: .utility)
|
||||||
|
private var isPmsetRefreshInFlight = false
|
||||||
|
private var lastPmsetRefreshDate: Date?
|
||||||
|
private let pmsetRefreshCooldown: TimeInterval = 5
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
private init() {
|
private init() {
|
||||||
|
|
@ -423,7 +427,7 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
triggerLiveBatteryRefreshIfNeeded()
|
triggerLiveBatteryRefreshIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyConnectedDeviceBatteryLevels() {
|
private func applyConnectedDeviceBatteryLevels(triggerPmsetFallback: Bool = true) {
|
||||||
guard !connectedDevices.isEmpty else {
|
guard !connectedDevices.isEmpty else {
|
||||||
lastConnectedDevice = nil
|
lastConnectedDevice = nil
|
||||||
return
|
return
|
||||||
|
|
@ -446,6 +450,11 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
if let last = updatedDevices.last {
|
if let last = updatedDevices.last {
|
||||||
lastConnectedDevice = last
|
lastConnectedDevice = last
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if triggerPmsetFallback,
|
||||||
|
updatedDevices.contains(where: { $0.batteryLevel == nil }) {
|
||||||
|
requestPmsetFallback(reason: "missing battery after refresh")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bestBatteryLevel(for device: BluetoothAudioDevice) -> Int? {
|
private func bestBatteryLevel(for device: BluetoothAudioDevice) -> Int? {
|
||||||
|
|
@ -456,6 +465,44 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
?? device.batteryLevel
|
?? device.batteryLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func requestPmsetFallback(reason: String) {
|
||||||
|
guard connectedDevices.contains(where: { $0.batteryLevel == nil }) else { return }
|
||||||
|
guard !isPmsetRefreshInFlight else { return }
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
|
if let lastPmsetRefreshDate,
|
||||||
|
now.timeIntervalSince(lastPmsetRefreshDate) < pmsetRefreshCooldown {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPmsetRefreshInFlight = true
|
||||||
|
print("🎧 [BluetoothAudioManager] 🔄 Triggering pmset fallback (\(reason))")
|
||||||
|
pmsetFetchQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let entries = self.collectPmsetAccessoryBatteryEntries()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.handlePmsetFallbackResults(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handlePmsetFallbackResults(_ entries: [PmsetAccessoryBatteryEntry]) {
|
||||||
|
isPmsetRefreshInFlight = false
|
||||||
|
lastPmsetRefreshDate = Date()
|
||||||
|
guard !entries.isEmpty else { return }
|
||||||
|
|
||||||
|
var updatedNames = batteryStatusByName
|
||||||
|
let newlyFilled = mergePmsetEntries(entries, into: &updatedNames, logNewEntries: true)
|
||||||
|
guard !newlyFilled.isEmpty else { return }
|
||||||
|
|
||||||
|
batteryStatusByName = updatedNames
|
||||||
|
applyConnectedDeviceBatteryLevels(triggerPmsetFallback: false)
|
||||||
|
|
||||||
|
if let level = hudBatteryLevelCandidate() {
|
||||||
|
updateActiveBluetoothHUDBattery(with: level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func triggerLiveBatteryRefreshIfNeeded() {
|
private func triggerLiveBatteryRefreshIfNeeded() {
|
||||||
guard !connectedDevices.isEmpty else { return }
|
guard !connectedDevices.isEmpty else { return }
|
||||||
guard connectedDevices.contains(where: { $0.batteryLevel == nil }) else { return }
|
guard connectedDevices.contains(where: { $0.batteryLevel == nil }) else { return }
|
||||||
|
|
@ -534,10 +581,8 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
guard didUpdate else { return }
|
guard didUpdate else { return }
|
||||||
|
|
||||||
applyConnectedDeviceBatteryLevels()
|
applyConnectedDeviceBatteryLevels()
|
||||||
if let lastLevel = lastConnectedDevice?.batteryLevel {
|
if let level = hudBatteryLevelCandidate() {
|
||||||
updateActiveBluetoothHUDBattery(with: lastLevel)
|
updateActiveBluetoothHUDBattery(with: level)
|
||||||
} else if let fallbackLevel = connectedDevices.last(where: { $0.batteryLevel != nil })?.batteryLevel {
|
|
||||||
updateActiveBluetoothHUDBattery(with: fallbackLevel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,6 +595,11 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func hudBatteryLevelCandidate() -> Int? {
|
||||||
|
lastConnectedDevice?.batteryLevel
|
||||||
|
?? connectedDevices.last(where: { $0.batteryLevel != nil })?.batteryLevel
|
||||||
|
}
|
||||||
|
|
||||||
private func coreBluetoothCacheSnapshot() -> CoreBluetoothCacheSnapshot {
|
private func coreBluetoothCacheSnapshot() -> CoreBluetoothCacheSnapshot {
|
||||||
guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite),
|
guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite),
|
||||||
let coreCache = preferences.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
|
let coreCache = preferences.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
|
||||||
|
|
@ -626,6 +676,46 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
static let empty = CoreBluetoothCacheSnapshot(byAddress: [:], byName: [:], namesByUUID: [:])
|
static let empty = CoreBluetoothCacheSnapshot(byAddress: [:], byName: [:], namesByUUID: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct PmsetAccessoryBatteryEntry {
|
||||||
|
let displayName: String
|
||||||
|
let normalizedName: String
|
||||||
|
let level: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func mergePmsetEntries(
|
||||||
|
_ entries: [PmsetAccessoryBatteryEntry],
|
||||||
|
into names: inout [String: Int],
|
||||||
|
logNewEntries: Bool
|
||||||
|
) -> [PmsetAccessoryBatteryEntry] {
|
||||||
|
guard !entries.isEmpty else { return [] }
|
||||||
|
|
||||||
|
var newlyFilled: [PmsetAccessoryBatteryEntry] = []
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let clamped = clampBatteryPercentage(entry.level)
|
||||||
|
let previous = names[entry.normalizedName]
|
||||||
|
|
||||||
|
if previous == nil {
|
||||||
|
newlyFilled.append(entry)
|
||||||
|
names[entry.normalizedName] = clamped
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previous, clamped > previous {
|
||||||
|
names[entry.normalizedName] = clamped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if logNewEntries {
|
||||||
|
for entry in newlyFilled {
|
||||||
|
print("🎧 [BluetoothAudioManager] ℹ️ pmset reported \(entry.level)% for \(entry.displayName)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlyFilled
|
||||||
|
}
|
||||||
|
|
||||||
private func batteryLevelFromDefaults(forAddress address: String?) -> Int? {
|
private func batteryLevelFromDefaults(forAddress address: String?) -> Int? {
|
||||||
guard let address, !address.isEmpty else { return nil }
|
guard let address, !address.isEmpty else { return nil }
|
||||||
guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite) else { return nil }
|
guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite) else { return nil }
|
||||||
|
|
@ -701,6 +791,9 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
mergeBatteryLevels(into: &combinedAddressPercentages, from: profiler.addresses)
|
mergeBatteryLevels(into: &combinedAddressPercentages, from: profiler.addresses)
|
||||||
mergeBatteryLevels(into: &combinedNamePercentages, from: profiler.names)
|
mergeBatteryLevels(into: &combinedNamePercentages, from: profiler.names)
|
||||||
|
|
||||||
|
let pmsetEntries = collectPmsetAccessoryBatteryEntries()
|
||||||
|
mergePmsetEntries(pmsetEntries, into: &combinedNamePercentages, logNewEntries: true)
|
||||||
|
|
||||||
var statuses: [String: String] = [:]
|
var statuses: [String: String] = [:]
|
||||||
for (key, value) in combinedAddressPercentages {
|
for (key, value) in combinedAddressPercentages {
|
||||||
statuses[key] = String(clampBatteryPercentage(value))
|
statuses[key] = String(clampBatteryPercentage(value))
|
||||||
|
|
@ -867,6 +960,71 @@ class BluetoothAudioManager: ObservableObject {
|
||||||
return (addressPercentages, namePercentages)
|
return (addressPercentages, namePercentages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func collectPmsetAccessoryBatteryEntries() -> [PmsetAccessoryBatteryEntry] {
|
||||||
|
let process = Process()
|
||||||
|
process.launchPath = "/usr/bin/pmset"
|
||||||
|
process.arguments = ["-g", "accps"]
|
||||||
|
|
||||||
|
let outputPipe = Pipe()
|
||||||
|
process.standardOutput = outputPipe
|
||||||
|
process.standardError = Pipe()
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
guard process.terminationStatus == 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !data.isEmpty, let output = String(data: data, encoding: .utf8) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let regex = try? NSRegularExpression(
|
||||||
|
pattern: #"^\s*-\s*(.+?)\s*(?:\(.+?\))?\s+(\d+)\s*%"#,
|
||||||
|
options: [.anchorsMatchLines]
|
||||||
|
) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries: [PmsetAccessoryBatteryEntry] = []
|
||||||
|
let nsOutput = output as NSString
|
||||||
|
let range = NSRange(location: 0, length: nsOutput.length)
|
||||||
|
|
||||||
|
regex.enumerateMatches(in: output, options: [], range: range) { match, _, _ in
|
||||||
|
guard let match, match.numberOfRanges >= 3 else { return }
|
||||||
|
|
||||||
|
let rawName = nsOutput
|
||||||
|
.substring(with: match.range(at: 1))
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let percentString = nsOutput.substring(with: match.range(at: 2))
|
||||||
|
|
||||||
|
guard !rawName.isEmpty, let level = Int(percentString) else { return }
|
||||||
|
|
||||||
|
let normalizedName = normalizeProductName(rawName)
|
||||||
|
guard !normalizedName.isEmpty else { return }
|
||||||
|
if normalizedName.hasPrefix("internalbattery") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(
|
||||||
|
PmsetAccessoryBatteryEntry(
|
||||||
|
displayName: rawName,
|
||||||
|
normalizedName: normalizedName,
|
||||||
|
level: level
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
private func identifiersFromDeviceCachePayload(_ payload: [String: Any]) -> [String] {
|
private func identifiersFromDeviceCachePayload(_ payload: [String: Any]) -> [String] {
|
||||||
var identifiers: Set<String> = []
|
var identifiers: Set<String> = []
|
||||||
let candidateKeys = ["DeviceAddress", "Address", "BD_ADDR", "SerialNumber"]
|
let candidateKeys = ["DeviceAddress", "Address", "BD_ADDR", "SerialNumber"]
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,12 @@ class ClipboardWindowManager: ObservableObject {
|
||||||
window.contentView = hostingView
|
window.contentView = hostingView
|
||||||
|
|
||||||
// Handle window closing
|
// Handle window closing
|
||||||
window.delegate = WindowDelegate { [weak self] in
|
window.delegate = WindowDelegate { [weak self] window in
|
||||||
|
ScreenCaptureVisibilityManager.shared.unregister(window)
|
||||||
self?.clipboardWindow = nil
|
self?.clipboardWindow = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly)
|
||||||
|
|
||||||
self.clipboardWindow = window
|
self.clipboardWindow = window
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
@ -88,21 +91,17 @@ class ClipboardWindowManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WindowDelegate: NSObject, NSWindowDelegate {
|
private class WindowDelegate: NSObject, NSWindowDelegate {
|
||||||
private let onClose: () -> Void
|
private let onClose: (NSWindow) -> Void
|
||||||
|
|
||||||
init(onClose: @escaping () -> Void) {
|
init(onClose: @escaping (NSWindow) -> Void) {
|
||||||
self.onClose = onClose
|
self.onClose = onClose
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||||
// Hide instead of close when user clicks close button
|
// Hide instead of close when user clicks close button
|
||||||
sender.orderOut(nil)
|
sender.orderOut(nil)
|
||||||
onClose()
|
onClose(sender)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ class LockScreenLiveActivityWindowManager {
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
window.animationBehavior = .none
|
window.animationBehavior = .none
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
|
||||||
|
|
||||||
self.window = window
|
self.window = window
|
||||||
self.hasDelegated = false
|
self.hasDelegated = false
|
||||||
return window
|
return window
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Combine
|
||||||
import AppKit
|
import AppKit
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
enum LockScreenAnimationTimings {
|
enum LockScreenAnimationTimings {
|
||||||
static let lockExpand: TimeInterval = 0.45
|
static let lockExpand: TimeInterval = 0.45
|
||||||
|
|
@ -80,6 +81,7 @@ class LockScreenManager: ObservableObject {
|
||||||
|
|
||||||
@objc private func screenLocked() {
|
@objc private func screenLocked() {
|
||||||
print("[\(timestamp())] LockScreenManager: 🔒 Screen LOCKED event received")
|
print("[\(timestamp())] LockScreenManager: 🔒 Screen LOCKED event received")
|
||||||
|
LockSoundPlayer.shared.playLockChime()
|
||||||
|
|
||||||
// Update state SYNCHRONOUSLY without Task/await to avoid any delay
|
// Update state SYNCHRONOUSLY without Task/await to avoid any delay
|
||||||
lastUpdated = Date()
|
lastUpdated = Date()
|
||||||
|
|
@ -120,6 +122,7 @@ class LockScreenManager: ObservableObject {
|
||||||
|
|
||||||
@objc private func screenUnlocked() {
|
@objc private func screenUnlocked() {
|
||||||
print("[\(timestamp())] LockScreenManager: 🔓 Screen UNLOCKED event received")
|
print("[\(timestamp())] LockScreenManager: 🔓 Screen UNLOCKED event received")
|
||||||
|
LockSoundPlayer.shared.playUnlockChime()
|
||||||
|
|
||||||
// Hide panel window immediately and synchronously
|
// Hide panel window immediately and synchronously
|
||||||
print("[\(timestamp())] LockScreenManager: 🚪 Hiding panel window")
|
print("[\(timestamp())] LockScreenManager: 🚪 Hiding panel window")
|
||||||
|
|
@ -190,3 +193,66 @@ extension LockScreenManager {
|
||||||
return true // Always available on macOS
|
return true // Always available on macOS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock Sound Playback
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class LockSoundPlayer {
|
||||||
|
static let shared = LockSoundPlayer()
|
||||||
|
private let throttleInterval: TimeInterval = 0.25
|
||||||
|
private var players: [SoundType: AVAudioPlayer] = [:]
|
||||||
|
private var lastPlaybackDates: [SoundType: Date] = [:]
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func playLockChime() {
|
||||||
|
play(.lock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playUnlockChime() {
|
||||||
|
play(.unlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func play(_ type: SoundType) {
|
||||||
|
guard Defaults[.enableLockSounds] else { return }
|
||||||
|
guard shouldPlay(type) else { return }
|
||||||
|
guard let player = resolvePlayer(for: type) else { return }
|
||||||
|
|
||||||
|
player.currentTime = 0
|
||||||
|
player.play()
|
||||||
|
lastPlaybackDates[type] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shouldPlay(_ type: SoundType) -> Bool {
|
||||||
|
guard let last = lastPlaybackDates[type] else { return true }
|
||||||
|
return Date().timeIntervalSince(last) >= throttleInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvePlayer(for type: SoundType) -> AVAudioPlayer? {
|
||||||
|
if let cached = players[type] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = Bundle.main.url(forResource: type.resourceName, withExtension: "mp3") else {
|
||||||
|
Logger.log("Missing \(type.resourceName).mp3 in bundle", category: .warning)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let player = try AVAudioPlayer(contentsOf: url)
|
||||||
|
player.prepareToPlay()
|
||||||
|
players[type] = player
|
||||||
|
return player
|
||||||
|
} catch {
|
||||||
|
Logger.log("Failed to initialize lock sound player for \(type.resourceName): \(error.localizedDescription)", category: .error)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SoundType: String {
|
||||||
|
case lock
|
||||||
|
case unlock
|
||||||
|
|
||||||
|
var resourceName: String { rawValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ class LockScreenPanelManager {
|
||||||
newWindow.isMovable = false
|
newWindow.isMovable = false
|
||||||
newWindow.hasShadow = false
|
newWindow.hasShadow = false
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
|
||||||
|
|
||||||
panelWindow = newWindow
|
panelWindow = newWindow
|
||||||
window = newWindow
|
window = newWindow
|
||||||
hasDelegated = false
|
hasDelegated = false
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ final class LockScreenReminderWidgetPanelManager {
|
||||||
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||||
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
|
||||||
|
|
||||||
window = newWindow
|
window = newWindow
|
||||||
if !hasDelegated {
|
if !hasDelegated {
|
||||||
SkyLightOperator.shared.delegateWindow(newWindow)
|
SkyLightOperator.shared.delegateWindow(newWindow)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ final class LockScreenWeatherPanelManager {
|
||||||
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||||
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface)
|
||||||
|
|
||||||
window = newWindow
|
window = newWindow
|
||||||
if !hasDelegated {
|
if !hasDelegated {
|
||||||
SkyLightOperator.shared.delegateWindow(newWindow)
|
SkyLightOperator.shared.delegateWindow(newWindow)
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,8 @@ final class MusicControlWindowManager {
|
||||||
window.isMovable = false
|
window.isMovable = false
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
|
||||||
|
|
||||||
self.window = window
|
self.window = window
|
||||||
self.hasDelegated = false
|
self.hasDelegated = false
|
||||||
return window
|
return window
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,8 @@ final class TimerControlWindowManager {
|
||||||
window.isMovable = false
|
window.isMovable = false
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
|
|
||||||
|
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
|
||||||
|
|
||||||
self.window = window
|
self.window = window
|
||||||
self.hasDelegated = false
|
self.hasDelegated = false
|
||||||
return window
|
return window
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ extension Defaults.Keys {
|
||||||
static let showOnAllDisplays = Key<Bool>("showOnAllDisplays", default: false)
|
static let showOnAllDisplays = Key<Bool>("showOnAllDisplays", default: false)
|
||||||
static let automaticallySwitchDisplay = Key<Bool>("automaticallySwitchDisplay", default: true)
|
static let automaticallySwitchDisplay = Key<Bool>("automaticallySwitchDisplay", default: true)
|
||||||
static let releaseName = Key<String>("releaseName", default: "alpha 0.0.1")
|
static let releaseName = Key<String>("releaseName", default: "alpha 0.0.1")
|
||||||
static let hidePanelsFromScreenCapture = Key<Bool>("hideFromScreenCapture", default: false)
|
static let hideDynamicIslandFromScreenCapture = Key<Bool>("hideDynamicIslandFromScreenCapture", default: false)
|
||||||
|
|
||||||
// MARK: Behavior
|
// MARK: Behavior
|
||||||
static let minimumHoverDuration = Key<TimeInterval>("minimumHoverDuration", default: 0.3)
|
static let minimumHoverDuration = Key<TimeInterval>("minimumHoverDuration", default: 0.3)
|
||||||
|
|
@ -601,6 +601,7 @@ extension Defaults.Keys {
|
||||||
|
|
||||||
// MARK: Lock Screen Features
|
// MARK: Lock Screen Features
|
||||||
static let enableLockScreenLiveActivity = Key<Bool>("enableLockScreenLiveActivity", default: true)
|
static let enableLockScreenLiveActivity = Key<Bool>("enableLockScreenLiveActivity", default: true)
|
||||||
|
static let enableLockSounds = Key<Bool>("enableLockSounds", default: true)
|
||||||
|
|
||||||
// MARK: ImageService
|
// MARK: ImageService
|
||||||
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
self.notchSize = updatedTarget
|
self.notchSize = updatedTarget
|
||||||
}
|
}
|
||||||
if let delegate = AppDelegate.shared {
|
if let delegate = AppDelegate.shared {
|
||||||
delegate.ensureWindowSize(updatedTarget, animated: true, force: false)
|
delegate.ensureWindowSize(
|
||||||
|
addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]),
|
||||||
|
animated: true,
|
||||||
|
force: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
@ -106,7 +110,32 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
self.notchSize = updatedTarget
|
self.notchSize = updatedTarget
|
||||||
}
|
}
|
||||||
if let delegate = AppDelegate.shared {
|
if let delegate = AppDelegate.shared {
|
||||||
delegate.ensureWindowSize(updatedTarget, animated: true, force: false)
|
delegate.ensureWindowSize(
|
||||||
|
addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]),
|
||||||
|
animated: true,
|
||||||
|
force: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
coordinator.$statsSecondRowExpansion
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.notchState == .open else { return }
|
||||||
|
let updatedTarget = self.calculateDynamicNotchSize()
|
||||||
|
guard self.notchSize != updatedTarget else { return }
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
|
self.notchSize = updatedTarget
|
||||||
|
}
|
||||||
|
if let delegate = AppDelegate.shared {
|
||||||
|
delegate.ensureWindowSize(
|
||||||
|
addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]),
|
||||||
|
animated: false,
|
||||||
|
force: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
@ -167,7 +196,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
let applyWindowResize: () -> Void = {
|
let applyWindowResize: () -> Void = {
|
||||||
guard let delegate = AppDelegate.shared else { return }
|
guard let delegate = AppDelegate.shared else { return }
|
||||||
delegate.ensureWindowSize(targetSize, animated: false, force: true)
|
delegate.ensureWindowSize(
|
||||||
|
addShadowPadding(to: targetSize, isMinimalistic: Defaults[.enableMinimalisticUI]),
|
||||||
|
animated: false,
|
||||||
|
force: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
|
|
@ -187,28 +220,12 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
private func calculateDynamicNotchSize() -> CGSize {
|
private func calculateDynamicNotchSize() -> CGSize {
|
||||||
// Use minimalistic size if minimalistic UI is enabled
|
// Use minimalistic size if minimalistic UI is enabled
|
||||||
var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
return statsAdjustedNotchSize(
|
||||||
// Only apply dynamic sizing when on stats tab and stats are enabled
|
from: baseSize,
|
||||||
guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else {
|
isStatsTabActive: DynamicIslandViewCoordinator.shared.currentView == .stats,
|
||||||
return baseSize
|
secondRowProgress: coordinator.statsSecondRowExpansion
|
||||||
}
|
)
|
||||||
|
|
||||||
let enabledGraphsCount = [
|
|
||||||
Defaults[.showCpuGraph],
|
|
||||||
Defaults[.showMemoryGraph],
|
|
||||||
Defaults[.showGpuGraph],
|
|
||||||
Defaults[.showNetworkGraph],
|
|
||||||
Defaults[.showDiskGraph]
|
|
||||||
].filter { $0 }.count
|
|
||||||
|
|
||||||
// If 4+ graphs are enabled, increase width
|
|
||||||
if enabledGraphsCount >= 4 {
|
|
||||||
let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120
|
|
||||||
return CGSize(width: baseSize.width + extraWidth, height: baseSize.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ let batterySneakSize: CGSize = .init(width: 160, height: 1)
|
||||||
let openNotchSize: CGSize = .init(width: 640, height: 190)
|
let openNotchSize: CGSize = .init(width: 640, height: 190)
|
||||||
private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180)
|
private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180)
|
||||||
private let minimalisticLyricsExtraHeight: CGFloat = 40
|
private let minimalisticLyricsExtraHeight: CGFloat = 40
|
||||||
|
let statsSecondRowContentHeight: CGFloat = 120
|
||||||
|
let statsGridSpacingHeight: CGFloat = 12
|
||||||
|
let notchShadowPaddingStandard: CGFloat = 18
|
||||||
|
let notchShadowPaddingMinimalistic: CGFloat = 12
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
var minimalisticOpenNotchSize: CGSize {
|
var minimalisticOpenNotchSize: CGSize {
|
||||||
|
|
@ -37,6 +41,46 @@ var minimalisticOpenNotchSize: CGSize {
|
||||||
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))
|
||||||
let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed)
|
let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed)
|
||||||
|
|
||||||
|
func statsAdjustedNotchSize(
|
||||||
|
from baseSize: CGSize,
|
||||||
|
isStatsTabActive: Bool,
|
||||||
|
secondRowProgress: CGFloat
|
||||||
|
) -> CGSize {
|
||||||
|
guard isStatsTabActive, Defaults[.enableStatsFeature] else {
|
||||||
|
return baseSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabledGraphsCount = [
|
||||||
|
Defaults[.showCpuGraph],
|
||||||
|
Defaults[.showMemoryGraph],
|
||||||
|
Defaults[.showGpuGraph],
|
||||||
|
Defaults[.showNetworkGraph],
|
||||||
|
Defaults[.showDiskGraph]
|
||||||
|
].filter { $0 }.count
|
||||||
|
|
||||||
|
guard enabledGraphsCount >= 4 else {
|
||||||
|
return baseSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let clampedProgress = max(0, min(secondRowProgress, 1))
|
||||||
|
guard clampedProgress > 0 else {
|
||||||
|
return baseSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var adjustedSize = baseSize
|
||||||
|
let extraHeight = (statsSecondRowContentHeight + statsGridSpacingHeight) * clampedProgress
|
||||||
|
adjustedSize.height += extraHeight
|
||||||
|
return adjustedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func notchShadowPaddingValue(isMinimalistic: Bool) -> CGFloat {
|
||||||
|
isMinimalistic ? notchShadowPaddingMinimalistic : notchShadowPaddingStandard
|
||||||
|
}
|
||||||
|
|
||||||
|
func addShadowPadding(to size: CGSize, isMinimalistic: Bool) -> CGSize {
|
||||||
|
CGSize(width: size.width, height: size.height + notchShadowPaddingValue(isMinimalistic: isMinimalistic))
|
||||||
|
}
|
||||||
|
|
||||||
enum MusicPlayerImageSizes {
|
enum MusicPlayerImageSizes {
|
||||||
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)
|
||||||
static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20))
|
static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20))
|
||||||
|
|
|
||||||
BIN
DynamicIsland/unlock.mp3
Normal file
BIN
DynamicIsland/unlock.mp3
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue