From edd8ecc3bb0f327ebd3190f135ee2e37c0f59133 Mon Sep 17 00:00:00 2001 From: NekomiRyan <77211538+NekomiRyan@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:48:11 -0400 Subject: [PATCH] feat: add camera selector for mirror mode and fix hover glitch --- DynamicIsland/ContentView.swift | 19 +++++++++++-- .../components/Settings/SettingsView.swift | 24 +++++++++++++++- DynamicIsland/managers/WebcamManager.swift | 28 +++++++++++++++---- DynamicIsland/models/Constants.swift | 1 + 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 07fccfd..7e29dca 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -124,6 +124,17 @@ struct ContentView: View { return baseSize } + if coordinator.currentView == .home && Defaults[.mediaController] == .all && !musicManager.secondarySources.isEmpty { + let toggleBarHeight: CGFloat = 34 + let rowHeight: CGFloat = 58 + let safetyPadding: CGFloat = 16 + var extraHeight = toggleBarHeight + safetyPadding + if musicManager.isMultiSourceListExpanded { + extraHeight += CGFloat(musicManager.secondarySources.count) * rowHeight + } + return CGSize(width: baseSize.width, height: baseSize.height + extraHeight) + } + if enableMinimalisticUI, coordinator.currentView == .home, let preferredHeight = extensionMinimalisticPreferredHeight(baseSize: baseSize) { @@ -1669,9 +1680,13 @@ struct ContentView: View { return false } - let horizontalPadding: CGFloat = 8 + let horizontalPadding: CGFloat = 12 let activationWidth = vm.closedNotchSize.width + horizontalPadding * 2 - let activationHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 14) + + // Hysteresis: Use a taller activation area when already hovering to prevent rapid flickering + let baseHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 16) + let activationHeight = isHovering ? baseHeight + 20 : baseHeight + let activationRect = CGRect( x: screen.frame.midX - activationWidth / 2, y: screen.frame.maxY - activationHeight, diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 95a1051..ef1cb42 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2487,7 +2487,11 @@ struct Media: View { } header: { Text("Media Source") } footer: { - if MusicManager.shared.isNowPlayingDeprecated { + if mediaController == .all { + Text("All Music detects every active media source and shows them in a switchable list below the main player.") + .foregroundStyle(.secondary) + .font(.caption) + } else if MusicManager.shared.isNowPlayingDeprecated { HStack { Text("YouTube Music requires this third-party app to be installed: ") .foregroundStyle(.secondary) @@ -3830,7 +3834,9 @@ struct LiveActivitiesSettings: View { struct Appearance: View { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + @ObservedObject var webcamManager = WebcamManager.shared @Default(.mirrorShape) var mirrorShape + @Default(.selectedCameraID) var selectedCameraID @Default(.sliderColor) var sliderColor @Default(.useMusicVisualizer) var useMusicVisualizer @Default(.customVisualizers) var customVisualizers @@ -4237,6 +4243,22 @@ struct Appearance: View { .tag(MirrorShapeEnum.rectangle) } .settingsHighlight(id: highlightID("Mirror shape")) + + if webcamManager.cameraAvailable { + Picker("Mirror Camera", selection: $selectedCameraID) { + ForEach(webcamManager.availableCameras, id: \.uniqueID) { device in + Text(device.localizedName) + .tag(device.uniqueID) + } + } + .onChange(of: selectedCameraID) { _, _ in + if Defaults[.showMirror] { + webcamManager.stopSession() + webcamManager.startSession() + } + } + .settingsHighlight(id: highlightID("Mirror Camera")) + } Defaults.Toggle(key: .showNotHumanFace) { Text("Idle Animation") } diff --git a/DynamicIsland/managers/WebcamManager.swift b/DynamicIsland/managers/WebcamManager.swift index ea49666..1865d60 100644 --- a/DynamicIsland/managers/WebcamManager.swift +++ b/DynamicIsland/managers/WebcamManager.swift @@ -22,6 +22,7 @@ import AVFoundation import SwiftUI +import Defaults class WebcamManager: NSObject, ObservableObject { static let shared = WebcamManager() @@ -51,6 +52,12 @@ class WebcamManager: NSObject, ObservableObject { } } + @Published var availableCameras: [AVCaptureDevice] = [] { + didSet { + objectWillChange.send() + } + } + private let sessionQueue = DispatchQueue(label: "DynamicIsland.WebcamManager.SessionQueue", qos: .userInitiated) private var isCleaningUp: Bool = false @@ -132,7 +139,7 @@ class WebcamManager: NSObject, ObservableObject { /// Checks if any camera devices are available and sets up capture session if needed func checkCameraAvailability() { let availableDevices = AVCaptureDevice.DiscoverySession( - deviceTypes: [.external, .builtInWideAngleCamera], + deviceTypes: [.external, .builtInWideAngleCamera, .deskViewCamera, .externalUnknown], mediaType: .video, position: .unspecified ).devices @@ -140,6 +147,7 @@ class WebcamManager: NSObject, ObservableObject { let hasAvailableDevices = !availableDevices.isEmpty DispatchQueue.main.async { + self.availableCameras = availableDevices self.cameraAvailable = hasAvailableDevices } } @@ -158,14 +166,24 @@ class WebcamManager: NSObject, ObservableObject { let session = AVCaptureSession() do { - // Get available devices and prefer external camera if available + // Get available devices let discoverySession = AVCaptureDevice.DiscoverySession( - deviceTypes: [.external, .builtInWideAngleCamera], + deviceTypes: [.external, .builtInWideAngleCamera, .deskViewCamera, .externalUnknown], mediaType: .video, position: .unspecified ) - guard let videoDevice = discoverySession.devices.first else { + let devices = discoverySession.devices + let selectedID = Defaults[.selectedCameraID] + + let videoDevice: AVCaptureDevice? + if !selectedID.isEmpty { + videoDevice = devices.first(where: { $0.uniqueID == selectedID }) ?? devices.first + } else { + videoDevice = devices.first + } + + guard let videoDevice = videoDevice else { NSLog("No video devices available") DispatchQueue.main.async { self.isSessionRunning = false @@ -175,7 +193,7 @@ class WebcamManager: NSObject, ObservableObject { return } - NSLog("Using camera: \(videoDevice.localizedName)") + NSLog("Using camera: \(videoDevice.localizedName) (ID: \(videoDevice.uniqueID))") // Lock device for configuration try videoDevice.lockForConfiguration() diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 7d9ebc9..c15f9ea 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -794,6 +794,7 @@ extension Defaults.Keys { //static let alwaysShowTabs = Key("alwaysShowTabs", default: true) static let showMirror = Key("showMirror", default: false) static let mirrorShape = Key("mirrorShape", default: MirrorShapeEnum.rectangle) + static let selectedCameraID = Key("selectedCameraID", default: "") static let settingsIconInNotch = Key("settingsIconInNotch", default: true) static let lightingEffect = Key("lightingEffect", default: true) static let accentColor = Key("accentColor", default: Color.blue)