version bump
[ShaarliOS.git] / swift4 / ShaarliOS / SettingsVC.swift
blob8f531a0b838e3d362e3e535fe20285d91cb95740
1 //
2 //  SettingsVC.swift
3 //  ShaarliOS
4 //
5 //  Created by Marcus Rohrmoser on 21.02.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 WebKit
25 internal func endpoints(_ base : String?, _ uid : String?, _ pwd : String?) -> ArraySlice<URL> {
26     var urls = ArraySlice<URL>()
27     guard let base = base?.trimmingCharacters(in:.whitespacesAndNewlines)
28         else { return urls }
29     let base_ = base.hasPrefix(HTTP_HTTPS + "://")
30     ? String(base.dropFirst(HTTP_HTTPS.count+"://".count))
31     : base.hasPrefix(HTTP_HTTP + "://")
32     ? String(base.dropFirst(HTTP_HTTP.count+"://".count))
33     : base.hasPrefix("//")
34     ? String(base.dropFirst("//".count))
35     : base
37     guard var ep = URLComponents(string:"//\(base_)")
38         else { return urls }
39     ep.user = uid
40     ep.password = pwd
42     ep.scheme = HTTP_HTTPS; urls.append(ep.url!)
43     ep.scheme = HTTP_HTTP;  urls.append(ep.url!)
45     let php = "/index.php"
46     let pa = ep.path.dropLast(ep.path.hasSuffix(php)
47         ? php.count
48         : ep.path.hasSuffix("/")
49         ? 1
50         : 0)
52     ep.path = pa + php
53     ep.scheme = HTTP_HTTPS; urls.append(ep.url!)
54     ep.scheme = HTTP_HTTP;  urls.append(ep.url!)
56     return urls
60 class SettingsVC: UITableViewController, UITextFieldDelegate, WKNavigationDelegate {
61     @IBOutlet private var txtEndpoint       : UITextField!
62     @IBOutlet private var sldTimeout        : UISlider!
63     @IBOutlet private var txtTimeout        : UITextField!
64     @IBOutlet private var swiSecure         : UISwitch!
65     @IBOutlet private var txtUserName       : UITextField!
66     @IBOutlet private var txtPassWord       : UITextField!
67     @IBOutlet private var txtBasicUid       : UITextField!
68     @IBOutlet private var txtBasicPwd       : UITextField!
69     @IBOutlet private var lblTitle          : UILabel!
70     @IBOutlet private var txtTags           : UITextField!
71     @IBOutlet private var spiLogin          : UIActivityIndicatorView!
72     @IBOutlet private var cellAbout         : UITableViewCell!
74     private let wwwAbout                    = WKWebView()
75     var current                             : BlogM?
77     // MARK: - Lifecycle
79     // https://www.objc.io/blog/2018/04/24/bindings-with-kvo-and-keypaths/
80     override func viewDidLoad() {
81         super.viewDidLoad()
83         title = NSLocalizedString("Settings", comment:"SettingsVC")
84         navigationItem.leftBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, target:self, action:#selector(SettingsVC.actionCancel(_:)))
85         navigationItem.rightBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target:self, action:#selector(SettingsVC.actionSignIn(_:)))
87         view.addSubview(spiLogin)
88         spiLogin.backgroundColor = .clear
89         txtTimeout.placeholder = NSLocalizedString("sec", comment:"SettingsVC")
90         sldTimeout.minimumValue = Float(timeoutMinimumValue)
91         sldTimeout.maximumValue = Float(timeoutMaximumValue)
92         sldTimeout.value = Float(timeoutDefaultValue)
94         [txtEndpoint, txtBasicUid, txtBasicPwd, txtTimeout, txtUserName, txtPassWord, txtTags].forEach {
95             $0.setValue($0.textColor?.withAlphaComponent(0.4), forKeyPath:"placeholderLabel.textColor")
96         }
98         guard let url = Bundle(for:type(of:self)).url(forResource:"about", withExtension:"html") else { return }
99         cellAbout.contentView.addSubview(wwwAbout)
100         wwwAbout.navigationDelegate = self
101         wwwAbout.frame = cellAbout!.contentView.bounds.insetBy(dx: 8, dy: 8)
102         wwwAbout.autoresizingMask = [.flexibleWidth, .flexibleHeight]
103         wwwAbout.contentScaleFactor = 1.0
104         wwwAbout.scrollView.isScrollEnabled = false
105         wwwAbout.scrollView.bounces = false
106         wwwAbout.isOpaque = false // avoid white flash https://stackoverflow.com/a/15670274
107         wwwAbout.backgroundColor = .black
108         wwwAbout.customUserAgent = SHAARLI_COMPANION_APP_URL
109         wwwAbout.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
110     }
112     override func viewWillAppear(_ animated: Bool) {
113         super.viewWillAppear(animated)
114         spiLogin.stopAnimating()
115         navigationItem.rightBarButtonItem?.isEnabled = true
116         txtEndpoint.becomeFirstResponder()
117         togui(current)
118     }
120     override func viewDidAppear(_ animated: Bool) {
121         super.viewDidAppear(animated)
123         spiLogin.translatesAutoresizingMaskIntoConstraints = false
124         let horizontalConstraint = spiLogin.centerXAnchor.constraint(equalTo: lblTitle.centerXAnchor)
125         let verticalConstraint = spiLogin.centerYAnchor.constraint(equalTo: lblTitle.centerYAnchor)
126         NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint])
128         guard let url = UIPasteboard.general.url else {
129             return
130         }
131         let alert = UIAlertController(
132             title:NSLocalizedString("Use URL form Clipboard", comment: "SettingsVC"),
133             message:String(format:NSLocalizedString("do you want to use the URL\n'%@'\nas shaarli endpoint?", comment:"SettingsVC"), url.description),
134             preferredStyle:.alert
135         )
136         alert.addAction(UIAlertAction(
137             title:NSLocalizedString("Cancel", comment:"SettingsVC"),
138             style:.cancel,
139             handler:nil))
140         alert.addAction(UIAlertAction(
141             title: NSLocalizedString("Yes", comment:"SettingsVC"),
142             style:.default,
143             handler:{(_) in self.togui(url)}))
144         present(alert, animated:true, completion:nil)
145     }
147     private func togui(_ ur : URL?) {
148         txtEndpoint.text        = nil
149         txtUserName.text        = nil
150         txtPassWord.text        = nil
151         swiSecure.isOn          = false
152         guard let ur = ur else { return }
153         guard var uc = URLComponents(url:ur, resolvingAgainstBaseURL:true) else { return }
154         txtUserName.text        = uc.user
155         txtPassWord.text        = uc.password
156         swiSecure.isOn          = HTTP_HTTPS == uc.scheme
157         uc.password             = nil
158         uc.user                 = nil
159         uc.scheme               = nil
160         guard let s = uc.url?.absoluteString else { return }
161         let su = s.suffix(from:.init(utf16Offset:2, in:s))
162         txtEndpoint.text        = String(su)
163     }
165     private func togui(_ b : BlogM?) {
166         guard let b = b else {
167             lblTitle.text = NSLocalizedString("No Shaarli yet.", comment:"SettingsVC")
168             lblTitle.textColor = .red
169             return
170         }
172         lblTitle.textColor      = ShaarliM.labelColor
173         lblTitle.text           = b.title;
174         txtBasicUid.text        = nil
175         txtBasicPwd.text        = nil
176         if let cred = b.credential {
177             if cred.hasPassword {
178                 txtBasicUid.text  = cred.user
179                 txtBasicPwd.text  = cred.password
180             }
181         }
182         togui(b.endpoint)
183         sldTimeout.value        = Float(b.timeout)
184         sldTimeoutChanged(self)
185         txtTags.text            = b.tagsDefault
186     }
188     // MARK: - Navigation
190     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
191         guard let vc = segue.destination as? MainVC else {return}
192         vc.current = current
193     }
195     // MARK: - Actions
197     @IBAction func sldTimeoutChanged(_ sender: Any) {
198         txtTimeout.text = String(format:"%.1f", sldTimeout.value)
199     }
201     @IBAction func actionCancel(_ sender: Any) {
202         print("actionCancel \(type(of: self))")
203         guard let navigationController = navigationController else { return }
204         navigationController.popViewController(animated:true)
205     }
207     @IBAction func actionSignIn(_ sender: Any) {
208         print("actionSignIn \(type(of: self))")
210         lblTitle.textColor = ShaarliM.labelColor
211         lblTitle.text = NSLocalizedString("", comment:"SettingsVC")// … probing server …
212         spiLogin.startAnimating()
214         let cli = ShaarliHtmlClient(AppDelegate.shared.semver)
215         let tim = TimeInterval(sldTimeout.value)
216         let cre = URLCredential(user:txtBasicUid.text ?? "",
217                             password:txtBasicPwd.text ?? "",
218                             persistence:.forSession)
219         func recurse(_ urls:ArraySlice<URL>) {
220             guard let cur = urls.first else {
221                 self.failure("Oops, something went utterly wrong.")
222                 return
223             }
224             cli.probe(cur, cre, tim) { (ur, ti, pride, tizo, er) in
225                 guard !ShaarliHtmlClient.isOk(er) else {
226                     self.success(ur, cre, ti, pride, tizo, tim)
227                     return
228                 }
229                 let res = urls.dropFirst()
230                 let tryNext = !res.isEmpty // todo: and not banned
231                 guard tryNext else {
232                     self.failure(er)
233                     return
234                 }
235                 recurse(res)
236             }
237         }
239         let urls = endpoints(txtEndpoint.text, txtUserName.text, txtPassWord.text)
240         spiLogin.startAnimating()
241         navigationItem.rightBarButtonItem?.isEnabled = false
242         recurse(urls)
243     }
245     // MARK: - Controller Logic
247     private func success(_ ur:URL, _ cre:URLCredential?, _ ti:String, _ pride:Bool, _ tizo:TimeZone?, _ tim:TimeInterval) {
248         let ad = ShaarliM.shared
249         DispatchQueue.main.sync {
250             ad.saveBlog(ad.defaults, BlogM(
251                 endpoint:ur,
252                 credential:cre,
253                 title:ti,
254                 timeout:tim,
255                 privateDefault:pride,
256                 timezone:tizo,
257                 tagsDefault:txtTags.text ?? ""
258             ))
259             navigationController?.popViewController(animated:true)
260         }
261     }
263     private func failure(_ er:String) {
264         DispatchQueue.main.sync {
265             spiLogin.stopAnimating()
266             navigationItem.rightBarButtonItem?.isEnabled = true
267             self.lblTitle.textColor = .red
268             self.lblTitle.text = er
269         }
270     }
272     // MARK: - UITextFieldDelegate
274     func textFieldShouldReturn(_ textField: UITextField) -> Bool {
275         print("textFieldShouldReturn \(type(of: self))")
276         switch textField {
277         case txtEndpoint:   txtUserName.becomeFirstResponder()
278         case txtUserName:   txtPassWord.becomeFirstResponder()
279         case txtPassWord:   txtTags.becomeFirstResponder()
280         case txtTags:       txtTimeout.becomeFirstResponder()
281         case txtTimeout:    txtBasicUid.becomeFirstResponder()
282         case txtBasicUid:   txtBasicPwd.becomeFirstResponder()
283         case txtBasicPwd:   textField.resignFirstResponder()
284             actionSignIn(textField) // keyboard doesn't show 'Done', but just in case... dispatch async?
285         default: return false
286         }
287         return true
288     }
290     func textFieldDidEndEditing(_ textField: UITextField) {
291         print("textFieldDidEndEditing \(type(of: self))")
292         switch textField {
293         case txtTimeout:
294                         guard let va = Float(txtTimeout.text ?? "") else {
295                             sldTimeout.value = Float(timeoutDefaultValue)
296                             sldTimeoutChanged(self)
297                             return
298                         }
299                         sldTimeout.value = Float(timeoutFromDouble(Double(va)))
300                         sldTimeoutChanged(self)
301         default: break
302         }
303     }
305     // MARK: - WKWebViewDelegate
307     func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
308         guard let url = navigationAction.request.url else {return}
309         if "file" == url.scheme {
310             decisionHandler(.allow)
311             return
312         }
314         let app = UIApplication.shared
315         if #available(iOS 10.0, *) {
316             app.open(url)
317         } else {
318             app.openURL(url)
319         }
320         decisionHandler(.cancel)
321     }
323     func webView(_ sender:WKWebView, didFinish:WKNavigation!) {
324         // even this late gives a flash sometimes: view.isOpaque = true
325         let semv = AppDelegate.shared.semver
326         let js = "injectVersion('\(semv)');"
327         wwwAbout.evaluateJavaScript(js) { res,err in print(err as Any) }
328         let s = wwwAbout.scrollView.contentSize
329         cellAbout.contentView.bounds = CGRect(origin: .zero, size: s)
330     }