From 2a9ead63deda8ae428949c60830f8bdb78dfbe6f Mon Sep 17 00:00:00 2001 From: Beingpax Date: Sat, 2 Aug 2025 00:43:15 +0545 Subject: [PATCH] Add edit functionality to word replacements --- .../Dictionary/EditReplacementSheet.swift | 134 ++++++++++++++++++ .../Dictionary/WordReplacementView.swift | 34 ++++- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 VoiceInk/Views/Dictionary/EditReplacementSheet.swift diff --git a/VoiceInk/Views/Dictionary/EditReplacementSheet.swift b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift new file mode 100644 index 0000000..988ffce --- /dev/null +++ b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift @@ -0,0 +1,134 @@ +import SwiftUI + +/// A reusable sheet for editing an existing word replacement entry. +/// Mirrors the UI of `AddReplacementSheet` for consistency while pre-populating +/// the fields with the existing values. +struct EditReplacementSheet: View { + @ObservedObject var manager: WordReplacementManager + let originalKey: String + + @Environment(\.dismiss) private var dismiss + + @State private var originalWord: String + @State private var replacementWord: String + + // MARK: – Initialiser + init(manager: WordReplacementManager, originalKey: String) { + self.manager = manager + self.originalKey = originalKey + _originalWord = State(initialValue: originalKey) + _replacementWord = State(initialValue: manager.replacements[originalKey] ?? "") + } + + var body: some View { + VStack(spacing: 0) { + header + Divider() + formContent + } + .frame(width: 460, height: 480) + } + + // MARK: – Subviews + private var header: some View { + HStack { + Button("Cancel", role: .cancel) { dismiss() } + .buttonStyle(.borderless) + .keyboardShortcut(.escape, modifiers: []) + + Spacer() + + Text("Edit Word Replacement") + .font(.headline) + + Spacer() + + Button("Save") { saveChanges() } + .buttonStyle(.borderedProminent) + .controlSize(.small) + .disabled(originalWord.isEmpty || replacementWord.isEmpty) + .keyboardShortcut(.return, modifiers: []) + } + .padding(.horizontal) + .padding(.vertical, 12) + .background(CardBackground(isSelected: false)) + } + + private var formContent: some View { + ScrollView { + VStack(spacing: 20) { + descriptionSection + inputSection + } + .padding(.vertical) + } + } + + private var descriptionSection: some View { + Text("Update the word or phrase that should be automatically replaced during AI enhancement.") + .font(.subheadline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .padding(.top, 8) + } + + private var inputSection: some View { + VStack(spacing: 16) { + // Original Text Field + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Original Text") + .font(.headline) + Text("Required") + .font(.caption) + .foregroundColor(.secondary) + } + TextField("Enter word or phrase to replace", text: $originalWord) + .textFieldStyle(.roundedBorder) + } + .padding(.horizontal) + + // Replacement Text Field + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Replacement Text") + .font(.headline) + 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) + } + } + + // MARK: – Actions + private func saveChanges() { + let newOriginal = originalWord.trimmingCharacters(in: .whitespacesAndNewlines) + let newReplacement = replacementWord.trimmingCharacters(in: .whitespacesAndNewlines) + guard !newOriginal.isEmpty, !newReplacement.isEmpty else { return } + + manager.updateReplacement(oldOriginal: originalKey, newOriginal: newOriginal, newReplacement: newReplacement) + dismiss() + } +} + +// MARK: – Preview +#if DEBUG +struct EditReplacementSheet_Previews: PreviewProvider { + static var previews: some View { + EditReplacementSheet(manager: WordReplacementManager(), originalKey: "hello") + } +} +#endif \ No newline at end of file diff --git a/VoiceInk/Views/Dictionary/WordReplacementView.swift b/VoiceInk/Views/Dictionary/WordReplacementView.swift index a487be3..ee8b4d0 100644 --- a/VoiceInk/Views/Dictionary/WordReplacementView.swift +++ b/VoiceInk/Views/Dictionary/WordReplacementView.swift @@ -1,5 +1,9 @@ import SwiftUI +extension String: Identifiable { + public var id: String { self } +} + class WordReplacementManager: ObservableObject { @Published var replacements: [String: String] { didSet { @@ -25,12 +29,23 @@ class WordReplacementManager: ObservableObject { 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 { @@ -88,7 +103,8 @@ struct WordReplacementView: View { ReplacementRow( original: original, replacement: manager.replacements[original] ?? "", - onDelete: { manager.removeReplacement(original: original) } + onDelete: { manager.removeReplacement(original: original) }, + onEdit: { editingOriginal = original } ) if original != manager.replacements.keys.sorted().last { @@ -106,6 +122,11 @@ struct WordReplacementView: View { .sheet(isPresented: $showAddReplacementModal) { AddReplacementSheet(manager: manager) } + // Edit existing replacement + .sheet(item: $editingOriginal) { original in + EditReplacementSheet(manager: manager, originalKey: original) + } + } } @@ -287,6 +308,7 @@ struct ReplacementRow: View { let original: String let replacement: String let onDelete: () -> Void + let onEdit: () -> Void var body: some View { HStack(spacing: 16) { @@ -321,6 +343,16 @@ struct ReplacementRow: View { } .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")