From da23e58ac54025e139aca296c429ce038f4cc745 Mon Sep 17 00:00:00 2001 From: Vitalii Parovishnyk <870237+ikorich@users.noreply.github.com> Date: Sat, 31 Jan 2026 13:38:58 +0200 Subject: [PATCH] #30 feat: add social media export presets - Add ExportPreset enum with twitter, linkedin, slack presets - Add preset picker in toolbar export section - Scale exported images to fit preset dimensions - Maintain aspect ratio and center content in preset canvas --- .../Sources/BifcodeFeature/ContentView.swift | 56 +++++++++++++ .../BifcodeFeature/Models/SettingsTypes.swift | 80 +++++++++++++++++++ .../BifcodeFeature/Views/ToolBarView.swift | 17 +++- 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift index 24e6a78..d9e07a1 100644 --- a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift @@ -91,6 +91,7 @@ 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("exportPreset") private var exportPresetRaw: String = ExportPreset.auto.rawValue private var layout: WindowLayout { WindowLayout(rawValue: windowLayoutRaw) ?? .horizontal @@ -112,6 +113,10 @@ public struct ContentView: View { ThemeMode(rawValue: themeModeRaw) ?? .dark } + private var exportPreset: ExportPreset { + ExportPreset(rawValue: exportPresetRaw) ?? .auto + } + public init() {} /// Check if both code panels are empty (export should be disabled) @@ -445,8 +450,59 @@ public struct ContentView: View { let finalImage = NSImage(size: size) finalImage.addRepresentation(scaledBitmapRep) + // Apply export preset scaling if not auto + if let presetSize = exportPreset.size { + return scaleImageToPreset(finalImage, targetSize: presetSize) + } + return finalImage } + + /// Scales an image to fit within the target preset size while maintaining aspect ratio. + /// + /// The image is scaled down to fit within the preset dimensions, centered on a transparent background. + /// If the content is smaller than the preset, it's centered without scaling up. + /// + /// - Parameters: + /// - image: The source image to scale. + /// - targetSize: The target dimensions from the export preset. + /// - Returns: A new image at the exact preset dimensions. + @MainActor + private func scaleImageToPreset(_ image: NSImage, targetSize: CGSize) -> NSImage { + let sourceSize = image.size + + // Calculate scale to fit within target while maintaining aspect ratio + let widthRatio = targetSize.width / sourceSize.width + let heightRatio = targetSize.height / sourceSize.height + let scale = min(widthRatio, heightRatio, 1.0) // Don't scale up, only down + + let scaledWidth = sourceSize.width * scale + let scaledHeight = sourceSize.height * scale + + // Center the scaled content within the target size + let xOffset = (targetSize.width - scaledWidth) / 2 + let yOffset = (targetSize.height - scaledHeight) / 2 + + // Create the final image at preset dimensions + let presetImage = NSImage(size: targetSize) + presetImage.lockFocus() + + // Clear background (transparent) + NSColor.clear.setFill() + NSRect(origin: .zero, size: targetSize).fill() + + // Draw the scaled image centered + image.draw( + in: NSRect(x: xOffset, y: yOffset, width: scaledWidth, height: scaledHeight), + from: .zero, + operation: .copy, + fraction: 1.0 + ) + + presetImage.unlockFocus() + + return presetImage + } } // MARK: - Preview diff --git a/BifcodePackage/Sources/BifcodeFeature/Models/SettingsTypes.swift b/BifcodePackage/Sources/BifcodeFeature/Models/SettingsTypes.swift index 1ce1ca0..2f236c8 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Models/SettingsTypes.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Models/SettingsTypes.swift @@ -286,6 +286,86 @@ public enum ThemeMode: String, CaseIterable, Sendable { } } +// MARK: - Export Preset + +/// Preset export dimensions for different platforms. +/// +/// Provides optimized dimensions for sharing code comparisons on +/// various social media platforms and messaging apps. +/// +/// ## Usage +/// +/// ```swift +/// @AppStorage("exportPreset") private var exportPreset = ExportPreset.auto.rawValue +/// ``` +/// +/// ## Topics +/// +/// ### Presets +/// +/// - ``auto`` +/// - ``twitter`` +/// - ``linkedin`` +/// - ``slack`` +public enum ExportPreset: String, CaseIterable, Sendable { + /// Automatic sizing based on content. + /// + /// The default behavior that calculates dimensions from the + /// actual code content and settings. + case auto + + /// Twitter/X optimized dimensions (1200×675). + /// + /// Optimal for Twitter image cards and timeline display. + case twitter + + /// LinkedIn optimized dimensions (1200×627). + /// + /// Matches LinkedIn's recommended image dimensions for posts. + case linkedin + + /// Slack optimized dimensions (800×600). + /// + /// Works well in Slack messages and previews. + case slack + + /// A human-readable label for display in pickers. + public var label: String { + switch self { + case .auto: "Auto" + case .twitter: "Twitter" + case .linkedin: "LinkedIn" + case .slack: "Slack" + } + } + + /// The target width for this preset, or nil for auto. + public var width: CGFloat? { + switch self { + case .auto: nil + case .twitter: 1200 + case .linkedin: 1200 + case .slack: 800 + } + } + + /// The target height for this preset, or nil for auto. + public var height: CGFloat? { + switch self { + case .auto: nil + case .twitter: 675 + case .linkedin: 627 + case .slack: 600 + } + } + + /// The target size for this preset, or nil for auto. + public var size: CGSize? { + guard let width, let height else { return nil } + return CGSize(width: width, height: height) + } +} + // MARK: - Editor Themes /// Available syntax highlighting themes for the code editor. diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift index 1232ac2..49ebc8d 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift @@ -60,6 +60,7 @@ 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("exportPreset") private var exportPresetRaw: String = ExportPreset.auto.rawValue @AppStorage("selectedLanguage") private var selectedLanguageRaw: String = "swift" @@ -546,6 +547,8 @@ public struct ToolBarView: View { // Secondary Actions VStack(spacing: 8) { + exportPresetPicker + Button { chooseSaveLocation() } label: { @@ -561,7 +564,19 @@ public struct ToolBarView: View { .padding(.top, 32) } } - .frame(minWidth: 80) + .frame(minWidth: 100) + } + + private var exportPresetPicker: some View { + Picker("", selection: $exportPresetRaw) { + ForEach(ExportPreset.allCases, id: \.self) { preset in + Text(preset.label).tag(preset.rawValue) + } + } + .pickerStyle(.menu) + .fixedSize() + .accessibilityLabel("Export Size") + .accessibilityHint("Choose preset dimensions for social media") } private var storeButton: some View {