From 8c9b33e64caf355fca29e4f74fdc2f887e8a880c Mon Sep 17 00:00:00 2001 From: Christian Anderson Date: Wed, 27 May 2026 12:23:46 -0700 Subject: [PATCH] Add mirror options for teleprompter overlays --- .../Textream/NotchOverlayController.swift | 14 ++++-- Textream/Textream/NotchSettings.swift | 20 ++++++++ Textream/Textream/SettingsView.swift | 47 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/Textream/Textream/NotchOverlayController.swift b/Textream/Textream/NotchOverlayController.swift index a499233..b333dee 100644 --- a/Textream/Textream/NotchOverlayController.swift +++ b/Textream/Textream/NotchOverlayController.swift @@ -280,7 +280,8 @@ class NotchOverlayController: NSObject { content: overlayContent, speechRecognizer: speechRecognizer, baseHeight: panelHeight, - followingCursor: true + followingCursor: true, + mirrorAxis: settings.floatingMirrorDisplay ? settings.floatingMirrorAxis : nil ) let contentView = NSHostingView(rootView: floatingView) @@ -312,7 +313,7 @@ class NotchOverlayController: NSObject { let fullscreenView = ExternalDisplayView( content: overlayContent, speechRecognizer: speechRecognizer, - mirrorAxis: nil + mirrorAxis: settings.fullscreenMirrorDisplay ? settings.fullscreenMirrorAxis : nil ) let contentView = NSHostingView(rootView: fullscreenView) @@ -347,7 +348,8 @@ class NotchOverlayController: NSObject { let floatingView = FloatingOverlayView( content: overlayContent, speechRecognizer: speechRecognizer, - baseHeight: panelHeight + baseHeight: panelHeight, + mirrorAxis: settings.floatingMirrorDisplay ? settings.floatingMirrorAxis : nil ) let contentView = NSHostingView(rootView: floatingView) @@ -1186,6 +1188,7 @@ struct FloatingOverlayView: View { @Bindable var speechRecognizer: SpeechRecognizer let baseHeight: CGFloat var followingCursor: Bool = false + var mirrorAxis: MirrorAxis? = nil private var words: [String] { content.words } private var totalCharCount: Int { content.totalCharCount } @@ -1292,7 +1295,10 @@ struct FloatingOverlayView: View { ) .clipShape(RoundedRectangle(cornerRadius: 16)) .opacity(appeared ? 1 : 0) - .scaleEffect(appeared ? 1 : 0.9) + .scaleEffect( + x: (mirrorAxis?.scaleX ?? 1) * (appeared ? 1 : 0.9), + y: (mirrorAxis?.scaleY ?? 1) * (appeared ? 1 : 0.9) + ) .onAppear { withAnimation(.easeOut(duration: 0.3)) { appeared = true diff --git a/Textream/Textream/NotchSettings.swift b/Textream/Textream/NotchSettings.swift index 40a287b..71668f6 100644 --- a/Textream/Textream/NotchSettings.swift +++ b/Textream/Textream/NotchSettings.swift @@ -382,6 +382,14 @@ class NotchSettings { didSet { UserDefaults.standard.set(followCursorWhenUndocked, forKey: "followCursorWhenUndocked") } } + var floatingMirrorDisplay: Bool { + didSet { UserDefaults.standard.set(floatingMirrorDisplay, forKey: "floatingMirrorDisplay") } + } + + var floatingMirrorAxis: MirrorAxis { + didSet { UserDefaults.standard.set(floatingMirrorAxis.rawValue, forKey: "floatingMirrorAxis") } + } + var externalDisplayMode: ExternalDisplayMode { didSet { UserDefaults.standard.set(externalDisplayMode.rawValue, forKey: "externalDisplayMode") } } @@ -427,6 +435,14 @@ class NotchSettings { didSet { UserDefaults.standard.set(Int(fullscreenScreenID), forKey: "fullscreenScreenID") } } + var fullscreenMirrorDisplay: Bool { + didSet { UserDefaults.standard.set(fullscreenMirrorDisplay, forKey: "fullscreenMirrorDisplay") } + } + + var fullscreenMirrorAxis: MirrorAxis { + didSet { UserDefaults.standard.set(fullscreenMirrorAxis.rawValue, forKey: "fullscreenMirrorAxis") } + } + var browserServerEnabled: Bool { didSet { UserDefaults.standard.set(browserServerEnabled, forKey: "browserServerEnabled") @@ -484,6 +500,8 @@ class NotchSettings { let savedTransparencyOpacity = UserDefaults.standard.double(forKey: "overlayTransparencyOpacity") self.overlayTransparencyOpacity = savedTransparencyOpacity > 0 ? savedTransparencyOpacity : 0.85 self.followCursorWhenUndocked = UserDefaults.standard.object(forKey: "followCursorWhenUndocked") as? Bool ?? false + self.floatingMirrorDisplay = UserDefaults.standard.object(forKey: "floatingMirrorDisplay") as? Bool ?? false + self.floatingMirrorAxis = MirrorAxis(rawValue: UserDefaults.standard.string(forKey: "floatingMirrorAxis") ?? "") ?? .horizontal self.externalDisplayMode = ExternalDisplayMode(rawValue: UserDefaults.standard.string(forKey: "externalDisplayMode") ?? "") ?? .off let savedScreenID = UserDefaults.standard.integer(forKey: "externalScreenID") self.externalScreenID = UInt32(savedScreenID) @@ -499,6 +517,8 @@ class NotchSettings { self.autoNextPageDelay = savedDelay > 0 ? savedDelay : 3 let savedFullscreenScreenID = UserDefaults.standard.integer(forKey: "fullscreenScreenID") self.fullscreenScreenID = UInt32(savedFullscreenScreenID) + self.fullscreenMirrorDisplay = UserDefaults.standard.object(forKey: "fullscreenMirrorDisplay") as? Bool ?? false + self.fullscreenMirrorAxis = MirrorAxis(rawValue: UserDefaults.standard.string(forKey: "fullscreenMirrorAxis") ?? "") ?? .horizontal self.browserServerEnabled = UserDefaults.standard.object(forKey: "browserServerEnabled") as? Bool ?? false let savedPort = UserDefaults.standard.integer(forKey: "browserServerPort") self.browserServerPort = savedPort > 0 ? UInt16(savedPort) : 7373 diff --git a/Textream/Textream/SettingsView.swift b/Textream/Textream/SettingsView.swift index a7be78f..8b3ba4e 100644 --- a/Textream/Textream/SettingsView.swift +++ b/Textream/Textream/SettingsView.swift @@ -874,6 +874,13 @@ struct SettingsView: View { Divider() + mirrorDisplayPicker( + isEnabled: $settings.floatingMirrorDisplay, + axis: $settings.floatingMirrorAxis + ) + + Divider() + Toggle(isOn: $settings.floatingGlassEffect) { Text("Glass Effect") .font(.system(size: 13, weight: .medium)) @@ -913,6 +920,13 @@ struct SettingsView: View { onRefresh: { refreshOverlayScreens() } ) + Divider() + + mirrorDisplayPicker( + isEnabled: $settings.fullscreenMirrorDisplay, + axis: $settings.fullscreenMirrorAxis + ) + HStack(spacing: 6) { Image(systemName: "escape") .font(.system(size: 11, weight: .semibold)) @@ -990,6 +1004,35 @@ struct SettingsView: View { .onAppear { refreshOverlayScreens() } } + private func mirrorDisplayPicker(isEnabled: Binding, axis: Binding) -> some View { + VStack(alignment: .leading, spacing: 8) { + Toggle(isOn: isEnabled) { + Text("Mirror Display") + .font(.system(size: 13, weight: .medium)) + } + .toggleStyle(.switch) + .controlSize(.small) + + Text("Flip the teleprompter for use with a mirror rig.") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + + if isEnabled.wrappedValue { + Picker("", selection: axis) { + ForEach(MirrorAxis.allCases) { mirrorAxis in + Text(mirrorAxis.label).tag(mirrorAxis) + } + } + .pickerStyle(.segmented) + .labelsHidden() + + Text(axis.wrappedValue.description) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + } + } + // MARK: - External Tab @State private var availableScreens: [NSScreen] = [] @@ -1391,7 +1434,11 @@ struct SettingsView: View { settings.overlayTransparency = false settings.overlayTransparencyOpacity = 0.85 settings.followCursorWhenUndocked = false + settings.floatingMirrorDisplay = false + settings.floatingMirrorAxis = .horizontal settings.fullscreenScreenID = 0 + settings.fullscreenMirrorDisplay = false + settings.fullscreenMirrorAxis = .horizontal settings.externalDisplayMode = .off settings.externalScreenID = 0 settings.mirrorAxis = .horizontal