5 // Created by Marcus Rohrmoser on 21.02.20.
6 // Copyright © 2020-2022 Marcus Rohrmoser mobile Software http://mro.name/me. All rights reserved.
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/>.
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)
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))
37 guard var ep = URLComponents(string:"//\(base_)")
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)
48 : ep.path.hasSuffix("/")
53 ep.scheme = HTTP_HTTPS; urls.append(ep.url!)
54 ep.scheme = HTTP_HTTP; urls.append(ep.url!)
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()
79 // https://www.objc.io/blog/2018/04/24/bindings-with-kvo-and-keypaths/
80 override func 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")
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())
112 override func viewWillAppear(_ animated: Bool) {
113 super.viewWillAppear(animated)
114 spiLogin.stopAnimating()
115 navigationItem.rightBarButtonItem?.isEnabled = true
116 txtEndpoint.becomeFirstResponder()
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 {
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
136 alert.addAction(UIAlertAction(
137 title:NSLocalizedString("Cancel", comment:"SettingsVC"),
140 alert.addAction(UIAlertAction(
141 title: NSLocalizedString("Yes", comment:"SettingsVC"),
143 handler:{(_) in self.togui(url)}))
144 present(alert, animated:true, completion:nil)
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
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)
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
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
183 sldTimeout.value = Float(b.timeout)
184 sldTimeoutChanged(self)
185 txtTags.text = b.tagsDefault
188 // MARK: - Navigation
190 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
191 guard let vc = segue.destination as? MainVC else {return}
197 @IBAction func sldTimeoutChanged(_ sender: Any) {
198 txtTimeout.text = String(format:"%.1f", sldTimeout.value)
201 @IBAction func actionCancel(_ sender: Any) {
202 print("actionCancel \(type(of: self))")
203 guard let navigationController = navigationController else { return }
204 navigationController.popViewController(animated:true)
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.")
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)
229 let res = urls.dropFirst()
230 let tryNext = !res.isEmpty // todo: and not banned
239 let urls = endpoints(txtEndpoint.text, txtUserName.text, txtPassWord.text)
240 spiLogin.startAnimating()
241 navigationItem.rightBarButtonItem?.isEnabled = false
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(
255 privateDefault:pride,
257 tagsDefault:txtTags.text ?? ""
259 navigationController?.popViewController(animated:true)
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
272 // MARK: - UITextFieldDelegate
274 func textFieldShouldReturn(_ textField: UITextField) -> Bool {
275 print("textFieldShouldReturn \(type(of: self))")
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
290 func textFieldDidEndEditing(_ textField: UITextField) {
291 print("textFieldDidEndEditing \(type(of: self))")
294 guard let va = Float(txtTimeout.text ?? "") else {
295 sldTimeout.value = Float(timeoutDefaultValue)
296 sldTimeoutChanged(self)
299 sldTimeout.value = Float(timeoutFromDouble(Double(va)))
300 sldTimeoutChanged(self)
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)
314 let app = UIApplication.shared
315 if #available(iOS 10.0, *) {
320 decisionHandler(.cancel)
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)