LanguageTranslatorViewModel.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import AppKit
  2. import Combine
  3. import Foundation
  4. @MainActor
  5. final class LanguageTranslatorViewModel: ObservableObject {
  6. static let maxWords = 10_000
  7. @Published var sourceText: String = ""
  8. @Published var translatedText: String = ""
  9. @Published var sourceLanguage: TranslationLanguage = .defaultSource
  10. @Published var targetLanguage: TranslationLanguage = .defaultTarget
  11. @Published private(set) var isTranslating = false
  12. @Published private(set) var errorMessage: String?
  13. private let translationService: any TranslationServicing
  14. private let historyStorage: HistoryStoring
  15. private let textExtractionService: any TextExtracting
  16. init(
  17. translationService: any TranslationServicing = TranslationService(),
  18. historyStorage: HistoryStoring = HistoryStorageService.shared,
  19. textExtractionService: any TextExtracting = TextExtractionService()
  20. ) {
  21. self.translationService = translationService
  22. self.historyStorage = historyStorage
  23. self.textExtractionService = textExtractionService
  24. }
  25. var wordCountLimitLabel: String {
  26. "\(sourceText.wordCount)/\(Self.maxWords)"
  27. }
  28. var canTranslate: Bool {
  29. !sourceText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isTranslating
  30. }
  31. var sourceLanguageOptions: [TranslationLanguage] {
  32. TranslationLanguage.all
  33. }
  34. var targetLanguageOptions: [TranslationLanguage] {
  35. TranslationLanguage.all.filter { $0 != sourceLanguage }
  36. }
  37. func pasteText() {
  38. guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
  39. sourceText = clipboard.truncated(toMaxWords: Self.maxWords)
  40. translatedText = ""
  41. errorMessage = nil
  42. }
  43. func uploadFile() {
  44. Task {
  45. do {
  46. guard let extracted = try await textExtractionService.pickAndExtractText() else { return }
  47. sourceText = extracted.truncated(toMaxWords: Self.maxWords)
  48. translatedText = ""
  49. errorMessage = nil
  50. } catch {
  51. errorMessage = error.localizedDescription
  52. }
  53. }
  54. }
  55. func clearText() {
  56. sourceText = ""
  57. translatedText = ""
  58. errorMessage = nil
  59. }
  60. func swapLanguages() {
  61. let previousSource = sourceLanguage
  62. sourceLanguage = targetLanguage
  63. targetLanguage = previousSource
  64. let previousSourceText = sourceText
  65. sourceText = translatedText
  66. translatedText = previousSourceText
  67. }
  68. func translate() {
  69. let trimmed = sourceText.trimmingCharacters(in: .whitespacesAndNewlines)
  70. guard !trimmed.isEmpty else { return }
  71. isTranslating = true
  72. translatedText = ""
  73. errorMessage = nil
  74. Task {
  75. do {
  76. translatedText = try await translationService.translate(
  77. trimmed,
  78. from: sourceLanguage,
  79. to: targetLanguage
  80. )
  81. saveToHistory(sourceText: trimmed, translatedText: translatedText)
  82. } catch {
  83. errorMessage = error.localizedDescription
  84. }
  85. isTranslating = false
  86. }
  87. }
  88. func restore(from entry: HistoryEntry) {
  89. guard case .languageTranslator(let data) = entry.payload else { return }
  90. sourceText = data.sourceText
  91. translatedText = data.translatedText
  92. sourceLanguage = TranslationLanguage.all.first { $0.id == data.sourceLanguageID } ?? .defaultSource
  93. targetLanguage = TranslationLanguage.all.first { $0.id == data.targetLanguageID } ?? .defaultTarget
  94. errorMessage = nil
  95. }
  96. private func saveToHistory(sourceText: String, translatedText: String) {
  97. let payload = HistoryPayload.languageTranslator(
  98. TranslationHistoryData(
  99. sourceText: sourceText,
  100. translatedText: translatedText,
  101. sourceLanguageID: sourceLanguage.id,
  102. targetLanguageID: targetLanguage.id
  103. )
  104. )
  105. let entry = HistoryEntry(
  106. tool: .languageTranslator,
  107. title: HistoryHelpers.title(from: sourceText, fallback: "Translation"),
  108. preview: HistoryHelpers.preview(from: translatedText),
  109. payload: payload
  110. )
  111. historyStorage.save(entry)
  112. }
  113. func syncTargetLanguageIfNeeded() {
  114. guard targetLanguage == sourceLanguage else { return }
  115. targetLanguage = TranslationLanguage.all.first { $0 != sourceLanguage } ?? .defaultTarget
  116. }
  117. }