Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

276 linhas
11 KiB

  1. import SwiftUI
  2. struct PostEditorView: View {
  3. @EnvironmentObject var model: WriteFreelyModel
  4. @EnvironmentObject var errorHandling: ErrorHandling
  5. @Environment(\.horizontalSizeClass) var horizontalSizeClass
  6. @Environment(\.managedObjectContext) var moc
  7. @Environment(\.presentationMode) var presentationMode
  8. @ObservedObject var post: WFAPost
  9. @State private var updatingTitleFromServer: Bool = false
  10. @State private var updatingBodyFromServer: Bool = false
  11. @State private var selectedCollection: WFACollection?
  12. @FetchRequest(
  13. entity: WFACollection.entity(),
  14. sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
  15. ) var collections: FetchedResults<WFACollection>
  16. var body: some View {
  17. VStack {
  18. if post.hasNewerRemoteCopy {
  19. RemoteChangePromptView(
  20. remoteChangeType: .remoteCopyUpdated,
  21. buttonHandler: { model.updateFromServer(post: post) }
  22. )
  23. } else if post.wasDeletedFromServer {
  24. RemoteChangePromptView(
  25. remoteChangeType: .remoteCopyDeleted,
  26. buttonHandler: {
  27. self.presentationMode.wrappedValue.dismiss()
  28. DispatchQueue.main.async {
  29. model.posts.remove(post)
  30. }
  31. }
  32. )
  33. }
  34. PostTextEditingView(
  35. post: _post,
  36. updatingTitleFromServer: $updatingTitleFromServer,
  37. updatingBodyFromServer: $updatingBodyFromServer
  38. )
  39. .withErrorHandling()
  40. }
  41. .navigationBarTitleDisplayMode(.inline)
  42. .padding()
  43. .toolbar {
  44. ToolbarItem(placement: .principal) {
  45. PostEditorStatusToolbarView(post: post)
  46. }
  47. ToolbarItem(placement: .primaryAction) {
  48. if model.isProcessingRequest {
  49. ProgressView()
  50. } else {
  51. Menu(content: {
  52. if post.status == PostStatus.local.rawValue {
  53. Menu(content: {
  54. Label("Publish to…", systemImage: "paperplane")
  55. Button(action: {
  56. if model.account.isLoggedIn {
  57. post.collectionAlias = nil
  58. publishPost()
  59. } else {
  60. self.model.isPresentingSettingsView = true
  61. }
  62. }, label: {
  63. Text(" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")")
  64. })
  65. ForEach(collections) { collection in
  66. Button(action: {
  67. if model.account.isLoggedIn {
  68. post.collectionAlias = collection.alias
  69. publishPost()
  70. } else {
  71. self.model.isPresentingSettingsView = true
  72. }
  73. }, label: {
  74. Text(" \(collection.title)")
  75. })
  76. }
  77. }, label: {
  78. Label("Publish…", systemImage: "paperplane")
  79. })
  80. .accessibilityHint(Text("Choose the blog you want to publish this post to"))
  81. .disabled(post.body.count == 0)
  82. } else {
  83. Button(action: {
  84. if model.account.isLoggedIn {
  85. publishPost()
  86. } else {
  87. self.model.isPresentingSettingsView = true
  88. }
  89. }, label: {
  90. Label("Publish", systemImage: "paperplane")
  91. })
  92. .disabled(post.status == PostStatus.published.rawValue || post.body.count == 0)
  93. }
  94. Button(action: {
  95. sharePost()
  96. }, label: {
  97. Label("Share", systemImage: "square.and.arrow.up")
  98. })
  99. .accessibilityHint(Text("Open the system share sheet to share a link to this post"))
  100. .disabled(post.postId == nil)
  101. if model.account.isLoggedIn && post.status != PostStatus.local.rawValue {
  102. Section(header: Text("Move To Collection")) {
  103. Label("Move to:", systemImage: "arrowshape.zigzag.right")
  104. Picker(selection: $selectedCollection, label: Text("Move to…")) {
  105. Text(
  106. " \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")"
  107. ).tag(nil as WFACollection?)
  108. ForEach(collections) { collection in
  109. Text(" \(collection.title)").tag(collection as WFACollection?)
  110. }
  111. }
  112. }
  113. }
  114. }, label: {
  115. ZStack {
  116. Image("does.not.exist")
  117. .accessibilityHidden(true)
  118. Image(systemName: "ellipsis.circle")
  119. .imageScale(.large)
  120. .accessibilityHidden(true)
  121. }
  122. })
  123. .accessibilityLabel(Text("Menu"))
  124. .accessibilityHint(Text("Opens a context menu to publish, share, or move the post"))
  125. .onTapGesture {
  126. hideKeyboard()
  127. }
  128. .disabled(post.body.count == 0)
  129. }
  130. }
  131. }
  132. .onChange(of: post.hasNewerRemoteCopy, perform: { _ in
  133. if !post.hasNewerRemoteCopy {
  134. updatingTitleFromServer = true
  135. updatingBodyFromServer = true
  136. }
  137. })
  138. .onChange(of: selectedCollection, perform: { [selectedCollection] newCollection in
  139. if post.collectionAlias == newCollection?.alias {
  140. return
  141. } else {
  142. post.collectionAlias = newCollection?.alias
  143. model.move(post: post, from: selectedCollection, to: newCollection)
  144. }
  145. })
  146. .onChange(of: post.status, perform: { value in
  147. if value != PostStatus.published.rawValue {
  148. self.model.editor.saveLastDraft(post)
  149. } else {
  150. self.model.editor.clearLastDraft()
  151. }
  152. DispatchQueue.main.async {
  153. LocalStorageManager.standard.saveContext()
  154. }
  155. })
  156. .onAppear(perform: {
  157. self.selectedCollection = collections.first { $0.alias == post.collectionAlias }
  158. if post.status != PostStatus.published.rawValue {
  159. DispatchQueue.main.async {
  160. self.model.editor.saveLastDraft(post)
  161. }
  162. } else {
  163. self.model.editor.clearLastDraft()
  164. }
  165. })
  166. .onChange(of: model.hasError) { value in
  167. if value {
  168. if let error = model.currentError {
  169. self.errorHandling.handle(error: error)
  170. } else {
  171. self.errorHandling.handle(error: AppError.genericError())
  172. }
  173. model.hasError = false
  174. }
  175. }
  176. .onDisappear(perform: {
  177. self.model.editor.clearLastDraft()
  178. if post.title.count == 0
  179. && post.body.count == 0
  180. && post.status == PostStatus.local.rawValue
  181. && post.updatedDate == nil
  182. && post.postId == nil {
  183. DispatchQueue.main.async {
  184. model.posts.remove(post)
  185. }
  186. } else if post.status != PostStatus.published.rawValue {
  187. DispatchQueue.main.async {
  188. LocalStorageManager.standard.saveContext()
  189. }
  190. }
  191. })
  192. }
  193. private func publishPost() {
  194. DispatchQueue.main.async {
  195. LocalStorageManager.standard.saveContext()
  196. model.publish(post: post)
  197. }
  198. #if os(iOS)
  199. self.hideKeyboard()
  200. #endif
  201. }
  202. private func sharePost() {
  203. // If the post doesn't have a post ID, it isn't published, and therefore can't be shared, so return early.
  204. guard let postId = post.postId else { return }
  205. var urlString: String
  206. if let postSlug = post.slug,
  207. let postCollectionAlias = post.collectionAlias {
  208. // This post is in a collection, so share the URL as baseURL/postSlug.
  209. let urls = collections.filter { $0.alias == postCollectionAlias }
  210. let baseURL = urls.first?.url ?? "\(model.account.server)/\(postCollectionAlias)/"
  211. urlString = "\(baseURL)\(postSlug)"
  212. } else {
  213. // This is a draft post, so share the URL as server/postID
  214. urlString = "\(model.account.server)/\(postId)"
  215. }
  216. guard let data = URL(string: urlString) else { return }
  217. let activityView = UIActivityViewController(activityItems: [data], applicationActivities: nil)
  218. UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil)
  219. if UIDevice.current.userInterfaceIdiom == .pad {
  220. activityView.popoverPresentationController?.permittedArrowDirections = .up
  221. activityView.popoverPresentationController?.sourceView = UIApplication.shared.windows.first
  222. activityView.popoverPresentationController?.sourceRect = CGRect(
  223. x: UIScreen.main.bounds.width,
  224. y: -125,
  225. width: 200,
  226. height: 200
  227. )
  228. }
  229. }
  230. }
  231. struct PostEditorView_EmptyPostPreviews: PreviewProvider {
  232. static var previews: some View {
  233. let context = LocalStorageManager.standard.container.viewContext
  234. let testPost = WFAPost(context: context)
  235. testPost.createdDate = Date()
  236. testPost.appearance = "norm"
  237. let model = WriteFreelyModel()
  238. return PostEditorView(post: testPost)
  239. .environment(\.managedObjectContext, context)
  240. .environmentObject(model)
  241. }
  242. }
  243. struct PostEditorView_ExistingPostPreviews: PreviewProvider {
  244. static var previews: some View {
  245. let context = LocalStorageManager.standard.container.viewContext
  246. let testPost = WFAPost(context: context)
  247. testPost.title = "Test Post Title"
  248. testPost.body = "Here's some cool sample body text."
  249. testPost.createdDate = Date()
  250. testPost.appearance = "code"
  251. testPost.hasNewerRemoteCopy = true
  252. let model = WriteFreelyModel()
  253. return PostEditorView(post: testPost)
  254. .environment(\.managedObjectContext, context)
  255. .environmentObject(model)
  256. }
  257. }