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/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/WebcamSettingsView.swift b/boringNotch/components/Settings/Views/WebcamSettingsView.swift new file mode 100644 index 00000000..b9c22dbd --- /dev/null +++ b/boringNotch/components/Settings/Views/WebcamSettingsView.swift @@ -0,0 +1,26 @@ +// +// WebcamSettings.swift +// boringNotch +// +// Created by Anmol Malhotra on 2026-02-24. +// + +import SwiftUI +import Defaults + +struct WebcamSettings: View { + + @Default(.enableFlipWebcamToggle) private var enableFlipWebcamToggle + + var body: some View { + Form { + Defaults.Toggle(key: .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 2fe50e93..ca119006 100644 --- a/boringNotch/components/Webcam/WebcamView.swift +++ b/boringNotch/components/Webcam/WebcamView.swift @@ -15,16 +15,36 @@ 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 + @Default(.isMirrored) 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: -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: .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: 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) + } + } } if !webcamManager.isSessionRunning { diff --git a/boringNotch/models/Constants.swift b/boringNotch/models/Constants.swift index 0e4b9f58..ce9acf61 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 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) static let lightingEffect = Key("lightingEffect", default: true)