SpellCheckerViewModel.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import AppKit
  2. import Combine
  3. import Foundation
  4. @MainActor
  5. final class SpellCheckerViewModel: ObservableObject {
  6. static let maxWords = 1_000
  7. @Published var text: String = ""
  8. @Published private(set) var mistakes: [SpellingMistake] = []
  9. @Published private(set) var correctedText: String = ""
  10. @Published private(set) var hasResults = false
  11. @Published private(set) var isChecking = false
  12. @Published private(set) var errorMessage: String?
  13. private let spellCheckService: any SpellCheckServicing
  14. private let historyStorage: HistoryStoring
  15. private let textExtractionService: any TextExtracting
  16. init(
  17. spellCheckService: any SpellCheckServicing = SpellCheckService(),
  18. historyStorage: HistoryStoring = HistoryStorageService.shared,
  19. textExtractionService: any TextExtracting = TextExtractionService()
  20. ) {
  21. self.spellCheckService = spellCheckService
  22. self.historyStorage = historyStorage
  23. self.textExtractionService = textExtractionService
  24. }
  25. var wordCountLimitLabel: String {
  26. "\(text.wordCount)/\(Self.maxWords)"
  27. }
  28. var mistakeCountLabel: String {
  29. let count = mistakes.count
  30. return "\(count) Mistake\(count == 1 ? "" : "s")"
  31. }
  32. var canCheck: Bool {
  33. !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isChecking
  34. }
  35. func pasteText() {
  36. guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
  37. text = WordLimit.applyImported(clipboard, maxWords: Self.maxWords, setError: { errorMessage = $0 })
  38. resetResults()
  39. }
  40. func uploadFile() {
  41. Task {
  42. do {
  43. guard let extracted = try await textExtractionService.pickAndExtractText() else { return }
  44. text = WordLimit.applyImported(extracted, maxWords: Self.maxWords, setError: { errorMessage = $0 })
  45. resetResults()
  46. } catch {
  47. errorMessage = error.localizedDescription
  48. }
  49. }
  50. }
  51. func notifyWordLimitReached() {
  52. errorMessage = WordLimit.message(maxWords: Self.maxWords)
  53. }
  54. func handleTextChange() {
  55. let (limited, exceeded) = WordLimit.apply(text, maxWords: Self.maxWords)
  56. if exceeded {
  57. text = limited
  58. errorMessage = WordLimit.message(maxWords: Self.maxWords)
  59. } else if errorMessage == WordLimit.message(maxWords: Self.maxWords) {
  60. errorMessage = nil
  61. }
  62. if hasResults {
  63. resetResults()
  64. }
  65. }
  66. func clearText() {
  67. text = ""
  68. resetResults()
  69. }
  70. func resetResults() {
  71. mistakes = []
  72. correctedText = ""
  73. hasResults = false
  74. errorMessage = nil
  75. }
  76. func checkSpelling() {
  77. let content = text.trimmingCharacters(in: .whitespacesAndNewlines)
  78. guard !content.isEmpty else {
  79. resetResults()
  80. return
  81. }
  82. isChecking = true
  83. errorMessage = nil
  84. Task {
  85. do {
  86. let result = try await spellCheckService.checkSpelling(in: content)
  87. mistakes = result.mistakes
  88. correctedText = result.correctedText
  89. hasResults = true
  90. saveToHistory(text: content, mistakes: result.mistakes, correctedText: result.correctedText)
  91. } catch {
  92. errorMessage = error.localizedDescription
  93. }
  94. isChecking = false
  95. }
  96. }
  97. func restore(from entry: HistoryEntry) {
  98. guard case .spellingChecker(let data) = entry.payload else { return }
  99. text = data.text
  100. mistakes = data.mistakes.map {
  101. SpellingMistake(original: $0.original, suggestion: $0.suggestion)
  102. }
  103. correctedText = data.correctedText
  104. hasResults = true
  105. errorMessage = nil
  106. }
  107. private func saveToHistory(text: String, mistakes: [SpellingMistake], correctedText: String) {
  108. let payload = HistoryPayload.spellingChecker(
  109. SpellCheckerHistoryData(
  110. text: text,
  111. mistakes: mistakes.map(\.stored),
  112. correctedText: correctedText
  113. )
  114. )
  115. let entry = HistoryEntry(
  116. tool: .spellingChecker,
  117. title: HistoryHelpers.title(from: text, fallback: "Spelling Check"),
  118. preview: HistoryHelpers.preview(from: correctedText),
  119. payload: payload
  120. )
  121. historyStorage.save(entry)
  122. }
  123. func copyCorrectedText() {
  124. guard !correctedText.isEmpty else { return }
  125. NSPasteboard.general.clearContents()
  126. NSPasteboard.general.setString(correctedText, forType: .string)
  127. }
  128. }