From fd9757a35c9ba73dabf91bc56dbcbeacca215906 Mon Sep 17 00:00:00 2001 From: Anmol Malhotra Date: Sat, 21 Feb 2026 01:00:18 -0500 Subject: [PATCH 1/4] Add mirror toggle to webcam preview Introduce a mirroring toggle for CameraPreviewView. Adds an @State isMirrored (default true) and uses it to control the preview layer's horizontal scaleEffect (x: -1 or 1). Adds a bottom-right overlay button (visible only when the webcam session is running) to toggle the mirror, updates the SF Symbol based on state, and provides a help tooltip. Keeps existing layout, clipping and opacity behaviour. --- .../components/Webcam/WebcamView.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/boringNotch/components/Webcam/WebcamView.swift b/boringNotch/components/Webcam/WebcamView.swift index 2fe50e93..f2a0ffba 100644 --- a/boringNotch/components/Webcam/WebcamView.swift +++ b/boringNotch/components/Webcam/WebcamView.swift @@ -15,13 +15,15 @@ struct CameraPreviewView: View { // Track if authorization request is in progress to avoid multiple requests @State private var isRequestingAuthorization: Bool = false + // Track the current state of mirror effect and the mirror icon in camera preview + @State private var isMirrored: Bool = true var body: some View { GeometryReader { geometry in ZStack { if let previewLayer = webcamManager.previewLayer { CameraPreviewLayerView(previewLayer: previewLayer) - .scaleEffect(x: -1, y: 1) + .scaleEffect(x: isMirrored ? -1 : 1, y: 1) .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? MusicPlayerImageSizes.cornerRadiusInset.opened : 100)) .frame(width: geometry.size.width, height: geometry.size.width) .opacity(webcamManager.isSessionRunning ? 1 : 0) @@ -44,6 +46,23 @@ struct CameraPreviewView: View { } } } + // The mirror toggle button should only be visible if the webcam session is running + .overlay(alignment: .bottomTrailing) { + if webcamManager.isSessionRunning { + Button { + isMirrored.toggle() + } label: { + Image(systemName: isMirrored ? "arrow.left.and.right.circle.fill" : "arrow.left.and.right.circle") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.white.opacity(0.9)) + .padding(6) + .background(.black.opacity(0.35), in: Circle()) + } + .buttonStyle(.borderless) + .help(isMirrored ? "webcam.unflip" : "webcam.flip") + .padding(8) + } + } .onTapGesture { handleCameraTap() } From 9efbe5a313fa262f2adaabd0039bda20b72cef02 Mon Sep 17 00:00:00 2001 From: Anmol Malhotra Date: Sat, 21 Feb 2026 01:07:05 -0500 Subject: [PATCH 2/4] Remove hover help from webcam flip button Remove the .help(...) modifier from the webcam mirror/flip button in WebcamView.swift. This stops the hover tooltip (which used localization keys) from appearing and simplifies the UI; no other behaviour changes were made. --- boringNotch/components/Webcam/WebcamView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/boringNotch/components/Webcam/WebcamView.swift b/boringNotch/components/Webcam/WebcamView.swift index f2a0ffba..b9954c8f 100644 --- a/boringNotch/components/Webcam/WebcamView.swift +++ b/boringNotch/components/Webcam/WebcamView.swift @@ -59,7 +59,6 @@ struct CameraPreviewView: View { .background(.black.opacity(0.35), in: Circle()) } .buttonStyle(.borderless) - .help(isMirrored ? "webcam.unflip" : "webcam.flip") .padding(8) } } From f7c8bdfe29355cc9f3471174e6df39547e9ac439 Mon Sep 17 00:00:00 2001 From: Anmol Malhotra Date: Mon, 23 Feb 2026 23:39:05 -0500 Subject: [PATCH 3/4] Add webcam settings and toggle functionality in SettingsView and WebcamView --- .../components/Settings/SettingsView.swift | 5 ++ .../Settings/Views/WebcamSettings.swift | 29 +++++++++++ .../components/Webcam/WebcamView.swift | 48 +++++++++---------- boringNotch/models/Constants.swift | 2 + 4 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 boringNotch/components/Settings/Views/WebcamSettings.swift diff --git a/boringNotch/components/Settings/SettingsView.swift b/boringNotch/components/Settings/SettingsView.swift index a1a17488..711c1cfa 100644 --- a/boringNotch/components/Settings/SettingsView.swift +++ b/boringNotch/components/Settings/SettingsView.swift @@ -43,6 +43,9 @@ struct SettingsView: View { NavigationLink(value: "Shelf") { Label("Shelf", systemImage: "books.vertical") } + NavigationLink(value: "Webcam") { + Label("Webcam", systemImage: "camera") + } NavigationLink(value: "Shortcuts") { Label("Shortcuts", systemImage: "keyboard") } @@ -74,6 +77,8 @@ struct SettingsView: View { Charge() case "Shelf": Shelf() + case "Webcam": + WebcamSettings() case "Shortcuts": Shortcuts() case "Advanced": diff --git a/boringNotch/components/Settings/Views/WebcamSettings.swift b/boringNotch/components/Settings/Views/WebcamSettings.swift new file mode 100644 index 00000000..c57dc820 --- /dev/null +++ b/boringNotch/components/Settings/Views/WebcamSettings.swift @@ -0,0 +1,29 @@ +// +// WebcamSettings.swift +// boringNotch +// +// Created by Anmol Malhotra on 23/02/2026. +// + +import SwiftUI +import Defaults + +struct WebcamSettings: View { + @Default(.mirrorWebcam) private var mirrorWebcam + @Default(.enableFlipWebcamToggle) private var enableFlipWebcamToggle + + var body: some View { + Form { + Toggle(isOn: $mirrorWebcam) { + Text("Mirror webcam video") + } + Toggle(isOn: $enableFlipWebcamToggle) { + Text("Enable toggle to flip webcam") + } + } + .formStyle(.grouped) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding() + .navigationTitle("Webcam") + } +} diff --git a/boringNotch/components/Webcam/WebcamView.swift b/boringNotch/components/Webcam/WebcamView.swift index b9954c8f..1f16a94d 100644 --- a/boringNotch/components/Webcam/WebcamView.swift +++ b/boringNotch/components/Webcam/WebcamView.swift @@ -15,18 +15,34 @@ struct CameraPreviewView: View { // Track if authorization request is in progress to avoid multiple requests @State private var isRequestingAuthorization: Bool = false - // Track the current state of mirror effect and the mirror icon in camera preview - @State private var isMirrored: Bool = true - + @Default(.mirrorWebcam) private var isMirrored + @Default(.enableFlipWebcamToggle) private var enableFlipWebcamToggle + var body: some View { GeometryReader { geometry in ZStack { if let previewLayer = webcamManager.previewLayer { - CameraPreviewLayerView(previewLayer: previewLayer) - .scaleEffect(x: isMirrored ? -1 : 1, y: 1) - .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? MusicPlayerImageSizes.cornerRadiusInset.opened : 100)) - .frame(width: geometry.size.width, height: geometry.size.width) - .opacity(webcamManager.isSessionRunning ? 1 : 0) + ZStack(alignment: .topTrailing) { + CameraPreviewLayerView(previewLayer: previewLayer) + .scaleEffect(x: isMirrored ? -1 : 1, y: 1) + .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? MusicPlayerImageSizes.cornerRadiusInset.opened : 100)) + .frame(width: geometry.size.width, height: geometry.size.width) + .opacity(webcamManager.isSessionRunning ? 1 : 0) + + if enableFlipWebcamToggle && webcamManager.isSessionRunning { + Button { + isMirrored.toggle() + } label: { + Image(systemName: "arrow.left.and.right.righttriangle.left.righttriangle.right") + .font(.system(size: 12, weight: .bold)) + .padding(6) + .background(.ultraThinMaterial) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .padding(8) + } + } } if !webcamManager.isSessionRunning { @@ -46,22 +62,6 @@ struct CameraPreviewView: View { } } } - // The mirror toggle button should only be visible if the webcam session is running - .overlay(alignment: .bottomTrailing) { - if webcamManager.isSessionRunning { - Button { - isMirrored.toggle() - } label: { - Image(systemName: isMirrored ? "arrow.left.and.right.circle.fill" : "arrow.left.and.right.circle") - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(.white.opacity(0.9)) - .padding(6) - .background(.black.opacity(0.35), in: Circle()) - } - .buttonStyle(.borderless) - .padding(8) - } - } .onTapGesture { handleCameraTap() } diff --git a/boringNotch/models/Constants.swift b/boringNotch/models/Constants.swift index 0e4b9f58..03ac4a9b 100644 --- a/boringNotch/models/Constants.swift +++ b/boringNotch/models/Constants.swift @@ -119,6 +119,8 @@ extension Defaults.Keys { // MARK: Appearance //static let alwaysShowTabs = Key("alwaysShowTabs", default: true) static let showMirror = Key("showMirror", default: false) + static let mirrorWebcam = Key("mirrorWebcam", default: true) + static let enableFlipWebcamToggle = Key("enableFlipWebcamToggle", default: false) static let mirrorShape = Key("mirrorShape", default: MirrorShapeEnum.rectangle) static let settingsIconInNotch = Key("settingsIconInNotch", default: true) static let lightingEffect = Key("lightingEffect", default: true) From 9b54e7c868886d8545d8986a6cfeefb5442d6f53 Mon Sep 17 00:00:00 2001 From: Anmol Malhotra Date: Tue, 24 Feb 2026 01:01:18 -0500 Subject: [PATCH 4/4] Add flip webcam toggle & rename mirror key - Rename WebcamSettings to WebcamSettingsView and register it in the Xcode project. - Introduce an enableFlipWebcamToggle UI flow: switch to using Defaults. - Toggle in the settings, show/hide a mirror button in the camera preview only when the toggle is enabled and the session is running, and improve the mirror button icon and styling. - Rename the Defaults key mirrorWebcam to isMirrored (default false) and update usages. - Add localization entries for the new toggle and the Webcam label and bump Localizable.xcstrings version. - Update project.pbxproj to include the new/renamed source file references. --- boringNotch.xcodeproj/project.pbxproj | 4 ++++ boringNotch/Localizable.xcstrings | 10 +++++++++- ...amSettings.swift => WebcamSettingsView.swift} | 9 +++------ boringNotch/components/Webcam/WebcamView.swift | 16 +++++++++------- boringNotch/models/Constants.swift | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) rename boringNotch/components/Settings/Views/{WebcamSettings.swift => WebcamSettingsView.swift} (64%) diff --git a/boringNotch.xcodeproj/project.pbxproj b/boringNotch.xcodeproj/project.pbxproj index d50c3689..c8f1b110 100644 --- a/boringNotch.xcodeproj/project.pbxproj +++ b/boringNotch.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 5917FD112E57891600E87F1C /* MediaKeyInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917FD102E57891600E87F1C /* MediaKeyInterceptor.swift */; }; 5955950D2E900ED800C66711 /* ApplicationRelauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */; }; 59D8C23C2E589FAA00147B33 /* VolumeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */; }; + 64FA50FB2F4D6F9E00008A28 /* WebcamSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA50FA2F4D6F9E00008A28 /* WebcamSettingsView.swift */; }; 9A0887322C7A693000C160EA /* TabButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887312C7A693000C160EA /* TabButton.swift */; }; 9A0887352C7AFF8E00C160EA /* TabSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */; }; 9A987A0D2C73CA66005CA465 /* ShelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A987A032C73CA66005CA465 /* ShelfView.swift */; }; @@ -308,6 +309,7 @@ 5917FD102E57891600E87F1C /* MediaKeyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaKeyInterceptor.swift; sourceTree = ""; }; 5955950C2E900ED800C66711 /* ApplicationRelauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRelauncher.swift; sourceTree = ""; }; 59D8C23B2E589FAA00147B33 /* VolumeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeManager.swift; sourceTree = ""; }; + 64FA50FA2F4D6F9E00008A28 /* WebcamSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebcamSettingsView.swift; sourceTree = ""; }; 9A0887312C7A693000C160EA /* TabButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabButton.swift; sourceTree = ""; }; 9A0887342C7AFF8E00C160EA /* TabSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSelectionView.swift; sourceTree = ""; }; 9A987A032C73CA66005CA465 /* ShelfView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShelfView.swift; sourceTree = ""; }; @@ -530,6 +532,7 @@ 11DB266F2EDD0CDF001EA0CF /* SettingsHelpers.swift */, 11DB26702EDD0CDF001EA0CF /* ShelfSettingsView.swift */, 11DB26712EDD0CDF001EA0CF /* ShortcutsSettingsView.swift */, + 64FA50FA2F4D6F9E00008A28 /* WebcamSettingsView.swift */, ); path = Views; sourceTree = ""; @@ -1069,6 +1072,7 @@ 11C5E3132DFE85970065821E /* SettingsWindowController.swift in Sources */, 110029272E84FD4C00035A57 /* TemporaryFileStorageService.swift in Sources */, 11CFC6652E09C7B300748C80 /* OnboardingFinishView.swift in Sources */, + 64FA50FB2F4D6F9E00008A28 /* WebcamSettingsView.swift in Sources */, 507266DB2C908E2E00A2D00D /* HoverButton.swift in Sources */, 1471A8592C6281BD0058408D /* BoringNotchWindow.swift in Sources */, 14CEF4182C5CAED300855D72 /* ContentView.swift in Sources */, diff --git a/boringNotch/Localizable.xcstrings b/boringNotch/Localizable.xcstrings index 905fcd34..af8437ba 100644 --- a/boringNotch/Localizable.xcstrings +++ b/boringNotch/Localizable.xcstrings @@ -7616,6 +7616,10 @@ } } }, + "Enable toggle to flip webcam" : { + "comment" : "A label for a toggle that enables or disables the option to flip the webcam.", + "isCommentAutoGenerated" : true + }, "Enable window shadow" : { "localizations" : { "ar" : { @@ -20234,6 +20238,10 @@ }, "Visibility" : { + }, + "Webcam" : { + "comment" : "A label displayed above a button that allows the user to configure webcam settings.", + "isCommentAutoGenerated" : true }, "Welcome" : { "localizations" : { @@ -20936,5 +20944,5 @@ } } }, - "version" : "1.0" + "version" : "1.1" } \ No newline at end of file diff --git a/boringNotch/components/Settings/Views/WebcamSettings.swift b/boringNotch/components/Settings/Views/WebcamSettingsView.swift similarity index 64% rename from boringNotch/components/Settings/Views/WebcamSettings.swift rename to boringNotch/components/Settings/Views/WebcamSettingsView.swift index c57dc820..b9c22dbd 100644 --- a/boringNotch/components/Settings/Views/WebcamSettings.swift +++ b/boringNotch/components/Settings/Views/WebcamSettingsView.swift @@ -2,22 +2,19 @@ // WebcamSettings.swift // boringNotch // -// Created by Anmol Malhotra on 23/02/2026. +// Created by Anmol Malhotra on 2026-02-24. // import SwiftUI import Defaults struct WebcamSettings: View { - @Default(.mirrorWebcam) private var mirrorWebcam + @Default(.enableFlipWebcamToggle) private var enableFlipWebcamToggle var body: some View { Form { - Toggle(isOn: $mirrorWebcam) { - Text("Mirror webcam video") - } - Toggle(isOn: $enableFlipWebcamToggle) { + Defaults.Toggle(key: .enableFlipWebcamToggle) { Text("Enable toggle to flip webcam") } } diff --git a/boringNotch/components/Webcam/WebcamView.swift b/boringNotch/components/Webcam/WebcamView.swift index 1f16a94d..ca119006 100644 --- a/boringNotch/components/Webcam/WebcamView.swift +++ b/boringNotch/components/Webcam/WebcamView.swift @@ -15,29 +15,31 @@ struct CameraPreviewView: View { // Track if authorization request is in progress to avoid multiple requests @State private var isRequestingAuthorization: Bool = false - @Default(.mirrorWebcam) private var isMirrored + // Track the current state of mirror effect and the mirror icon in camera preview + @Default(.isMirrored) private var isMirrored @Default(.enableFlipWebcamToggle) private var enableFlipWebcamToggle var body: some View { GeometryReader { geometry in ZStack { if let previewLayer = webcamManager.previewLayer { - ZStack(alignment: .topTrailing) { + ZStack(alignment: .bottomTrailing) { CameraPreviewLayerView(previewLayer: previewLayer) .scaleEffect(x: isMirrored ? -1 : 1, y: 1) .clipShape(RoundedRectangle(cornerRadius: Defaults[.mirrorShape] == .rectangle ? MusicPlayerImageSizes.cornerRadiusInset.opened : 100)) .frame(width: geometry.size.width, height: geometry.size.width) .opacity(webcamManager.isSessionRunning ? 1 : 0) + // The mirror toggle button should only be visible if the webcam session is running and the setting to enable it is turned on if enableFlipWebcamToggle && webcamManager.isSessionRunning { Button { isMirrored.toggle() } label: { - Image(systemName: "arrow.left.and.right.righttriangle.left.righttriangle.right") - .font(.system(size: 12, weight: .bold)) - .padding(6) - .background(.ultraThinMaterial) - .clipShape(Circle()) + Image(systemName: isMirrored ? "arrow.left.and.right.circle.fill" : "arrow.left.and.right.circle") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.white.opacity(0.9)) + .padding(6) + .background(.black.opacity(0.35), in: Circle()) } .buttonStyle(.plain) .padding(8) diff --git a/boringNotch/models/Constants.swift b/boringNotch/models/Constants.swift index 03ac4a9b..ce9acf61 100644 --- a/boringNotch/models/Constants.swift +++ b/boringNotch/models/Constants.swift @@ -119,7 +119,7 @@ extension Defaults.Keys { // MARK: Appearance //static let alwaysShowTabs = Key("alwaysShowTabs", default: true) static let showMirror = Key("showMirror", default: false) - static let mirrorWebcam = Key("mirrorWebcam", default: true) + static let isMirrored = Key("isMirrored", default: false) static let enableFlipWebcamToggle = Key("enableFlipWebcamToggle", default: false) static let mirrorShape = Key("mirrorShape", default: MirrorShapeEnum.rectangle) static let settingsIconInNotch = Key("settingsIconInNotch", default: true)