feat: add camera selector for mirror mode and fix hover glitch

This commit is contained in:
NekomiRyan 2026-04-09 13:48:11 -04:00 • committed by Hariharan Mudaliar
commit edd8ecc3bb
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
4 changed files with 64 additions and 8 deletions

View file

@ -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,

View file

@ -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")
}

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)