ShareVC.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. //
  2. // ShareVC.swift
  3. // Share
  4. //
  5. // Created by Marcus Rohrmoser on 02.03.20.
  6. // Copyright © 2020-2022 Marcus Rohrmoser mobile Software http://mro.name/me. All rights reserved.
  7. //
  8. // This program is free software: you can redistribute it and/or modify
  9. // it under the terms of the GNU General Public License as published by
  10. // the Free Software Foundation, either version 3 of the License, or
  11. // (at your option) any later version.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU General Public License
  19. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. //
  21. import UIKit
  22. import Social
  23. import MobileCoreServices
  24. import AudioToolbox
  25. private func stringFromPrivacy(_ priv : Bool) -> String {
  26. return priv
  27. ? NSLocalizedString("Private 🔐", comment:"ShareVC")
  28. : NSLocalizedString("Public 🔓", comment:"ShareVC")
  29. }
  30. private func privacyFromString(_ s : String) -> Bool {
  31. return s != stringFromPrivacy(false)
  32. }
  33. private func play_sound_ok() {
  34. // https://github.com/irccloud/ios/blob/6e3255eab82be047be141ced6e482ead5ac413f4/ShareExtension/ShareViewController.m#L155
  35. AudioServicesPlaySystemSound(1001)
  36. }
  37. private func play_sound_err() {
  38. AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
  39. }
  40. @objc (ShareVC) // https://blog.hellocode.co/post/share-extension/
  41. class ShareVC: SLComposeServiceViewController {
  42. private let semver = info_to_semver(Bundle.main.infoDictionary)
  43. private var current : BlogM? // may become local if not for missing cfg error
  44. private var wasTouched = false
  45. private var itemTitle : SLComposeSheetConfigurationItem?
  46. private var itemAudience : SLComposeSheetConfigurationItem?
  47. private var session : URLSession?
  48. private var action : URL = URLEmpty
  49. private var ctx : HtmlFormDict = [:]
  50. private var url : URL = URLEmpty
  51. override func viewDidLoad() {
  52. debugPrint("viewDidLoad")
  53. super.viewDidLoad()
  54. }
  55. override func configurationItems() -> [Any]! {
  56. debugPrint("configurationItems")
  57. guard let iTi = SLComposeSheetConfigurationItem() else {return []}
  58. iTi.title = NSLocalizedString("Title", comment:"ShareVC")
  59. iTi.value = self.contentText
  60. guard let iAu = SLComposeSheetConfigurationItem() else {return []}
  61. iAu.title = NSLocalizedString("Audience", comment:"ShareVC")
  62. iAu.value = stringFromPrivacy(false)
  63. weak var wself = self
  64. iAu.tapHandler = {
  65. guard let sf = wself else {return}
  66. guard let iAu = sf.itemAudience else {return}
  67. iAu.value = stringFromPrivacy( !privacyFromString(iAu.value) )
  68. sf.wasTouched = true
  69. }
  70. itemTitle = iTi
  71. itemAudience = iAu
  72. return [iTi, iAu]
  73. }
  74. override func viewWillAppear(_ animated: Bool) {
  75. debugPrint("viewWillAppear")
  76. super.viewWillAppear(animated)
  77. view.tintColor = UIColor(red:128 / 255.0, green:173 / 255.0, blue:72 / 255.0, alpha:1.0)
  78. assert(itemTitle != nil)
  79. assert(itemAudience != nil)
  80. let sha = ShaarliM.shared
  81. // sha.defaults.removePersistentDomain(forName:"group.\(BUNDLE_ID)") // doesn't do it.
  82. current = sha.loadBlog(sha.defaults)
  83. guard let current = current else {
  84. // do nothing here and let viewDidAppear display a error popup
  85. return
  86. }
  87. let cli = ShaarliHtmlClient(semver)
  88. textView.keyboardType = .twitter
  89. view.subviews.forEach({ (v) in
  90. // dark mode?
  91. v.backgroundColor = UIColor.white.withAlphaComponent(0.89)
  92. })
  93. guard let itemTitle = itemTitle else {return}
  94. guard let itemAudience = itemAudience else {return}
  95. guard let textView = textView else {return}
  96. title = current.title
  97. itemTitle.value = contentText
  98. let preset = tagsNormalise(description:itemTitle.value, extended:current.tagsDefault, tags:[], known:[])
  99. textView.text = "\(preset.extended) \(NSLocalizedString("🔄", comment:"ShareVC"))"
  100. itemAudience.value = stringFromPrivacy(current.privateDefault)
  101. let tPli = kUTTypePropertyList as String
  102. let tUrl = kUTTypeURL as String
  103. let tTxt = kUTTypeText as String
  104. let RK = NSExtensionJavaScriptPreprocessingResultsKey as String
  105. weak var ws = self
  106. for _item in (extensionContext?.inputItems)! {
  107. let item = _item as! NSExtensionItem
  108. for ip in (item.attachments!) {
  109. guard let ws = ws else {return}
  110. // let ip = _ip as! NSItemProvider // required for Xcode <10
  111. if( ip.hasItemConformingToTypeIdentifier(tPli)) {
  112. ip.loadItem(forTypeIdentifier:tPli, options:nil) { (_dic, err) in
  113. guard let _dic = (_dic as? NSDictionary)?[RK] as? [String:String] else {
  114. play_sound_err()
  115. ws.showError(
  116. title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
  117. message:NSLocalizedString("I got no dictionary to share.", comment: "ShareVC"),
  118. showsettings:false
  119. )
  120. return
  121. }
  122. let kUrl = "url"
  123. let kTit = "title"
  124. let kDsc = "description"
  125. let kTgs = "keywords"
  126. let kImg = "image"
  127. guard let _url = URL(string:_dic[kUrl] ?? "") else {
  128. play_sound_err()
  129. ws.showError(
  130. title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
  131. message:NSLocalizedString("I got no url to share.", comment: "ShareVC"),
  132. showsettings:false
  133. )
  134. return
  135. }
  136. guard let err = err else {
  137. let t0 = Date()
  138. cli.get(current.endpoint, current.credential, current.timeout, _url, { (ses, act, ctx, _url, tit, dsc, tgs, pri, tim, seti, err) in
  139. guard "" == err else {
  140. play_sound_err()
  141. ws.showError(
  142. title:NSLocalizedString("Can't reach Shaarli", comment: "ShareVC"),
  143. message:err,
  144. showsettings:true
  145. )
  146. return
  147. }
  148. guard URLEmpty != act else {
  149. play_sound_err()
  150. ws.showError(
  151. title:NSLocalizedString("Can't post to Shaarli", comment: "ShareVC"),
  152. message:NSLocalizedString("the Shaarli responded an empty linkform action, I don't know where to post to.", comment: "ShareVC"),
  153. showsettings:true
  154. )
  155. return
  156. }
  157. self.session = ses
  158. // should 'old' come out of get( callback(... old) )?
  159. let old = isOld(t0, seti, cli.timeShaarli(current.timezone, tim))
  160. let dti = _dic[kTit] ?? ""
  161. let dde = _dic[kDsc] ?? ""
  162. // let dim = URL(string:_dic[kImg] ?? "", relativeTo:_url)
  163. let v = old || ("" != tit || "" != dsc)
  164. ? (tit, dsc, tgs)
  165. : (dti == "" && dde == ""
  166. ? (itemTitle.value ?? "", preset.extended, [])
  167. : (dti, dde, tagsSplit(_dic[kTgs])) )
  168. let r = tagsNormalise(description:v.0, extended:v.1, tags:v.2.union(preset.tags), known:[])
  169. DispatchQueue.main.async {
  170. ws.action = act
  171. ws.ctx = ctx
  172. ws.url = _url
  173. itemTitle.value = r.description
  174. textView.text = "\(r.extended) "
  175. itemAudience.value = stringFromPrivacy(pri)
  176. }
  177. })
  178. return
  179. }
  180. ws.showError(
  181. title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
  182. message:err.localizedDescription,
  183. showsettings:false
  184. )
  185. }
  186. }
  187. // see predicate from http://stackoverflow.com/a/27932776
  188. else if( ip.hasItemConformingToTypeIdentifier(tUrl) ) {
  189. // maybe this whole block can go away
  190. ip.loadItem(forTypeIdentifier:tUrl, options:nil) { (_url, err) in
  191. guard let _url = _url as? URL else {
  192. play_sound_err()
  193. ws.showError(
  194. title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
  195. message:NSLocalizedString("I got no url to share.", comment: "ShareVC"),
  196. showsettings:false
  197. )
  198. return
  199. }
  200. guard let err = err else {
  201. cli.get(current.endpoint, current.credential, current.timeout, _url, { (ses, act, ctx, _url, tit, dsc, tgs, pri, tim, seti, err) in
  202. guard "" == err else {
  203. play_sound_err()
  204. ws.showError(
  205. title:NSLocalizedString("Can't reach Shaarli", comment: "ShareVC"),
  206. message:err,
  207. showsettings:true
  208. )
  209. return
  210. }
  211. guard URLEmpty != act else {
  212. play_sound_err()
  213. ws.showError(
  214. title:NSLocalizedString("Can't post to Shaarli", comment: "ShareVC"),
  215. message:NSLocalizedString("the Shaarli responded an empty linkform action, I don't know where to post to.", comment: "ShareVC"),
  216. showsettings:true
  217. )
  218. return
  219. }
  220. self.session = ses
  221. let v = "" == tit && "" == dsc
  222. ? (itemTitle.value, preset.extended)
  223. : (tit, dsc)
  224. let r = tagsNormalise(description:v.0, extended:v.1, tags:tgs.union(preset.tags), known:[])
  225. DispatchQueue.main.async {
  226. ws.action = act
  227. ws.ctx = ctx
  228. ws.url = _url
  229. itemTitle.value = r.description
  230. textView.text = "\(r.extended) "
  231. itemAudience.value = stringFromPrivacy(pri)
  232. }
  233. })
  234. return
  235. }
  236. ws.showError(
  237. title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
  238. message:err.localizedDescription,
  239. showsettings:false
  240. )
  241. }
  242. }
  243. else if( ip.hasItemConformingToTypeIdentifier(tTxt) ) {
  244. ip.loadItem(forTypeIdentifier:tTxt, options:nil) { (_txt, err) in
  245. guard let err = err else {
  246. debugPrint("done. title:\(itemTitle.value ?? "-") txt:\(String(describing: _txt))")
  247. return
  248. }
  249. ws.showError(
  250. title:NSLocalizedString("TXT Share Sheet failed", comment: "ShareVC"),
  251. message:err.localizedDescription,
  252. showsettings:false
  253. )
  254. }
  255. }
  256. }
  257. }
  258. }
  259. override func didSelectPost() {
  260. debugPrint("didSelectPost")
  261. let c = ShaarliHtmlClient(semver)
  262. guard let tit = itemTitle?.value else {return}
  263. guard let dsc = textView.text else {return}
  264. let pri = privacyFromString((itemAudience?.value)!)
  265. let r = tagsNormalise(description:tit, extended:dsc, tags:[], known:[])
  266. c.add(session!, action, ctx, url, r.description, r.extended, r.tags, pri) { err in
  267. guard "" == err else {
  268. play_sound_err()
  269. self.showError(
  270. title:NSLocalizedString("Share failed", comment: "ShareVC"),
  271. message:err,
  272. showsettings:false
  273. )
  274. usleep(750 * 1000)
  275. return
  276. }
  277. play_sound_ok()
  278. // wait until the sound finished
  279. usleep(750 * 1000)
  280. super.didSelectPost()
  281. }
  282. // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
  283. // self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
  284. }
  285. override func viewDidAppear(_ animated: Bool) {
  286. super.viewDidAppear(animated)
  287. guard nil != current else {
  288. showError(
  289. title:NSLocalizedString("No Shaarli found", comment:"ShareVC"),
  290. message:NSLocalizedString("Please add one in the Settings.", comment:"ShareVC"),
  291. showsettings:true)
  292. return
  293. }
  294. wasTouched = false
  295. }
  296. private func showError(title:String, message:String, showsettings:Bool) {
  297. DispatchQueue.main.async {
  298. let alert = UIAlertController(
  299. title:title,
  300. message:message,
  301. preferredStyle:.alert
  302. )
  303. alert.addAction(UIAlertAction(
  304. title: NSLocalizedString("Cancel", comment:"ShareVC"),
  305. style:.cancel,
  306. handler:{ (_) in
  307. self.cancel()
  308. }))
  309. if showsettings {
  310. alert.addAction(UIAlertAction(
  311. title: NSLocalizedString("Settings", comment:"ShareVC"),
  312. style:.default,
  313. handler:{ (_) in
  314. // https://stackoverflow.com/a/44499222/349514
  315. DispatchGroup().notify(queue: DispatchQueue.main) {
  316. let _ = self.openURL(URL(string:"\(SELF_URL_PREFIX):///settings")!)
  317. }
  318. self.cancel()
  319. }))
  320. }
  321. self.present(alert, animated:true, completion:nil)
  322. }
  323. }
  324. override func presentationAnimationDidFinish() {
  325. debugPrint("presentationAnimationDidFinish")
  326. }
  327. override func isContentValid() -> Bool {
  328. debugPrint("isContentValid")
  329. // Do validation of contentText and/or NSExtensionContext attachments here
  330. wasTouched = true
  331. return true
  332. }
  333. // No preview image right upper inside the share dialog.
  334. override func loadPreviewView() -> UIView! {
  335. return nil
  336. }
  337. override func didSelectCancel() {
  338. debugPrint("didSelectCancel")
  339. super.didSelectCancel()
  340. }
  341. // https://stackoverflow.com/a/44499222/349514
  342. // Function must be named exactly like this so a selector can be found by the compiler!
  343. // Anyway - it's another selector in another instance that would be "performed" instead.
  344. @objc private func openURL(_ url: URL) -> Bool {
  345. var rep: UIResponder? = self
  346. while rep != nil {
  347. if let app = rep as? UIApplication {
  348. return app.perform(#selector(openURL(_:)), with: url) != nil
  349. }
  350. rep = rep?.next
  351. }
  352. return false
  353. }
  354. }