371 lines
14 KiB
Swift
371 lines
14 KiB
Swift
import SwiftUI
|
|
|
|
extension String: Identifiable {
|
|
public var id: String { self }
|
|
}
|
|
|
|
class WordReplacementManager: ObservableObject {
|
|
@Published var replacements: [String: String] {
|
|
didSet {
|
|
UserDefaults.standard.set(replacements, forKey: "wordReplacements")
|
|
}
|
|
}
|
|
|
|
@Published var isEnabled: Bool {
|
|
didSet {
|
|
UserDefaults.standard.set(isEnabled, forKey: "IsWordReplacementEnabled")
|
|
}
|
|
}
|
|
|
|
init() {
|
|
self.replacements = UserDefaults.standard.dictionary(forKey: "wordReplacements") as? [String: String] ?? [:]
|
|
self.isEnabled = UserDefaults.standard.bool(forKey: "IsWordReplacementEnabled")
|
|
}
|
|
|
|
func addReplacement(original: String, replacement: String) {
|
|
replacements[original] = replacement
|
|
}
|
|
|
|
func removeReplacement(original: String) {
|
|
replacements.removeValue(forKey: original)
|
|
}
|
|
|
|
func updateReplacement(oldOriginal: String, newOriginal: String, newReplacement: String) {
|
|
// Remove the old key if the original text has changed
|
|
if oldOriginal != newOriginal {
|
|
replacements.removeValue(forKey: oldOriginal)
|
|
}
|
|
// Update (or insert) the new key/value pair
|
|
replacements[newOriginal] = newReplacement
|
|
}
|
|
}
|
|
|
|
struct WordReplacementView: View {
|
|
@StateObject private var manager = WordReplacementManager()
|
|
@State private var showAddReplacementModal = false
|
|
@State private var showAlert = false
|
|
@State private var editingOriginal: String? = nil
|
|
|
|
@State private var alertMessage = ""
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
// Info Section with Toggle
|
|
GroupBox {
|
|
HStack {
|
|
Label {
|
|
Text("Define word replacements to automatically replace specific words or phrases")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.frame(alignment: .leading)
|
|
} icon: {
|
|
Image(systemName: "info.circle.fill")
|
|
.foregroundColor(.blue)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Toggle("Enable", isOn: $manager.isEnabled)
|
|
.toggleStyle(.switch)
|
|
.labelsHidden()
|
|
.help("Enable automatic word replacement after transcription")
|
|
}
|
|
}
|
|
|
|
VStack(spacing: 0) {
|
|
// Header with action button
|
|
HStack {
|
|
Text("Word Replacements")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Button(action: { showAddReplacementModal = true }) {
|
|
Image(systemName: "plus")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Add new replacement")
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.background(Color(.controlBackgroundColor))
|
|
|
|
Divider()
|
|
|
|
// Content
|
|
if manager.replacements.isEmpty {
|
|
EmptyStateView(showAddModal: $showAddReplacementModal)
|
|
} else {
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
ForEach(Array(manager.replacements.keys.sorted()), id: \.self) { original in
|
|
ReplacementRow(
|
|
original: original,
|
|
replacement: manager.replacements[original] ?? "",
|
|
onDelete: { manager.removeReplacement(original: original) },
|
|
onEdit: { editingOriginal = original }
|
|
)
|
|
|
|
if original != manager.replacements.keys.sorted().last {
|
|
Divider()
|
|
.padding(.leading, 32)
|
|
}
|
|
}
|
|
}
|
|
.background(Color(.controlBackgroundColor))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.sheet(isPresented: $showAddReplacementModal) {
|
|
AddReplacementSheet(manager: manager)
|
|
}
|
|
// Edit existing replacement
|
|
.sheet(item: $editingOriginal) { original in
|
|
EditReplacementSheet(manager: manager, originalKey: original)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
struct EmptyStateView: View {
|
|
@Binding var showAddModal: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "text.word.spacing")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("No Replacements")
|
|
.font(.headline)
|
|
|
|
Text("Add word replacements to automatically replace text during AI enhancement.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.frame(maxWidth: 250)
|
|
|
|
Button("Add Replacement") {
|
|
showAddModal = true
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.regular)
|
|
.padding(.top, 8)
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
struct AddReplacementSheet: View {
|
|
@ObservedObject var manager: WordReplacementManager
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var originalWord = ""
|
|
@State private var replacementWord = ""
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Header
|
|
HStack {
|
|
Button("Cancel", role: .cancel) {
|
|
dismiss()
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.keyboardShortcut(.escape, modifiers: [])
|
|
|
|
Spacer()
|
|
|
|
Text("Add Word Replacement")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Button("Add") {
|
|
addReplacement()
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.small)
|
|
.disabled(originalWord.isEmpty || replacementWord.isEmpty)
|
|
.keyboardShortcut(.return, modifiers: [])
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 12)
|
|
.background(CardBackground(isSelected: false))
|
|
|
|
Divider()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
// Description
|
|
Text("Define a word or phrase to be automatically replaced during AI enhancement.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal)
|
|
.padding(.top, 8)
|
|
|
|
// Form Content
|
|
VStack(spacing: 16) {
|
|
// Original Text Section
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack {
|
|
Text("Original Text")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
|
|
Text("Required")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
TextField("Enter word or phrase to replace", text: $originalWord)
|
|
.textFieldStyle(.roundedBorder)
|
|
.font(.body)
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
// Replacement Text Section
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack {
|
|
Text("Replacement Text")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
|
|
Text("Required")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
TextEditor(text: $replacementWord)
|
|
.font(.body)
|
|
.frame(height: 100)
|
|
.padding(8)
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(6)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color(.separatorColor), lineWidth: 1)
|
|
)
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Example Section
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Example")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
HStack(spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Original:")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
Text("my website link")
|
|
.font(.callout)
|
|
}
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Replacement:")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
Text("https://tryvoiceink.com")
|
|
.font(.callout)
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(8)
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.top, 8)
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
}
|
|
.frame(width: 460, height: 480)
|
|
}
|
|
|
|
private func addReplacement() {
|
|
let original = originalWord
|
|
let replacement = replacementWord
|
|
|
|
guard !original.isEmpty && !replacement.isEmpty else { return }
|
|
|
|
manager.addReplacement(original: original, replacement: replacement)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
struct ReplacementRow: View {
|
|
let original: String
|
|
let replacement: String
|
|
let onDelete: () -> Void
|
|
let onEdit: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 16) {
|
|
// Original Text Container
|
|
HStack {
|
|
Text(original)
|
|
.font(.body)
|
|
.lineLimit(2)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(6)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
|
|
// Arrow
|
|
Image(systemName: "arrow.right")
|
|
.foregroundColor(.secondary)
|
|
.font(.system(size: 12))
|
|
|
|
// Replacement Text Container
|
|
HStack {
|
|
Text(replacement)
|
|
.font(.body)
|
|
.lineLimit(2)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(6)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
|
|
// Edit Button
|
|
Button(action: onEdit) {
|
|
Image(systemName: "pencil.circle.fill")
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundColor(.accentColor)
|
|
.font(.system(size: 16))
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Edit replacement")
|
|
|
|
// Delete Button
|
|
Button(action: onDelete) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(.red)
|
|
.font(.system(size: 16))
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Remove replacement")
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.contentShape(Rectangle())
|
|
.background(Color(.controlBackgroundColor))
|
|
}
|
|
} |