diff --git a/VoiceInk/Views/Dictionary/EditReplacementSheet.swift b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift index 7dfd91f..ecebe76 100644 --- a/VoiceInk/Views/Dictionary/EditReplacementSheet.swift +++ b/VoiceInk/Views/Dictionary/EditReplacementSheet.swift @@ -8,6 +8,8 @@ struct EditReplacementSheet: View { @State private var originalWord: String @State private var replacementWord: String + @State private var showAlert = false + @State private var alertMessage = "" // MARK: – Initialiser init(manager: WordReplacementManager, originalKey: String) { @@ -24,6 +26,11 @@ struct EditReplacementSheet: View { formContent } .frame(width: 460, height: 560) + .alert("Word Replacement", isPresented: $showAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(alertMessage) + } } // MARK: – Subviews @@ -115,15 +122,23 @@ struct EditReplacementSheet: View { private func saveChanges() { let newOriginal = originalWord.trimmingCharacters(in: .whitespacesAndNewlines) let newReplacement = replacementWord - // Ensure at least one non-empty token let tokens = newOriginal .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } guard !tokens.isEmpty, !newReplacement.isEmpty else { return } - manager.updateReplacement(oldOriginal: originalKey, newOriginal: newOriginal, newReplacement: newReplacement) - dismiss() + let result = manager.updateReplacement(oldOriginal: originalKey, newOriginal: newOriginal, newReplacement: newReplacement) + if result.success { + dismiss() + } else { + if let conflictingWord = result.conflictingWord { + alertMessage = "'\(conflictingWord)' already exists in word replacements" + } else { + alertMessage = "This word replacement already exists" + } + showAlert = true + } } } diff --git a/VoiceInk/Views/Dictionary/WordReplacementView.swift b/VoiceInk/Views/Dictionary/WordReplacementView.swift index ef603b4..52fd99d 100644 --- a/VoiceInk/Views/Dictionary/WordReplacementView.swift +++ b/VoiceInk/Views/Dictionary/WordReplacementView.swift @@ -26,24 +26,68 @@ class WordReplacementManager: ObservableObject { init() { self.replacements = UserDefaults.standard.dictionary(forKey: "wordReplacements") as? [String: String] ?? [:] } - - func addReplacement(original: String, replacement: String) { - // Preserve comma-separated originals as a single entry + + func addReplacement(original: String, replacement: String) -> (success: Bool, conflictingWord: String?) { let trimmed = original.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return } + guard !trimmed.isEmpty else { return (false, nil) } + + let newTokensPairs = trimmed + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .map { (original: $0, lowercased: $0.lowercased()) } + + for existingKey in replacements.keys { + let existingTokens = existingKey + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + .filter { !$0.isEmpty } + + for tokenPair in newTokensPairs { + if existingTokens.contains(tokenPair.lowercased) { + return (false, tokenPair.original) + } + } + } + replacements[trimmed] = replacement + return (true, nil) } func removeReplacement(original: String) { replacements.removeValue(forKey: original) } - func updateReplacement(oldOriginal: String, newOriginal: String, newReplacement: String) { - // Replace old key with the new comma-preserved key - replacements.removeValue(forKey: oldOriginal) + func updateReplacement(oldOriginal: String, newOriginal: String, newReplacement: String) -> (success: Bool, conflictingWord: String?) { let trimmed = newOriginal.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return } + guard !trimmed.isEmpty else { return (false, nil) } + + let newTokensPairs = trimmed + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .map { (original: $0, lowercased: $0.lowercased()) } + + for existingKey in replacements.keys { + if existingKey == oldOriginal { + continue + } + + let existingTokens = existingKey + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + .filter { !$0.isEmpty } + + for tokenPair in newTokensPairs { + if existingTokens.contains(tokenPair.lowercased) { + return (false, tokenPair.original) + } + } + } + + replacements.removeValue(forKey: oldOriginal) replacements[trimmed] = newReplacement + return (true, nil) } } @@ -227,7 +271,9 @@ struct AddReplacementSheet: View { @Environment(\.dismiss) private var dismiss @State private var originalWord = "" @State private var replacementWord = "" - + @State private var showAlert = false + @State private var alertMessage = "" + var body: some View { VStack(spacing: 0) { // Header @@ -385,21 +431,34 @@ struct AddReplacementSheet: View { } } .frame(width: 460, height: 520) + .alert("Word Replacement", isPresented: $showAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(alertMessage) + } } - + private func addReplacement() { - let original = originalWord + let original = originalWord.trimmingCharacters(in: .whitespacesAndNewlines) let replacement = replacementWord - - // Validate that at least one non-empty token exists + let tokens = original .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } guard !tokens.isEmpty && !replacement.isEmpty else { return } - - manager.addReplacement(original: original, replacement: replacement) - dismiss() + + let result = manager.addReplacement(original: original, replacement: replacement) + if result.success { + dismiss() + } else { + if let conflictingWord = result.conflictingWord { + alertMessage = "'\(conflictingWord)' already exists in word replacements" + } else { + alertMessage = "This word replacement already exists" + } + showAlert = true + } } }