version bump
[ShaarliOS.git] / swift4 / Share / ShareVC.swift
blob3825c37b589c45af98d825399c93e399f87e97c7
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.
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.
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/>.
22 import UIKit
23 import Social
24 import MobileCoreServices
25 import AudioToolbox
27 private func stringFromPrivacy(_ priv : Bool) -> String {
28     return priv
29         ? NSLocalizedString("Private πŸ”", comment:"ShareVC")
30         : NSLocalizedString("Public πŸ”“", comment:"ShareVC")
33 private func privacyFromString(_ s : String) -> Bool {
34     return s != stringFromPrivacy(false)
37 private func play_sound_ok() {
38     // https://github.com/irccloud/ios/blob/6e3255eab82be047be141ced6e482ead5ac413f4/ShareExtension/ShareViewController.m#L155
39     AudioServicesPlaySystemSound(1001)
42 private func play_sound_err() {
43     AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
46 @objc (ShareVC) // https://blog.hellocode.co/post/share-extension/
47 class ShareVC: SLComposeServiceViewController {
49     private let semver          =  info_to_semver(Bundle.main.infoDictionary)
51     private var current         : BlogM? // may become local if not for missing cfg error
52     private var wasTouched      = false
53     private var itemTitle       : SLComposeSheetConfigurationItem?
54     private var itemAudience    : SLComposeSheetConfigurationItem?
55     private var session         : URLSession?
56     private var action          : URL = URLEmpty
57     private var ctx             : HtmlFormDict = [:]
58     private var url             : URL = URLEmpty
60     override func viewDidLoad() {
61         debugPrint("viewDidLoad")
62         super.viewDidLoad()
63     }
65     override func configurationItems() -> [Any]! {
66         debugPrint("configurationItems")
67         guard let iTi = SLComposeSheetConfigurationItem() else {return []}
68         iTi.title = NSLocalizedString("Title", comment:"ShareVC")
69         iTi.value = self.contentText
71         guard let iAu = SLComposeSheetConfigurationItem() else {return []}
72         iAu.title = NSLocalizedString("Audience", comment:"ShareVC")
73         iAu.value = stringFromPrivacy(false)
74         weak var wself = self
75         iAu.tapHandler = {
76             guard let sf = wself else {return}
77             guard let iAu = sf.itemAudience else {return}
78             iAu.value = stringFromPrivacy( !privacyFromString(iAu.value) )
79             sf.wasTouched = true
80         }
82         itemTitle = iTi
83         itemAudience = iAu
84         return [iTi, iAu]
85     }
87     override func viewWillAppear(_ animated: Bool) {
88         debugPrint("viewWillAppear")
89         super.viewWillAppear(animated)
90         view.tintColor = UIColor(red:128 / 255.0, green:173 / 255.0, blue:72 / 255.0, alpha:1.0)
91         assert(itemTitle != nil)
92         assert(itemAudience != nil)
94         let sha = ShaarliM.shared
95         // sha.defaults.removePersistentDomain(forName:"group.\(BUNDLE_ID)") // doesn't do it.
96         current = sha.loadBlog(sha.defaults)
97         guard let current = current else {
98             // do nothing here and let viewDidAppear display a error popup
99             return
100         }
101         let cli = ShaarliHtmlClient(semver)
103         textView.keyboardType = .twitter
104         view.subviews.forEach({ (v) in
105             // dark mode?
106             v.backgroundColor = UIColor.white.withAlphaComponent(0.89)
107         })
109         guard let itemTitle = itemTitle else {return}
110         guard let itemAudience = itemAudience else {return}
111         guard let textView = textView else {return}
113         title = current.title
114         itemTitle.value = contentText
115         let preset = tagsNormalise(description:itemTitle.value, extended:current.tagsDefault, tags:[], known:[])
116         textView.text = "\(preset.extended) \(NSLocalizedString("πŸ”„", comment:"ShareVC"))"
117         itemAudience.value = stringFromPrivacy(current.privateDefault)
119         let tPli = kUTTypePropertyList as String
120         let tUrl = kUTTypeURL as String
121         let tTxt = kUTTypeText as String
122         let RK = NSExtensionJavaScriptPreprocessingResultsKey as String
123         weak var ws = self
124         for _item in (extensionContext?.inputItems)! {
125             let item = _item as! NSExtensionItem
126             for ip in (item.attachments!) {
127                 guard let ws = ws else {return}
128                 // let ip = _ip as! NSItemProvider // required for Xcode <10
129                 if( ip.hasItemConformingToTypeIdentifier(tPli)) {
130                     ip.loadItem(forTypeIdentifier:tPli, options:nil) { (_dic, err) in
131                         guard let _dic = (_dic as? NSDictionary)?[RK] as? [String:String] else {
132                             play_sound_err()
133                             ws.showError(
134                                 title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
135                                 message:NSLocalizedString("I got no dictionary to share.", comment: "ShareVC"),
136                                 showsettings:false
137                             )
138                             return
139                         }
140                         let kUrl = "url"
141                         let kTit = "title"
142                         let kDsc = "description"
143                         let kTgs = "keywords"
144                         let kImg = "image"
145                         guard let _url = URL(string:_dic[kUrl] ?? "") else {
146                             play_sound_err()
147                             ws.showError(
148                                 title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
149                                 message:NSLocalizedString("I got no url to share.", comment: "ShareVC"),
150                                 showsettings:false
151                             )
152                             return
153                         }
154                         guard let err = err else {
155                             let t0 = Date()
156                             cli.get(current.endpoint, current.credential, current.timeout, _url, { (ses, act, ctx, _url, tit, dsc, tgs, pri, tim, seti, err) in
157                                 guard "" == err else {
158                                     play_sound_err()
159                                     ws.showError(
160                                         title:NSLocalizedString("Can't reach Shaarli", comment: "ShareVC"),
161                                         message:err,
162                                         showsettings:true
163                                     )
164                                     return
165                                 }
166                                 guard URLEmpty != act else {
167                                     play_sound_err()
168                                     ws.showError(
169                                         title:NSLocalizedString("Can't post to Shaarli", comment: "ShareVC"),
170                                         message:NSLocalizedString("the Shaarli responded an empty linkform action, I don't know where to post to.", comment: "ShareVC"),
171                                         showsettings:true
172                                     )
173                                     return
174                                 }
175                                 self.session = ses
176                                 // should 'old' come out of get( callback(... old) )?
177                                 let old = isOld(t0, seti, cli.timeShaarli(current.timezone, tim))
178                                 let dti = _dic[kTit] ?? ""
179                                 let dde = _dic[kDsc] ?? ""
180                                 // let dim = URL(string:_dic[kImg] ?? "", relativeTo:_url)
181                                 let v = old || ("" != tit || "" != dsc)
182                                     ? (tit, dsc, tgs)
183                                     : (dti == "" && dde == ""
184                                         ? (itemTitle.value ?? "", preset.extended, [])
185                                         : (dti, dde, tagsSplit(_dic[kTgs])) )
186                                 let r = tagsNormalise(description:v.0, extended:v.1, tags:v.2.union(preset.tags), known:[])
187                                 DispatchQueue.main.async {
188                                     ws.action = act
189                                     ws.ctx = ctx
190                                     ws.url = _url
191                                     itemTitle.value = r.description
192                                     textView.text = "\(r.extended) "
193                                     itemAudience.value = stringFromPrivacy(pri)
194                                 }
195                             })
196                             return
197                         }
198                         ws.showError(
199                             title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
200                             message:err.localizedDescription,
201                             showsettings:false
202                         )
203                     }
204                 }
205                 // see predicate from http://stackoverflow.com/a/27932776
206                 else if( ip.hasItemConformingToTypeIdentifier(tUrl) ) {
207                     // maybe this whole block can go away
208                     ip.loadItem(forTypeIdentifier:tUrl, options:nil) { (_url, err) in
209                         guard let _url = _url as? URL else {
210                             play_sound_err()
211                             ws.showError(
212                                 title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
213                                 message:NSLocalizedString("I got no url to share.", comment: "ShareVC"),
214                                 showsettings:false
215                             )
216                             return
217                         }
218                         guard let err = err else {
219                             cli.get(current.endpoint, current.credential, current.timeout, _url, { (ses, act, ctx, _url, tit, dsc, tgs, pri, tim, seti, err) in
220                                 guard "" == err else {
221                                     play_sound_err()
222                                     ws.showError(
223                                         title:NSLocalizedString("Can't reach Shaarli", comment: "ShareVC"),
224                                         message:err,
225                                         showsettings:true
226                                     )
227                                     return
228                                 }
229                                 guard URLEmpty != act else {
230                                     play_sound_err()
231                                     ws.showError(
232                                         title:NSLocalizedString("Can't post to Shaarli", comment: "ShareVC"),
233                                         message:NSLocalizedString("the Shaarli responded an empty linkform action, I don't know where to post to.", comment: "ShareVC"),
234                                         showsettings:true
235                                     )
236                                     return
237                                 }
238                                 self.session = ses
239                                 let v = "" == tit && "" == dsc
240                                     ? (itemTitle.value, preset.extended)
241                                     : (tit, dsc)
242                                 let r = tagsNormalise(description:v.0, extended:v.1, tags:tgs.union(preset.tags), known:[])
243                                 DispatchQueue.main.async {
244                                     ws.action = act
245                                     ws.ctx = ctx
246                                     ws.url = _url
247                                     itemTitle.value = r.description
248                                     textView.text = "\(r.extended) "
249                                     itemAudience.value = stringFromPrivacy(pri)
250                                 }
251                             })
252                             return
253                         }
254                         ws.showError(
255                             title:NSLocalizedString("URL Share Sheet failed", comment: "ShareVC"),
256                             message:err.localizedDescription,
257                             showsettings:false
258                         )
259                     }
260                 }
261                 else if( ip.hasItemConformingToTypeIdentifier(tTxt) ) {
262                     ip.loadItem(forTypeIdentifier:tTxt, options:nil) { (_txt, err) in
263                         guard let err = err else {
264                             debugPrint("done. title:\(itemTitle.value ?? "-") txt:\(String(describing: _txt))")
265                             return
266                         }
267                         ws.showError(
268                             title:NSLocalizedString("TXT Share Sheet failed", comment: "ShareVC"),
269                             message:err.localizedDescription,
270                             showsettings:false
271                         )
272                     }
273                 }
274             }
275         }
276     }
278     override func didSelectPost() {
279         debugPrint("didSelectPost")
280         let c = ShaarliHtmlClient(semver)
281         guard let tit = itemTitle?.value else {return}
282         guard let dsc = textView.text else {return}
283         let pri = privacyFromString((itemAudience?.value)!)
284         let r = tagsNormalise(description:tit, extended:dsc, tags:[], known:[])
285         c.add(session!, action, ctx, url, r.description, r.extended, r.tags, pri) { err in
286             guard "" == err else {
287                 play_sound_err()
288                 self.showError(
289                     title:NSLocalizedString("Share failed", comment: "ShareVC"),
290                     message:err,
291                     showsettings:false
292                 )
293                 usleep(750 * 1000)
294                 return
295             }
297             play_sound_ok()
298             // wait until the sound finished
299             usleep(750 * 1000)
300             super.didSelectPost()
301         }
302         // 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.
303         // self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
304     }
306     override func viewDidAppear(_ animated: Bool) {
307         super.viewDidAppear(animated)
309         guard nil != current else {
310             showError(
311                 title:NSLocalizedString("No Shaarli found", comment:"ShareVC"),
312                 message:NSLocalizedString("Please add one in the Settings.", comment:"ShareVC"),
313                 showsettings:true)
314             return
315         }
317         wasTouched = false
318     }
320     private func showError(title:String, message:String, showsettings:Bool) {
321         DispatchQueue.main.async {
322             let alert = UIAlertController(
323                 title:title,
324                 message:message,
325                 preferredStyle:.alert
326             )
328             alert.addAction(UIAlertAction(
329                 title: NSLocalizedString("Cancel", comment:"ShareVC"),
330                 style:.cancel,
331                 handler:{ (_) in
332                     self.cancel()
333             }))
335             if showsettings {
336                 alert.addAction(UIAlertAction(
337                     title: NSLocalizedString("Settings", comment:"ShareVC"),
338                     style:.default,
339                     handler:{ (_) in
340                         // https://stackoverflow.com/a/44499222/349514
341                         DispatchGroup().notify(queue: DispatchQueue.main) {
342                             let _ = self.openURL(URL(string:"\(SELF_URL_PREFIX):///settings")!)
343                         }
344                         self.cancel()
345                 }))
346             }
348             self.present(alert, animated:true, completion:nil)
349         }
350     }
352     override func presentationAnimationDidFinish() {
353         debugPrint("presentationAnimationDidFinish")
354     }
356     override func isContentValid() -> Bool {
357         debugPrint("isContentValid")
358         // Do validation of contentText and/or NSExtensionContext attachments here
359         wasTouched = true
360         return true
361     }
363     // No preview image right upper inside the share dialog.
364     override func loadPreviewView() -> UIView! {
365         return nil
366     }
368     override func didSelectCancel() {
369         debugPrint("didSelectCancel")
370         super.didSelectCancel()
371     }
373     // https://stackoverflow.com/a/44499222/349514
374     // Function must be named exactly like this so a selector can be found by the compiler!
375     // Anyway - it's another selector in another instance that would be "performed" instead.
376     @objc private func openURL(_ url: URL) -> Bool {
377         var rep: UIResponder? = self
378         while rep != nil {
379             if let app = rep as? UIApplication {
380                 return app.perform(#selector(openURL(_:)), with: url) != nil
381             }
382             rep = rep?.next
383         }
384         return false
385     }