Replace SwiftUI renderer with HTML/WKWebView
This commit is contained in:
parent
9d1d8828a0
commit
2d60f733c3
|
@ -0,0 +1,97 @@
|
||||||
|
//
|
||||||
|
// BrowserNavigationController.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/16/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import BrowserCore
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class BrowserNavigationController: UINavigationController {
|
||||||
|
|
||||||
|
let navigator: NavigationManager
|
||||||
|
|
||||||
|
var poppedViewControllers = [UIViewController]()
|
||||||
|
var skipResetPoppedOnNextPush = false
|
||||||
|
|
||||||
|
private var interactivePushTransition: InteractivePushTransition!
|
||||||
|
|
||||||
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
|
init(navigator: NavigationManager) {
|
||||||
|
self.navigator = navigator
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
navigator.$currentURL
|
||||||
|
.sink { [weak self] (newURL) in
|
||||||
|
self?.pushViewController(BrowserWebViewController(navigator: navigator, url: newURL), animated: true)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func popViewController(animated: Bool) -> UIViewController? {
|
||||||
|
if let popped = super.popViewController(animated: animated) {
|
||||||
|
poppedViewControllers.insert(popped, at: 0)
|
||||||
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||||
|
if let popped = super.popToRootViewController(animated: animated) {
|
||||||
|
poppedViewControllers = popped
|
||||||
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
||||||
|
if let popped = super.popToViewController(viewController, animated: animated) {
|
||||||
|
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
||||||
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||||
|
if skipResetPoppedOnNextPush {
|
||||||
|
skipResetPoppedOnNextPush = false
|
||||||
|
} else {
|
||||||
|
self.poppedViewControllers = []
|
||||||
|
}
|
||||||
|
super.pushViewController(viewController, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onWillShow() {
|
||||||
|
self.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in
|
||||||
|
if context.isCancelled {
|
||||||
|
if self.interactivePushTransition.interactive {
|
||||||
|
// when an interactive push gesture is cancelled, make sure to adding the VC that was being pushed back onto the popped stack so it doesn't disappear
|
||||||
|
self.poppedViewControllers.insert(self.interactivePushTransition.pushingViewController!, at: 0)
|
||||||
|
} else {
|
||||||
|
// when an interactive pop gesture is cancelled (i.e. the user lifts their finger before it triggers),
|
||||||
|
// the popViewController(animated:) method has already been called so the VC has already been added to the popped stack
|
||||||
|
// so we make sure to remove it, otherwise there could be duplicate VCs on the navigation stasck
|
||||||
|
self.poppedViewControllers.remove(at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
//
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
//
|
||||||
|
// InteractivePushTransition.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/16/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Allows interactively moving forward through the navigation stack after popping
|
||||||
|
/// Based on https://github.com/NSExceptional/TBInteractivePushTransition
|
||||||
|
class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
|
||||||
|
|
||||||
|
fileprivate let minimumPushVelocityThreshold: CGFloat = 700
|
||||||
|
fileprivate let minimumPushDistanceThreshold: CGFloat = 0.5
|
||||||
|
fileprivate let pushAnimationDuration: TimeInterval = 0.35
|
||||||
|
|
||||||
|
private(set) weak var navigationController: BrowserNavigationController!
|
||||||
|
|
||||||
|
private(set) weak var interactivePushGestureRecognizer: UIScreenEdgePanGestureRecognizer!
|
||||||
|
|
||||||
|
private(set) var interactive = false
|
||||||
|
private(set) weak var pushingViewController: UIViewController?
|
||||||
|
|
||||||
|
init(navigationController: BrowserNavigationController) {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.navigationController = navigationController
|
||||||
|
|
||||||
|
let interactivePushGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
||||||
|
self.interactivePushGestureRecognizer = interactivePushGestureRecognizer
|
||||||
|
|
||||||
|
navigationController.delegate = self
|
||||||
|
interactivePushGestureRecognizer.edges = .right
|
||||||
|
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||||
|
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
|
||||||
|
|
||||||
|
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
||||||
|
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||||
|
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
interactive = true
|
||||||
|
|
||||||
|
let velocity = recognizer.velocity(in: recognizer.view)
|
||||||
|
let translation = recognizer.translation(in: recognizer.view)
|
||||||
|
let dx = -translation.x / navigationController.view.bounds.width
|
||||||
|
let vx = -velocity.x
|
||||||
|
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
if let viewController = navigationController.poppedViewControllers.first {
|
||||||
|
pushingViewController = viewController
|
||||||
|
navigationController.poppedViewControllers.removeFirst()
|
||||||
|
navigationController.skipResetPoppedOnNextPush = true
|
||||||
|
navigationController.pushViewController(viewController, animated: true)
|
||||||
|
} else {
|
||||||
|
interactive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
update(dx)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
if (dx > minimumPushDistanceThreshold || vx > minimumPushVelocityThreshold) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
interactive = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
cancel()
|
||||||
|
interactive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InteractivePushTransition: UINavigationControllerDelegate {
|
||||||
|
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||||
|
self.navigationController.onWillShow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
if operation == .push, interactive {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
||||||
|
if interactive {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InteractivePushTransition: UIViewControllerAnimatedTransitioning {
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return pushAnimationDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
let toView = transitionContext.view(forKey: .to)!
|
||||||
|
let fromView = transitionContext.view(forKey: .from)!
|
||||||
|
|
||||||
|
let dimmingView = UIView()
|
||||||
|
dimmingView.backgroundColor = .black
|
||||||
|
dimmingView.alpha = 0
|
||||||
|
dimmingView.frame = fromView.frame
|
||||||
|
|
||||||
|
transitionContext.containerView.addSubview(dimmingView)
|
||||||
|
transitionContext.containerView.addSubview(toView)
|
||||||
|
|
||||||
|
// modify frame of presented view to go from x = <screen width> to x = 0
|
||||||
|
var frame = fromView.frame
|
||||||
|
frame.origin.x = frame.width
|
||||||
|
toView.frame = frame
|
||||||
|
frame.origin.x = 0
|
||||||
|
|
||||||
|
// we want it linear while interactive, but we want the "ease out" animation
|
||||||
|
// if the user flicks the screen ahrd enough to finish the transition without interaction
|
||||||
|
let options = interactive ? UIView.AnimationOptions.curveLinear : .curveEaseOut
|
||||||
|
|
||||||
|
let duration = transitionDuration(using: transitionContext)
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
|
||||||
|
// these magic numbers scientifically determined by repeatedly adjusting and comparing to the system animation
|
||||||
|
let translationDistance = -frame.size.width * 0.3
|
||||||
|
fromView.transform = CGAffineTransform(translationX: translationDistance, y: 0)
|
||||||
|
toView.frame = frame
|
||||||
|
dimmingView.alpha = 0.075
|
||||||
|
}) { (finished) in
|
||||||
|
fromView.transform = .identity
|
||||||
|
dimmingView.removeFromSuperview()
|
||||||
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
body {
|
||||||
|
font-family: ui-serif;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
overflow-x: auto;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.link {
|
||||||
|
display: block;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(0, 122, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(10, 132, 255);
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,13 +40,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
navigationManager.delegate = self
|
navigationManager.delegate = self
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
let contentView = ContentView(navigator: navigationManager)
|
// let contentView = ContentView(navigator: navigationManager)
|
||||||
|
|
||||||
// Use a UIHostingController as window root view controller.
|
// Use a UIHostingController as window root view controller.
|
||||||
if let windowScene = scene as? UIWindowScene {
|
if let windowScene = scene as? UIWindowScene {
|
||||||
let window = UIWindow(windowScene: windowScene)
|
let window = UIWindow(windowScene: windowScene)
|
||||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
window.rootViewController = UIHostingController(rootView: contentView)
|
window.rootViewController = BrowserNavigationController(navigator: navigationManager)
|
||||||
|
// window.rootViewController = UIHostingController(rootView: contentView)
|
||||||
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
||||||
self.window = window
|
self.window = window
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// TrackpadScrollGestureRecognizer.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/16/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
|
||||||
|
|
||||||
|
override init(target: Any?, action: Selector?) {
|
||||||
|
super.init(target: target, action: action)
|
||||||
|
|
||||||
|
self.allowedScrollTypesMask = .all
|
||||||
|
}
|
||||||
|
|
||||||
|
override func shouldReceive(_ event: UIEvent) -> Bool {
|
||||||
|
return event.type == .scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 52;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
@ -40,6 +40,13 @@
|
||||||
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
||||||
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
||||||
|
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */; };
|
||||||
|
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
||||||
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
||||||
|
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
||||||
|
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F620258B0811003A0A73 /* BrowserNavigationController.swift */; };
|
||||||
|
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F629258B0833003A0A73 /* InteractivePushTransition.swift */; };
|
||||||
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||||
|
@ -298,6 +305,12 @@
|
||||||
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
|
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
|
||||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
||||||
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||||
|
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
|
||||||
|
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
||||||
|
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||||
|
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -373,6 +386,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */,
|
||||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -530,6 +544,7 @@
|
||||||
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
||||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
||||||
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
||||||
|
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */,
|
||||||
);
|
);
|
||||||
path = GeminiRenderer;
|
path = GeminiRenderer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -550,11 +565,23 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D688F618258AD231003A0A73 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D688F5FE258ACE6B003A0A73 /* browser.css */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||||
|
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */,
|
||||||
|
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */,
|
||||||
|
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
|
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
||||||
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||||
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||||
|
@ -562,6 +589,7 @@
|
||||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||||
D62BCEE1252553620031D894 /* ActivityView.swift */,
|
D62BCEE1252553620031D894 /* ActivityView.swift */,
|
||||||
|
D688F618258AD231003A0A73 /* Resources */,
|
||||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||||
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
||||||
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
||||||
|
@ -748,6 +776,9 @@
|
||||||
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = GeminiRenderer;
|
name = GeminiRenderer;
|
||||||
|
packageProductDependencies = (
|
||||||
|
D688F58F258AC814003A0A73 /* HTMLEntities */,
|
||||||
|
);
|
||||||
productName = GeminiRenderer;
|
productName = GeminiRenderer;
|
||||||
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
||||||
productType = "com.apple.product-type.framework";
|
productType = "com.apple.product-type.framework";
|
||||||
|
@ -890,6 +921,9 @@
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = D626645224BBF1C200DF9B88;
|
mainGroup = D626645224BBF1C200DF9B88;
|
||||||
|
packageReferences = (
|
||||||
|
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
|
||||||
|
);
|
||||||
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
|
@ -968,6 +1002,7 @@
|
||||||
files = (
|
files = (
|
||||||
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
||||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
||||||
|
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */,
|
||||||
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1048,6 +1083,7 @@
|
||||||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
||||||
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
||||||
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
||||||
|
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */,
|
||||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1064,7 +1100,11 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||||
|
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */,
|
||||||
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||||
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
||||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||||
|
@ -1949,6 +1989,25 @@
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Kitura/swift-html-entities";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 3.0.200;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
D688F58F258AC814003A0A73 /* HTMLEntities */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
||||||
|
productName = HTMLEntities;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = D626645324BBF1C200DF9B88 /* Project object */;
|
rootObject = D626645324BBF1C200DF9B88 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "HTMLEntities",
|
||||||
|
"repositoryURL": "https://github.com/Kitura/swift-html-entities",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "2b14531d0c36dbb7c1c45a4d38db9c2e7898a307",
|
||||||
|
"version": "3.0.200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
|
@ -21,6 +21,13 @@ public struct Document {
|
||||||
// todo: should this be \r\n
|
// todo: should this be \r\n
|
||||||
return lines.map { $0.geminiText() }.joined(separator: "\n")
|
return lines.map { $0.geminiText() }.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var title: String? {
|
||||||
|
for case let .heading(text, level: _) in lines {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Document {
|
public extension Document {
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
//
|
||||||
|
// GeminiHTMLRenderer.swift
|
||||||
|
// GeminiRenderer
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/16/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GeminiFormat
|
||||||
|
import HTMLEntities
|
||||||
|
|
||||||
|
public protocol GeminiHTMLRendererDelegate: class {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeminiHTMLRenderer {
|
||||||
|
|
||||||
|
public weak var delegate: GeminiHTMLRendererDelegate?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderDocumentToHTML(_ doc: Document) -> String {
|
||||||
|
var str = ""
|
||||||
|
|
||||||
|
var inPreformatting = false
|
||||||
|
var inList = false
|
||||||
|
|
||||||
|
for line in doc.lines {
|
||||||
|
if inList && !line.isListItem {
|
||||||
|
str += "</ul>"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch line {
|
||||||
|
case let .text(text):
|
||||||
|
str += "<p>\(text.htmlEscape())</p>"
|
||||||
|
case let .link(url, text: maybeText):
|
||||||
|
let text = maybeText ?? url.absoluteString
|
||||||
|
str += "<p class=\"link\"><a href=\"\(url.absoluteString)\">\(text.htmlEscape())</a></p>"
|
||||||
|
case .preformattedToggle(alt: _):
|
||||||
|
inPreformatting = !inPreformatting
|
||||||
|
if inPreformatting {
|
||||||
|
str += "<pre>"
|
||||||
|
} else {
|
||||||
|
str += "</pre>"
|
||||||
|
}
|
||||||
|
case let .preformattedText(text):
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n"
|
||||||
|
case let .heading(text, level: level):
|
||||||
|
let tag = "h\(level.rawValue)"
|
||||||
|
str += "<\(tag)>\(text.htmlEscape())</\(tag)>"
|
||||||
|
case let .unorderedListItem(text):
|
||||||
|
if !inList {
|
||||||
|
inList = true
|
||||||
|
str += "<ul>"
|
||||||
|
}
|
||||||
|
str += "<li>\(text.htmlEscape())</li>"
|
||||||
|
case let .quote(text):
|
||||||
|
str += "<blockquote>\(text.htmlEscape())</blockquote>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Document.Line {
|
||||||
|
var isListItem: Bool {
|
||||||
|
switch self {
|
||||||
|
case .unorderedListItem(_):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue