diff --git a/Textream/Textream/BrowserServer.swift b/Textream/Textream/BrowserServer.swift index c4676fa..c58f481 100644 --- a/Textream/Textream/BrowserServer.swift +++ b/Textream/Textream/BrowserServer.swift @@ -18,6 +18,7 @@ struct BrowserState: Codable { let isListening: Bool let isDone: Bool let fontColor: String + let cueColor: String let hasNextPage: Bool let isActive: Bool let highlightWords: Bool @@ -212,6 +213,7 @@ class BrowserServer { isListening: speechRecognizer?.isListening ?? false, isDone: isDone, fontColor: NotchSettings.shared.fontColorPreset.cssColor, + cueColor: NotchSettings.shared.cueColorPreset.cssColor, hasNextPage: hasNextPage, isActive: true, highlightWords: highlightWords, @@ -224,7 +226,7 @@ class BrowserServer { let state = BrowserState( words: [], highlightedCharCount: 0, totalCharCount: 0, audioLevels: [], isListening: false, isDone: false, - fontColor: "#ffffff", hasNextPage: false, isActive: false, + fontColor: "#ffffff", cueColor: "#ffffff", hasNextPage: false, isActive: false, highlightWords: true, lastSpokenText: "" ) broadcast(state) @@ -457,7 +459,9 @@ class BrowserServer { const c=document.getElementById('text-container'), words=s.words||[], fc=s.fontColor||'#ffffff', + cc=s.cueColor||fc, rgb=parseColor(fc), + crgb=parseColor(cc), hlWords=s.highlightWords!==false, hcc=s.highlightedCharCount||0; @@ -510,10 +514,10 @@ class BrowserServer { if(!hlWords){ // Classic / silence-paused: uniform color, no per-word highlight - color=ann?'rgba(255,255,255,0.4)':fc; + color=ann?rgba(crgb,0.4):fc; } else if(ann){ - // Annotation: italic, white with varying opacity - color=isFullyLit?'rgba(255,255,255,0.5)':'rgba(255,255,255,0.2)'; + // Annotation: cue color with varying opacity + color=isFullyLit?rgba(crgb,0.5):rgba(crgb,0.2); } else if(isFullyLit){ // Already read: dimmed color=rgba(rgb,0.3); diff --git a/Textream/Textream/DirectorServer.swift b/Textream/Textream/DirectorServer.swift index 9f5b831..7c0fd99 100644 --- a/Textream/Textream/DirectorServer.swift +++ b/Textream/Textream/DirectorServer.swift @@ -18,6 +18,7 @@ struct DirectorState: Codable { let isDone: Bool let isListening: Bool let fontColor: String + let cueColor: String let lastSpokenText: String let audioLevels: [Double] } @@ -272,6 +273,7 @@ class DirectorServer { isDone: isDone, isListening: speechRecognizer?.isListening ?? false, fontColor: NotchSettings.shared.fontColorPreset.cssColor, + cueColor: NotchSettings.shared.cueColorPreset.cssColor, lastSpokenText: speechRecognizer?.lastSpokenText ?? "", audioLevels: (speechRecognizer?.audioLevels ?? []).map { Double($0) } ) @@ -282,7 +284,7 @@ class DirectorServer { let state = DirectorState( words: [], highlightedCharCount: 0, totalCharCount: 0, isActive: false, isDone: false, isListening: false, - fontColor: "#ffffff", lastSpokenText: "", + fontColor: "#ffffff", cueColor: "#ffffff", lastSpokenText: "", audioLevels: [] ) broadcast(state) diff --git a/Textream/Textream/ExternalDisplayController.swift b/Textream/Textream/ExternalDisplayController.swift index 40a2845..ac2b272 100644 --- a/Textream/Textream/ExternalDisplayController.swift +++ b/Textream/Textream/ExternalDisplayController.swift @@ -225,6 +225,9 @@ struct ExternalDisplayView: View { highlightedCharCount: effectiveCharCount, font: .systemFont(ofSize: fontSize, weight: .semibold), highlightColor: NotchSettings.shared.fontColorPreset.color, + cueColor: NotchSettings.shared.cueColorPreset.color, + cueUnreadOpacity: NotchSettings.shared.cueBrightness.unreadOpacity, + cueReadOpacity: NotchSettings.shared.cueBrightness.readOpacity, onWordTap: { charOffset in if listeningMode == .wordTracking { speechRecognizer.jumpTo(charOffset: charOffset) diff --git a/Textream/Textream/MarqueeTextView.swift b/Textream/Textream/MarqueeTextView.swift index cc03efd..74e5afb 100644 --- a/Textream/Textream/MarqueeTextView.swift +++ b/Textream/Textream/MarqueeTextView.swift @@ -81,6 +81,9 @@ struct SpeechScrollView: View { let highlightedCharCount: Int var font: NSFont = .systemFont(ofSize: 18, weight: .semibold) var highlightColor: Color = .white + var cueColor: Color = .white + var cueUnreadOpacity: Double = 0.2 + var cueReadOpacity: Double = 0.5 var onWordTap: ((Int) -> Void)? = nil /// Called when user starts/stops manual scrolling in smooth mode. /// Bool: true = scrolling started (pause timer), false = scrolling ended (resume timer). @@ -104,6 +107,9 @@ struct SpeechScrollView: View { highlightedCharCount: highlightedCharCount, font: font, highlightColor: highlightColor, + cueColor: cueColor, + cueUnreadOpacity: cueUnreadOpacity, + cueReadOpacity: cueReadOpacity, highlightWords: !smoothScroll, containerWidth: geo.size.width, onWordTap: { charOffset in @@ -328,6 +334,9 @@ struct WordFlowLayout: View { let highlightedCharCount: Int let font: NSFont var highlightColor: Color = .white + var cueColor: Color = .white + var cueUnreadOpacity: Double = 0.2 + var cueReadOpacity: Double = 0.5 var highlightWords: Bool = true let containerWidth: CGFloat var onWordTap: ((Int) -> Void)? = nil @@ -422,7 +431,7 @@ struct WordFlowLayout: View { // When highlighting is off (classic/silence-paused), use uniform color if !highlightWords { let uniformColor: Color = item.isAnnotation - ? Color.white.opacity(0.4) + ? cueColor.opacity(cueUnreadOpacity) : highlightColor return Text(item.word + " ") @@ -442,11 +451,11 @@ struct WordFlowLayout: View { } } - // Annotations: italic, always dimmed + // Annotations: italic, dimmed with cue color if item.isAnnotation { let annotationColor: Color = isFullyLit - ? Color.white.opacity(0.5) - : Color.white.opacity(0.2) + ? cueColor.opacity(cueReadOpacity) + : cueColor.opacity(cueUnreadOpacity) return Text(item.word + " ") .font(Font(font).italic()) diff --git a/Textream/Textream/NotchOverlayController.swift b/Textream/Textream/NotchOverlayController.swift index 04708a6..330d202 100644 --- a/Textream/Textream/NotchOverlayController.swift +++ b/Textream/Textream/NotchOverlayController.swift @@ -823,6 +823,9 @@ struct NotchOverlayView: View { highlightedCharCount: effectiveCharCount, font: NotchSettings.shared.font, highlightColor: NotchSettings.shared.fontColorPreset.color, + cueColor: NotchSettings.shared.cueColorPreset.color, + cueUnreadOpacity: NotchSettings.shared.cueBrightness.unreadOpacity, + cueReadOpacity: NotchSettings.shared.cueBrightness.readOpacity, onWordTap: { charOffset in if listeningMode == .wordTracking { speechRecognizer.jumpTo(charOffset: charOffset) @@ -1298,6 +1301,9 @@ struct FloatingOverlayView: View { highlightedCharCount: effectiveCharCount, font: NotchSettings.shared.font, highlightColor: NotchSettings.shared.fontColorPreset.color, + cueColor: NotchSettings.shared.cueColorPreset.color, + cueUnreadOpacity: NotchSettings.shared.cueBrightness.unreadOpacity, + cueReadOpacity: NotchSettings.shared.cueBrightness.readOpacity, onWordTap: { charOffset in if listeningMode == .wordTracking { speechRecognizer.jumpTo(charOffset: charOffset) diff --git a/Textream/Textream/NotchSettings.swift b/Textream/Textream/NotchSettings.swift index ea60315..1a02c9d 100644 --- a/Textream/Textream/NotchSettings.swift +++ b/Textream/Textream/NotchSettings.swift @@ -128,6 +128,43 @@ enum FontColorPreset: String, CaseIterable, Identifiable { } } +// MARK: - Cue Brightness + +enum CueBrightness: String, CaseIterable, Identifiable { + case dim, low, medium, bright + + var id: String { rawValue } + + var label: String { + switch self { + case .dim: return "Dim" + case .low: return "Low" + case .medium: return "Medium" + case .bright: return "Bright" + } + } + + /// Opacity for unread annotations + var unreadOpacity: Double { + switch self { + case .dim: return 0.2 + case .low: return 0.35 + case .medium: return 0.5 + case .bright: return 0.8 + } + } + + /// Opacity for already-read annotations + var readOpacity: Double { + switch self { + case .dim: return 0.5 + case .low: return 0.6 + case .medium: return 0.7 + case .bright: return 1.0 + } + } +} + // MARK: - Overlay Mode enum OverlayMode: String, CaseIterable, Identifiable { @@ -305,6 +342,14 @@ class NotchSettings { didSet { UserDefaults.standard.set(fontColorPreset.rawValue, forKey: "fontColorPreset") } } + var cueColorPreset: FontColorPreset { + didSet { UserDefaults.standard.set(cueColorPreset.rawValue, forKey: "cueColorPreset") } + } + + var cueBrightness: CueBrightness { + didSet { UserDefaults.standard.set(cueBrightness.rawValue, forKey: "cueBrightness") } + } + var overlayMode: OverlayMode { didSet { UserDefaults.standard.set(overlayMode.rawValue, forKey: "overlayMode") } } @@ -418,6 +463,8 @@ class NotchSettings { self.fontSizePreset = FontSizePreset(rawValue: UserDefaults.standard.string(forKey: "fontSizePreset") ?? "") ?? .lg self.fontFamilyPreset = FontFamilyPreset(rawValue: UserDefaults.standard.string(forKey: "fontFamilyPreset") ?? "") ?? .sans self.fontColorPreset = FontColorPreset(rawValue: UserDefaults.standard.string(forKey: "fontColorPreset") ?? "") ?? .white + self.cueColorPreset = FontColorPreset(rawValue: UserDefaults.standard.string(forKey: "cueColorPreset") ?? "") ?? .white + self.cueBrightness = CueBrightness(rawValue: UserDefaults.standard.string(forKey: "cueBrightness") ?? "") ?? .dim self.overlayMode = OverlayMode(rawValue: UserDefaults.standard.string(forKey: "overlayMode") ?? "") ?? .pinned self.notchDisplayMode = NotchDisplayMode(rawValue: UserDefaults.standard.string(forKey: "notchDisplayMode") ?? "") ?? .followMouse let savedPinnedScreenID = UserDefaults.standard.integer(forKey: "pinnedScreenID") diff --git a/Textream/Textream/SettingsView.swift b/Textream/Textream/SettingsView.swift index 8072bbc..581526b 100644 --- a/Textream/Textream/SettingsView.swift +++ b/Textream/Textream/SettingsView.swift @@ -139,7 +139,7 @@ struct NotchPreviewContent: View { @Bindable var settings: NotchSettings let menuBarHeight: CGFloat - private static let loremWords = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem aperiam eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt".split(separator: " ").map(String.init) + private static let loremWords = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor [pause] incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem aperiam eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt".split(separator: " ").map(String.init) private let highlightedCount = 42 @State private var previewWordProgress: Double = 0 @@ -198,6 +198,9 @@ struct NotchPreviewContent: View { highlightedCharCount: settings.listeningMode == .wordTracking ? highlightedCount : Self.loremWords.count * 5, font: settings.font, highlightColor: settings.fontColorPreset.color, + cueColor: settings.cueColorPreset.color, + cueUnreadOpacity: settings.cueBrightness.unreadOpacity, + cueReadOpacity: settings.cueBrightness.readOpacity, smoothScroll: settings.listeningMode != .wordTracking, smoothWordProgress: previewWordProgress, isListening: settings.listeningMode != .wordTracking @@ -560,6 +563,63 @@ struct SettingsView: View { } } + // Cue Color + Text("Cue Color") + .font(.system(size: 13, weight: .medium)) + + HStack(spacing: 8) { + ForEach(FontColorPreset.allCases) { preset in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + settings.cueColorPreset = preset + } + } label: { + VStack(spacing: 6) { + Circle() + .fill(preset.color) + .frame(width: 22, height: 22) + .overlay( + Circle() + .strokeBorder(Color.primary.opacity(0.15), lineWidth: 1) + ) + .overlay( + settings.cueColorPreset == preset + ? Image(systemName: "checkmark") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(preset == .white ? .black : .white) + : nil + ) + Text(preset.label) + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(settings.cueColorPreset == preset ? .primary : .secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(settings.cueColorPreset == preset ? preset.color.opacity(0.1) : Color.primary.opacity(0.05)) + ) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(settings.cueColorPreset == preset ? preset.color.opacity(0.4) : Color.clear, lineWidth: 1.5) + ) + } + .buttonStyle(.plain) + } + } + + // Cue Brightness + Text("Cue Brightness") + .font(.system(size: 13, weight: .medium)) + + Picker("", selection: $settings.cueBrightness) { + ForEach(CueBrightness.allCases) { brightness in + Text(brightness.label).tag(brightness) + } + } + .pickerStyle(.segmented) + .labelsHidden() + Divider() // Dimensions @@ -1261,6 +1321,8 @@ struct SettingsView: View { settings.fontSizePreset = .lg settings.fontFamilyPreset = .sans settings.fontColorPreset = .white + settings.cueColorPreset = .white + settings.cueBrightness = .dim settings.overlayMode = .pinned settings.notchDisplayMode = .followMouse settings.pinnedScreenID = 0