LanguageTranslatorViewModel.swift 4.6 KB

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