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 1/2] 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) From a03d361243a273c4d66adba1fcc60fbdb88a5b02 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 13 Apr 2026 13:54:53 +0530 Subject: [PATCH 2/2] Clean-up commit --- DynamicIsland/ContentView.swift | 20 +++--------------- DynamicIsland/Localizable.xcstrings | 21 +++---------------- .../components/Settings/SettingsView.swift | 6 +----- 3 files changed, 7 insertions(+), 40 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 7e29dca..72de3e2 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -124,17 +124,6 @@ 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) { @@ -1680,13 +1669,10 @@ struct ContentView: View { return false } - let horizontalPadding: CGFloat = 12 + let horizontalPadding: CGFloat = 8 let activationWidth = vm.closedNotchSize.width + horizontalPadding * 2 - - // 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 activationHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 14) + let activationRect = CGRect( x: screen.frame.midX - activationWidth / 2, y: screen.frame.maxY - activationHeight, diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index ad88f35..1687df1 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2314,7 +2314,6 @@ }, "±10 Seconds" : { "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -3138,7 +3137,6 @@ }, "Accent" : { "comment" : "Display name for the caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -3696,7 +3694,6 @@ }, "Adaptive gradient" : { "comment" : "Display name for the timer icon color mode: adaptive gradient.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -6016,7 +6013,6 @@ }, "Apple Music" : { "comment" : "Media controller type for Apple Music", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -6920,7 +6916,6 @@ }, "Bar" : { "comment" : "Name of the timer progress style that shows a horizontal bar.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -14521,7 +14516,6 @@ }, "Digital" : { "comment" : "The display name for the \"Digital\" reminder presentation style.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -20829,7 +20823,6 @@ }, "Green" : { "comment" : "Display name for the green caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -26874,6 +26867,9 @@ } } } + }, + "Mirror Camera" : { + }, "Mirror macOS Clock timers" : { "comment" : "A toggle that enables or disables the app's ability to mirror macOS Clock timers.", @@ -29941,7 +29937,6 @@ }, "Now Playing" : { "comment" : "Name of the media controller type for the \"Now Playing\" option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -30687,7 +30682,6 @@ }, "Panel" : { "comment" : "Display name for the clipboard display mode \"Panel\".", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -34230,7 +34224,6 @@ }, "Ring" : { "comment" : "The name of the timer progress style that shows a ring shape.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -35752,7 +35745,6 @@ }, "Separate Tab" : { "comment" : "Display name for the clipboard display mode option to open the clipboard in a separate tab.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -38185,7 +38177,6 @@ }, "Shows screen assistant as a dropdown attached to the AI button" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant as a dropdown attached to the AI button.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -38210,7 +38201,6 @@ }, "Shows screen assistant in a floating panel near the notch" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant in a floating panel near the notch.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -38438,7 +38428,6 @@ }, "Skip forward or backward by ten seconds" : { "comment" : "Description of a music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -39162,7 +39151,6 @@ }, "Spotify" : { "comment" : "Spotify media controller type.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -39399,7 +39387,6 @@ }, "Standard previous/next track controls" : { "comment" : "Description of the music skip behavior option to use standard previous/next track controls.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -42227,7 +42214,6 @@ }, "Track Skip" : { "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -45585,7 +45571,6 @@ }, "Youtube Music" : { "comment" : "Spotify music service.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index ef1cb42..51c1721 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2487,11 +2487,7 @@ struct Media: View { } header: { Text("Media Source") } footer: { - 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 { + if MusicManager.shared.isNowPlayingDeprecated { HStack { Text("YouTube Music requires this third-party app to be installed: ") .foregroundStyle(.secondary)