Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

199 lines
7.7 KiB

  1. import SwiftUI
  2. import MobileCoreServices
  3. import UniformTypeIdentifiers
  4. import WriteFreely
  5. enum WFActionExtensionError: Error {
  6. case userCancelledRequest
  7. case couldNotParseInputItems
  8. }
  9. struct ContentView: View {
  10. @Environment(\.extensionContext) private var extensionContext: NSExtensionContext!
  11. @Environment(\.managedObjectContext) private var managedObjectContext
  12. @AppStorage(WFDefaults.defaultFontIntegerKey, store: UserDefaults.shared) var fontIndex: Int = 0
  13. @FetchRequest(
  14. entity: WFACollection.entity(),
  15. sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
  16. ) var collections: FetchedResults<WFACollection>
  17. @State private var draftTitle: String = ""
  18. @State private var draftText: String = ""
  19. @State private var isShowingAlert: Bool = false
  20. @State private var selectedBlog: WFACollection?
  21. private var draftsCollectionName: String {
  22. guard UserDefaults.shared.string(forKey: WFDefaults.serverStringKey) == "https://write.as" else {
  23. return "Drafts"
  24. }
  25. return "Anonymous"
  26. }
  27. private var controls: some View {
  28. HStack {
  29. Group {
  30. Button(
  31. action: { extensionContext.cancelRequest(withError: WFActionExtensionError.userCancelledRequest) },
  32. label: { Image(systemName: "xmark.circle").imageScale(.large) }
  33. )
  34. .accessibilityLabel(Text("Cancel"))
  35. Spacer()
  36. Button(
  37. action: {
  38. savePostToCollection(collection: selectedBlog, title: draftTitle, body: draftText)
  39. extensionContext.completeRequest(returningItems: nil, completionHandler: nil)
  40. },
  41. label: { Image(systemName: "square.and.arrow.down").imageScale(.large) }
  42. )
  43. .accessibilityLabel(Text("Create new draft"))
  44. }
  45. .padding()
  46. }
  47. }
  48. var body: some View {
  49. VStack {
  50. controls
  51. Form {
  52. Section(header: Text("Title")) {
  53. switch fontIndex {
  54. case 1:
  55. TextField("Draft Title", text: $draftTitle).font(.custom("OpenSans-Regular", size: 26))
  56. case 2:
  57. TextField("Draft Title", text: $draftTitle).font(.custom("Hack-Regular", size: 26))
  58. default:
  59. TextField("Draft Title", text: $draftTitle).font(.custom("Lora", size: 26))
  60. }
  61. }
  62. Section(header: Text("Content")) {
  63. switch fontIndex {
  64. case 1:
  65. TextEditor(text: $draftText).font(.custom("OpenSans-Regular", size: 17))
  66. case 2:
  67. TextEditor(text: $draftText).font(.custom("Hack-Regular", size: 17))
  68. default:
  69. TextEditor(text: $draftText).font(.custom("Lora", size: 17))
  70. }
  71. }
  72. Section(header: Text("Save To")) {
  73. Button(action: {
  74. self.selectedBlog = nil
  75. }, label: {
  76. HStack {
  77. Text(draftsCollectionName)
  78. .foregroundColor(selectedBlog == nil ? .primary : .secondary)
  79. Spacer()
  80. if selectedBlog == nil {
  81. Image(systemName: "checkmark")
  82. }
  83. }
  84. })
  85. ForEach(collections, id: \.self) { collection in
  86. Button(action: {
  87. self.selectedBlog = collection
  88. }, label: {
  89. HStack {
  90. Text(collection.title)
  91. .foregroundColor(selectedBlog == collection ? .primary : .secondary)
  92. Spacer()
  93. if selectedBlog == collection {
  94. Image(systemName: "checkmark")
  95. }
  96. }
  97. })
  98. }
  99. }
  100. }
  101. .padding(.bottom, 24)
  102. }
  103. .alert(isPresented: $isShowingAlert, content: {
  104. Alert(
  105. title: Text("Something Went Wrong"),
  106. message: Text("WriteFreely can't create a draft with the data received."),
  107. dismissButton: .default(Text("OK"), action: {
  108. extensionContext.cancelRequest(withError: WFActionExtensionError.couldNotParseInputItems)
  109. }))
  110. })
  111. .onAppear {
  112. do {
  113. try getPageDataFromExtensionContext()
  114. } catch {
  115. self.isShowingAlert = true
  116. }
  117. }
  118. }
  119. private func savePostToCollection(collection: WFACollection?, title: String, body: String) {
  120. let post = WFAPost(context: managedObjectContext)
  121. post.createdDate = Date()
  122. post.title = title
  123. post.body = body
  124. post.status = PostStatus.local.rawValue
  125. post.collectionAlias = collection?.alias
  126. switch fontIndex {
  127. case 1:
  128. post.appearance = "sans"
  129. case 2:
  130. post.appearance = "wrap"
  131. default:
  132. post.appearance = "serif"
  133. }
  134. if let languageCode = Locale.current.languageCode {
  135. post.language = languageCode
  136. post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
  137. }
  138. LocalStorageManager.standard.saveContext()
  139. }
  140. private func getPageDataFromExtensionContext() throws {
  141. if let inputItem = extensionContext.inputItems.first as? NSExtensionItem {
  142. if let itemProvider = inputItem.attachments?.first {
  143. let typeIdentifier: String
  144. if #available(iOS 15, *) {
  145. typeIdentifier = UTType.propertyList.identifier
  146. } else {
  147. typeIdentifier = kUTTypePropertyList as String
  148. }
  149. itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { (dict, error) in
  150. if error != nil {
  151. self.isShowingAlert = true
  152. }
  153. guard let itemDict = dict as? NSDictionary else {
  154. return
  155. }
  156. guard let jsValues = itemDict[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary else {
  157. return
  158. }
  159. let pageTitle = jsValues["title"] as? String ?? ""
  160. let pageURL = jsValues["URL"] as? String ?? ""
  161. let pageSelectedText = jsValues["selection"] as? String ?? ""
  162. if pageSelectedText.isEmpty {
  163. // If there's no selected text, create a Markdown link to the webpage.
  164. self.draftText = "[\(pageTitle)](\(pageURL))"
  165. } else {
  166. // If there is selected text, create a Markdown blockquote with the selection
  167. // and add a Markdown link to the webpage.
  168. self.draftText = """
  169. > \(pageSelectedText)
  170. Via: [\(pageTitle)](\(pageURL))
  171. """
  172. }
  173. }
  174. } else {
  175. throw WFActionExtensionError.couldNotParseInputItems
  176. }
  177. } else {
  178. throw WFActionExtensionError.couldNotParseInputItems
  179. }
  180. }
  181. }