From bfb6d58393a60a882b08d18c4b38d1446ac469c6 Mon Sep 17 00:00:00 2001 From: Vitalii Parovishnyk <870237+ikorich@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:38:28 +0200 Subject: [PATCH] #27 feat: add watermark support to exports - Add showWatermark and watermarkText settings via @AppStorage - Add watermark overlay to ExportView at bottom-center - Add semantic watermarkText color (35% opacity primary) - Add watermark configuration UI to ToolBarView with toggle and text field - Default watermark text is "bifcode", shown by default --- BifcodePackage/Package.resolved | 123 ++++++++++++++++++ .../Sources/BifcodeFeature/ContentView.swift | 4 + .../Extensions/Color+Semantic.swift | 12 +- .../BifcodeFeature/Views/ExportView.swift | 46 +++++-- .../BifcodeFeature/Views/ToolBarView.swift | 122 +++++++++++------ 5 files changed, 257 insertions(+), 50 deletions(-) create mode 100644 BifcodePackage/Package.resolved diff --git a/BifcodePackage/Package.resolved b/BifcodePackage/Package.resolved new file mode 100644 index 0000000..1f753ad --- /dev/null +++ b/BifcodePackage/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "940797f4df50fc3ed2854a3b92465fc27d52127b53e5d66f7742a229653030a0", + "pins" : [ + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "424453d2232c9912933a3b5a1f3d3df669404ed0", + "version" : "0.15.2" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, + { + "identity" : "developersupportstore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/IGRSoft/DeveloperSupportStore", + "state" : { + "revision" : "c9d8f3282a649273fdbf4d3841e45e10a4951ee4", + "version" : "1.0.3" + } + }, + { + "identity" : "rearrange", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/Rearrange", + "state" : { + "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", + "version" : "2.0.0" + } + }, + { + "identity" : "storehelper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/russell-archer/StoreHelper.git", + "state" : { + "revision" : "1c55f49473d847fd5a2f495aabe45f6e5ea3e2ef", + "version" : "2.7.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "a3877065a7d72ee40e1fa64e13b9e33e846c667c", + "version" : "0.63.1" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" + } + } + ], + "version" : 3 +} diff --git a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift index 24e6a78..55db0f0 100644 --- a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift @@ -91,6 +91,8 @@ public struct ContentView: View { @AppStorage("fontSize") private var fontSize: Double = 14 @AppStorage("selectedTheme") private var selectedThemeRaw: String = EditorThemeOption.atomOneDark.rawValue @AppStorage("themeMode") private var themeModeRaw: String = ThemeMode.dark.rawValue + @AppStorage("showWatermark") private var showWatermark: Bool = true + @AppStorage("watermarkText") private var watermarkText: String = "bifcode" private var layout: WindowLayout { WindowLayout(rawValue: windowLayoutRaw) ?? .horizontal @@ -223,6 +225,8 @@ public struct ContentView: View { fontSize: fontSize, theme: selectedTheme.editorTheme, themeMode: themeMode, + showWatermark: showWatermark, + watermarkText: watermarkText, panelWidth: panelWidth, doPanelHeight: doPanelHeight, dontPanelHeight: dontPanelHeight diff --git a/BifcodePackage/Sources/BifcodeFeature/Extensions/Color+Semantic.swift b/BifcodePackage/Sources/BifcodeFeature/Extensions/Color+Semantic.swift index 69bc7fb..07fc7df 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Extensions/Color+Semantic.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Extensions/Color+Semantic.swift @@ -160,12 +160,20 @@ public extension Color { } // MARK: - Toast Colors - + /// Toast success background - uses semantic green static let toastSuccess = Color("IndicatorDo", bundle: .module) - + /// Toast error background - uses semantic red static let toastError = Color("IndicatorDont", bundle: .module) + + // MARK: - Watermark Colors + + /// Watermark text color for exported images + /// Semi-transparent gray that works on both light and dark backgrounds + static var watermarkText: Color { + Color.primary.opacity(0.35) + } // MARK: - Theme Mode Aware Colors diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ExportView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ExportView.swift index 6f1b795..6cfccde 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ExportView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ExportView.swift @@ -68,9 +68,17 @@ struct ExportView: View { /// The current theme mode for window styling. let themeMode: ThemeMode - + + // Watermark settings + + /// Whether to show the watermark in exports. + let showWatermark: Bool + + /// The text to display as watermark. + let watermarkText: String + // Panel dimensions for export - + /// Width of each panel in points. let panelWidth: CGFloat @@ -81,18 +89,30 @@ struct ExportView: View { let dontPanelHeight: CGFloat var body: some View { - Group { - if layout == .horizontal { - HStack(alignment: .top, spacing: 24) { - panels - } - } else { - VStack(spacing: 24) { - panels + ZStack(alignment: .bottom) { + Group { + if layout == .horizontal { + HStack(alignment: .top, spacing: 24) { + panels + } + } else { + VStack(spacing: 24) { + panels + } } } + .padding(24) + + // Watermark overlay at bottom-center + if showWatermark, !watermarkText.isEmpty { + Text(watermarkText) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(Color.watermarkText) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .padding(.bottom, 8) + } } - .padding(24) .background(Color.clear) } @@ -160,6 +180,8 @@ struct ExportView: View { fontSize: 14, theme: .atomOneDark, themeMode: .dark, + showWatermark: true, + watermarkText: "bifcode", panelWidth: 350, doPanelHeight: 120, dontPanelHeight: 120 @@ -195,6 +217,8 @@ struct ExportView: View { fontSize: 14, theme: .atomOneDark, themeMode: .dark, + showWatermark: true, + watermarkText: "bifcode", panelWidth: 400, doPanelHeight: 120, dontPanelHeight: 120 diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift index 1232ac2..cc4999b 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift @@ -60,8 +60,12 @@ public struct ToolBarView: View { @AppStorage("fontSize") private var fontSize: Double = 14 @AppStorage("selectedTheme") private var selectedThemeRaw: String = EditorThemeOption.atomOneDark.rawValue @AppStorage("themeMode") private var themeModeRaw: String = ThemeMode.dark.rawValue - + @AppStorage("selectedLanguage") private var selectedLanguageRaw: String = "swift" + + // Watermark settings + @AppStorage("showWatermark") private var showWatermark: Bool = true + @AppStorage("watermarkText") private var watermarkText: String = "bifcode" @Environment(\.accessibilityReduceMotion) private var reduceMotion @@ -523,45 +527,89 @@ public struct ToolBarView: View { private let storeService = StoreService() private var exportButton: some View { - VStack(spacing: 12) { - // Primary Export Action - HIG: Use .prominent for key actions - Button { onExport() } label: { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 22, weight: .medium)) - .frame(width: 48, height: 48) - } - .buttonStyle(.borderedProminent) - .clipShape(Circle()) - .scaleEffect(isExportButtonHovered && !isExportDisabled ? 1.05 : 1.0) - .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: isExportButtonHovered) - .onHover { hovering in - isExportButtonHovered = hovering - } - .disabled(isExportDisabled) - .help("Export as PNG") - .accessibilityLabel("Export as PNG") - .accessibilityHint(isExportDisabled - ? "Disabled: Add code to enable export" - : "Save comparison image to selected folder") - - // Secondary Actions - VStack(spacing: 8) { - Button { - chooseSaveLocation() - } label: { - Label("Location", systemImage: "folder") - .font(.caption) + HStack(alignment: .top, spacing: 16) { + // Watermark Settings + watermarkSettingsView() + + // Export Actions + VStack(spacing: 12) { + // Primary Export Action - HIG: Use .prominent for key actions + Button { onExport() } label: { + Image(systemName: "square.and.arrow.up") + .font(.system(size: 22, weight: .medium)) + .frame(width: 48, height: 48) + } + .buttonStyle(.borderedProminent) + .clipShape(Circle()) + .scaleEffect(isExportButtonHovered && !isExportDisabled ? 1.05 : 1.0) + .animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: isExportButtonHovered) + .onHover { hovering in + isExportButtonHovered = hovering + } + .disabled(isExportDisabled) + .help("Export as PNG") + .accessibilityLabel("Export as PNG") + .accessibilityHint(isExportDisabled + ? "Disabled: Add code to enable export" + : "Save comparison image to selected folder") + + // Secondary Actions + VStack(spacing: 8) { + Button { + chooseSaveLocation() + } label: { + Label("Location", systemImage: "folder") + .font(.caption) + } + .buttonStyle(.borderless) + .help("Choose save location") + .accessibilityLabel("Choose Save Location") + .accessibilityHint("Select folder for exported images") + + storeButton + .padding(.top, 32) } - .buttonStyle(.borderless) - .help("Choose save location") - .accessibilityLabel("Choose Save Location") - .accessibilityHint("Select folder for exported images") - - storeButton - .padding(.top, 32) } + .frame(minWidth: 80) } - .frame(minWidth: 80) + } + + @ViewBuilder + private func watermarkSettingsView() -> some View { + VStack(alignment: .leading, spacing: 12) { + // Section Header + Text("Watermark") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Color.sectionHeader) + + // Enable/Disable Toggle + Toggle("Show", isOn: $showWatermark) + .toggleStyle(.checkbox) + .font(.caption) + .accessibilityLabel("Show Watermark") + .accessibilityHint("Toggle watermark visibility in exports") + + // Watermark Text Field + VStack(alignment: .leading, spacing: 4) { + Text("Text") + .font(.caption) + .foregroundStyle(Color.tertiaryText) + TextField("bifcode", text: $watermarkText) + .textFieldStyle(.roundedBorder) + .frame(width: 100) + .disabled(!showWatermark) + .onChange(of: watermarkText) { _, newValue in + if newValue.count > 30 { + watermarkText = String(newValue.prefix(30)) + } + } + .accessibilityLabel("Watermark Text") + .accessibilityHint(showWatermark + ? "Custom text shown at bottom of exports" + : "Disabled: Enable watermark to edit") + } + } + .fixedSize(horizontal: true, vertical: false) } private var storeButton: some View {