diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 07fccfd..72de3e2 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -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, 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 95a1051..51c1721 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -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") } 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)