Merge pull request #412 from Ebullioscopic/feature/camera-mirror-mode

Multiple camera choice
This commit is contained in:
Hariharan Mudaliar 2026-04-13 14:00:43 +05:30 • committed by GitHub
commit dbdeabdd83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 46 additions and 23 deletions

View file

@ -1672,6 +1672,7 @@ struct ContentView: View {
let horizontalPadding: CGFloat = 8
let activationWidth = vm.closedNotchSize.width + horizontalPadding * 2
let activationHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 14)
let activationRect = CGRect(
x: screen.frame.midX - activationWidth / 2,
y: screen.frame.maxY - activationHeight,

View file

@ -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" : {

View file

@ -3830,7 +3830,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 +4239,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")
}

View file

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

View file

@ -794,6 +794,7 @@ extension Defaults.Keys {
//static let alwaysShowTabs = Key<Bool>("alwaysShowTabs", default: true)
static let showMirror = Key<Bool>("showMirror", default: false)
static let mirrorShape = Key<MirrorShapeEnum>("mirrorShape", default: MirrorShapeEnum.rectangle)
static let selectedCameraID = Key<String>("selectedCameraID", default: "")
static let settingsIconInNotch = Key<Bool>("settingsIconInNotch", default: true)
static let lightingEffect = Key<Bool>("lightingEffect", default: true)
static let accentColor = Key<Color>("accentColor", default: Color.blue)