GrammarCheckerViewModel.swift 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import AppKit
  2. import Combine
  3. import Foundation
  4. @MainActor
  5. final class GrammarCheckerViewModel: ObservableObject {
  6. static let maxWords = 1_000
  7. @Published var text: String = ""
  8. @Published private(set) var issues: [GrammarIssue] = []
  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 grammarCheckService: any GrammarCheckServicing
  14. private let historyStorage: HistoryStoring
  15. private let textExtractionService: any TextExtracting
  16. init(
  17. grammarCheckService: any GrammarCheckServicing = GrammarCheckService(),
  18. historyStorage: HistoryStoring = HistoryStorageService.shared,
  19. textExtractionService: any TextExtracting = TextExtractionService()
  20. ) {
  21. self.grammarCheckService = grammarCheckService
  22. self.historyStorage = historyStorage
  23. self.textExtractionService = textExtractionService
  24. }
  25. var wordCountLimitLabel: String {
  26. "\(text.wordCount)/\(Self.maxWords)"
  27. }
  28. var issueCountLabel: String {
  29. let count = issues.count
  30. return "\(count) Issue\(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 = String.importedApplyingWordLimit(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 = String.importedApplyingWordLimit(extracted, maxWords: Self.maxWords, setError: { errorMessage = $0 })
  45. resetResults()
  46. } catch {
  47. errorMessage = error.localizedDescription
  48. }
  49. }
  50. }
  51. func notifyWordLimitReached() {
  52. errorMessage = String.wordLimitExceededMessage(maxWords: Self.maxWords)
  53. }
  54. func handleTextChange() {
  55. let (limited, exceeded) = text.applyingWordLimit(maxWords: Self.maxWords)
  56. if exceeded {
  57. text = limited
  58. errorMessage = String.wordLimitExceededMessage(maxWords: Self.maxWords)
  59. } else if errorMessage == String.wordLimitExceededMessage(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. issues = []
  72. correctedText = ""
  73. hasResults = false
  74. errorMessage = nil
  75. }
  76. func checkGrammar() {
  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 grammarCheckService.checkGrammar(in: content)
  87. issues = result.issues
  88. correctedText = result.correctedText
  89. hasResults = true
  90. saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText)
  91. } catch {
  92. errorMessage = error.localizedDescription
  93. }
  94. isChecking = false
  95. }
  96. }
  97. func copyCorrectedText() {
  98. guard !correctedText.isEmpty else { return }
  99. NSPasteboard.general.clearContents()
  100. NSPasteboard.general.setString(correctedText, forType: .string)
  101. }
  102. func restore(from entry: HistoryEntry) {
  103. guard case .grammarChecker(let data) = entry.payload else { return }
  104. text = data.text
  105. issues = data.issues.map {
  106. GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
  107. }
  108. correctedText = data.correctedText
  109. hasResults = true
  110. errorMessage = nil
  111. }
  112. private func saveToHistory(text: String, issues: [GrammarIssue], correctedText: String) {
  113. let payload = HistoryPayload.grammarChecker(
  114. GrammarCheckerHistoryData(
  115. text: text,
  116. issues: issues.map(\.stored),
  117. correctedText: correctedText
  118. )
  119. )
  120. let entry = HistoryEntry(
  121. tool: .grammarChecker,
  122. title: HistoryHelpers.title(from: text, fallback: "Grammar Check"),
  123. preview: HistoryHelpers.preview(from: correctedText.isEmpty ? text : correctedText),
  124. payload: payload
  125. )
  126. historyStorage.save(entry)
  127. }
  128. }