Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

269 rindas
11 KiB

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