PostGeneratorViewModel.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import AppKit
  2. import SwiftUI
  3. import UniformTypeIdentifiers
  4. @MainActor
  5. @Observable
  6. final class PostGeneratorViewModel {
  7. var draft = PostDraft()
  8. var selectedTab: PostGeneratorTab = .compose
  9. var isGenerating = false
  10. var errorMessage: String?
  11. var successMessage: String?
  12. var showImageImporter = false
  13. private let generationService: any PostGenerationServiceProtocol
  14. init(generationService: any PostGenerationServiceProtocol = MockPostGenerationService()) {
  15. self.generationService = generationService
  16. }
  17. var formattedSubreddit: String {
  18. let trimmed = draft.subreddit.trimmingCharacters(in: .whitespaces)
  19. guard !trimmed.isEmpty else { return "r/subreddit" }
  20. return trimmed.hasPrefix("r/") ? trimmed : "r/\(trimmed)"
  21. }
  22. var canGenerate: Bool {
  23. !draft.topic.trimmingCharacters(in: .whitespaces).isEmpty
  24. && !draft.subreddit.trimmingCharacters(in: .whitespaces).isEmpty
  25. && !isGenerating
  26. }
  27. var canAddPollOption: Bool {
  28. draft.pollOptions.count < 6
  29. }
  30. var canRemovePollOption: Bool {
  31. draft.pollOptions.count > 2
  32. }
  33. func selectPostType(_ type: RedditPostType) {
  34. draft.postType = type
  35. clearMessages()
  36. }
  37. func addPollOption() {
  38. guard canAddPollOption else { return }
  39. draft.pollOptions.append(PollOption())
  40. }
  41. func removePollOption(_ option: PollOption) {
  42. guard canRemovePollOption else { return }
  43. draft.pollOptions.removeAll { $0.id == option.id }
  44. }
  45. func setImage(from url: URL) {
  46. draft.imageFileURL = url
  47. clearMessages()
  48. }
  49. func removeImage() {
  50. draft.imageFileURL = nil
  51. }
  52. func generatePost() async {
  53. guard canGenerate else { return }
  54. isGenerating = true
  55. errorMessage = nil
  56. successMessage = nil
  57. do {
  58. let result = try await generationService.generatePost(from: draft)
  59. draft.title = result.title
  60. if draft.postType == .text || draft.postType == .poll {
  61. draft.body = result.body
  62. }
  63. if let flair = result.suggestedFlair, draft.flair.isEmpty {
  64. draft.flair = flair
  65. }
  66. successMessage = "Post generated successfully."
  67. } catch {
  68. errorMessage = error.localizedDescription
  69. }
  70. isGenerating = false
  71. }
  72. func copyToClipboard() {
  73. let content = exportText()
  74. NSPasteboard.general.clearContents()
  75. NSPasteboard.general.setString(content, forType: .string)
  76. successMessage = "Copied to clipboard."
  77. errorMessage = nil
  78. }
  79. func resetDraft() {
  80. draft = PostDraft()
  81. selectedTab = .compose
  82. clearMessages()
  83. }
  84. func exportText() -> String {
  85. var lines: [String] = []
  86. lines.append("Subreddit: \(formattedSubreddit)")
  87. lines.append("Type: \(draft.postType.title)")
  88. if !draft.title.isEmpty {
  89. lines.append("Title: \(draft.title)")
  90. }
  91. switch draft.postType {
  92. case .text:
  93. if !draft.body.isEmpty { lines.append("\n\(draft.body)") }
  94. case .image:
  95. if let url = draft.imageFileURL {
  96. lines.append("Image: \(url.lastPathComponent)")
  97. }
  98. if !draft.body.isEmpty { lines.append("Caption: \(draft.body)") }
  99. case .link:
  100. if !draft.linkURL.isEmpty { lines.append("URL: \(draft.linkURL)") }
  101. if !draft.body.isEmpty { lines.append("\n\(draft.body)") }
  102. case .video:
  103. if !draft.videoURL.isEmpty { lines.append("Video URL: \(draft.videoURL)") }
  104. if !draft.body.isEmpty { lines.append("\n\(draft.body)") }
  105. case .poll:
  106. lines.append("Duration: \(draft.pollDuration.label)")
  107. for (index, option) in draft.pollOptions.enumerated() where !option.text.isEmpty {
  108. lines.append("Option \(index + 1): \(option.text)")
  109. }
  110. if !draft.body.isEmpty { lines.append("\n\(draft.body)") }
  111. }
  112. var tags: [String] = []
  113. if draft.isNSFW { tags.append("NSFW") }
  114. if draft.isSpoiler { tags.append("Spoiler") }
  115. if draft.isOC { tags.append("OC") }
  116. if !tags.isEmpty { lines.append("Tags: \(tags.joined(separator: ", "))") }
  117. if !draft.flair.isEmpty { lines.append("Flair: \(draft.flair)") }
  118. return lines.joined(separator: "\n")
  119. }
  120. private func clearMessages() {
  121. errorMessage = nil
  122. successMessage = nil
  123. }
  124. }