Gemini/Gemini-iOS/BrowserWebViewController.swift

189 lines
6.4 KiB
Swift

//
// BrowserWebViewController.swift
// Gemini-iOS
//
// Created by Shadowfacts on 12/16/20.
//
import UIKit
import BrowserCore
import WebKit
import GeminiProtocol
import GeminiFormat
import GeminiRenderer
class BrowserWebViewController: UIViewController {
let navigator: NavigationManager
let url: URL
private var task: GeminiDataTask?
private let renderer = GeminiHTMLRenderer()
private var loaded = false
private var errorStack: UIStackView!
private var errorMessageLabel: UILabel!
private var activityIndicator: UIActivityIndicatorView!
private var webView: WKWebView!
init(navigator: NavigationManager, url: URL) {
self.navigator = navigator
self.url = url
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
webView = WKWebView()
webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let errorTitle = UILabel()
errorTitle.text = "An error occurred"
errorTitle.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline).withDesign(.serif)!, size: 0)
errorTitle.numberOfLines = 0
errorMessageLabel = UILabel()
errorMessageLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withDesign(.serif)!, size: 0)
errorMessageLabel.numberOfLines = 0
errorStack = UIStackView(arrangedSubviews: [
errorTitle,
errorMessageLabel,
])
errorStack.translatesAutoresizingMaskIntoConstraints = false
errorStack.axis = .vertical
errorStack.alignment = .center
errorStack.isHidden = true
view.addSubview(errorStack)
NSLayoutConstraint.activate([
errorStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
errorStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
errorStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.isHidden = true
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadDocument()
}
private func loadDocument() {
guard !loaded else { return }
webView.isHidden = true
errorStack.isHidden = true
activityIndicator.isHidden = false
activityIndicator.startAnimating()
let url = self.url
task = try! GeminiDataTask(url: url) { (response) in
self.task = nil
self.loaded = true
switch response {
case let .failure(error):
DispatchQueue.main.async {
self.showError(message: error.localizedDescription)
}
case let .success(response):
if response.status.isRedirect {
if let redirect = URL(string: response.meta) {
self.navigator.changeURL(redirect)
} else {
DispatchQueue.main.async {
self.showError(message: "Invalid redirect URL: '\(response.meta)'")
}
}
} else if response.status.isSuccess,
let text = response.bodyText {
self.renderDocument(GeminiParser.parse(text: text, baseURL: url))
} else {
DispatchQueue.main.async {
self.showError(message: "Unknown error: \(response.header)")
}
}
}
}
task!.resume()
}
private func showError(message: String) {
webView.isHidden = true
errorStack.isHidden = false
activityIndicator.isHidden = true
activityIndicator.stopAnimating()
errorMessageLabel.text = message
}
private func renderDocument(_ doc: Document) {
let html = BrowserWebViewController.preamble + renderer.renderDocumentToHTML(doc) + BrowserWebViewController.postamble
DispatchQueue.main.async {
self.webView.isHidden = false
self.errorStack.isHidden = true
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
if let title = doc.title {
self.navigationItem.title = title
}
self.webView.loadHTMLString(html, baseURL: Bundle.main.bundleURL)
}
}
private static let preamble = """
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="\(Bundle.main.url(forResource: "browser", withExtension: "css")!.absoluteString)">
</head>
<body>
"""
private static let postamble = """
</body>
</html>
"""
}
extension BrowserWebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url!
if url.scheme == "file" {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
navigator.changeURL(url)
loadDocument()
}
}
}