Compare commits
No commits in common. "92fe14cd9bc9bbfb3d87942401735998b5b41f8b" and "9d1d8828a0fb319053355f8d4a454d0b30e6b11d" have entirely different histories.
92fe14cd9b
...
9d1d8828a0
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public protocol NavigationManagerDelegate: class {
|
||||
func loadNonGeminiURL(_ url: URL)
|
||||
|
@ -20,8 +19,6 @@ public class NavigationManager: NSObject, ObservableObject {
|
|||
@Published public var backStack = [URL]()
|
||||
@Published public var forwardStack = [URL]()
|
||||
|
||||
public let navigationOperation = PassthroughSubject<Operation, Never>()
|
||||
|
||||
public var displayURL: String {
|
||||
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
|
||||
if components.port == 1965 {
|
||||
|
@ -61,18 +58,17 @@ public class NavigationManager: NSObject, ObservableObject {
|
|||
backStack.append(currentURL)
|
||||
currentURL = url
|
||||
forwardStack = []
|
||||
|
||||
navigationOperation.send(.go)
|
||||
}
|
||||
|
||||
@objc public func reload() {
|
||||
public func reload() {
|
||||
let url = currentURL
|
||||
currentURL = url
|
||||
// todo: send navigation op
|
||||
}
|
||||
|
||||
@objc public func goBack() {
|
||||
back(count: 1)
|
||||
@objc public func back() {
|
||||
guard !backStack.isEmpty else { return }
|
||||
forwardStack.insert(currentURL, at: 0)
|
||||
currentURL = backStack.removeLast()
|
||||
}
|
||||
|
||||
public func back(count: Int) {
|
||||
|
@ -82,12 +78,12 @@ public class NavigationManager: NSObject, ObservableObject {
|
|||
forwardStack.insert(currentURL, at: 0)
|
||||
currentURL = removed.removeFirst()
|
||||
forwardStack.insert(contentsOf: removed, at: 0)
|
||||
|
||||
navigationOperation.send(.backward(count: count))
|
||||
}
|
||||
|
||||
@objc public func goForward() {
|
||||
forward(count: 1)
|
||||
@objc public func forward() {
|
||||
guard !forwardStack.isEmpty else { return }
|
||||
backStack.append(currentURL)
|
||||
currentURL = forwardStack.removeFirst()
|
||||
}
|
||||
|
||||
public func forward(count: Int) {
|
||||
|
@ -97,14 +93,6 @@ public class NavigationManager: NSObject, ObservableObject {
|
|||
backStack.append(currentURL)
|
||||
currentURL = removed.removeLast()
|
||||
backStack.append(contentsOf: removed)
|
||||
|
||||
navigationOperation.send(.forward(count: count))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension NavigationManager {
|
||||
enum Operation {
|
||||
case go, forward(count: Int), backward(count: Int)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// ActivityView.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/30/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityView: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = UIActivityViewController
|
||||
|
||||
let items: [Any]
|
||||
let activities: [UIActivity]?
|
||||
|
||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||
return UIActivityViewController(activityItems: items, applicationActivities: activities)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -13,8 +13,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
SymbolCache.load()
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
//
|
||||
// BrowserNavigationController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/17/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
class BrowserNavigationController: UIViewController {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
private var backBrowserVCs = [BrowserWebViewController]()
|
||||
private var forwardBrowserVCs = [BrowserWebViewController]()
|
||||
private var currentBrowserVC: BrowserWebViewController!
|
||||
|
||||
private var browserContainer: UIView!
|
||||
private var navBarView: NavigationBarView!
|
||||
private var toolbarView: ToolbarView!
|
||||
|
||||
private var gestureState: GestureState?
|
||||
private var trackingScroll = false
|
||||
private var scrollStartedBelowEnd = false
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
private var toolbarOffset: CGFloat = 0 {
|
||||
didSet {
|
||||
let realOffset = toolbarOffset * max(toolbarView.bounds.height, navBarView.bounds.height)
|
||||
toolbarView.transform = CGAffineTransform(translationX: 0, y: realOffset)
|
||||
navBarView.transform = CGAffineTransform(translationX: 0, y: -realOffset)
|
||||
|
||||
if (oldValue <= 0.5 && toolbarOffset > 0.5) || (oldValue > 0.5 && toolbarOffset <= 0.5) {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
toolbarOffset > 0.5
|
||||
}
|
||||
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
.slide
|
||||
}
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
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
|
||||
|
||||
browserContainer = UIView()
|
||||
browserContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.embedSubview(browserContainer)
|
||||
|
||||
currentBrowserVC = createBrowserVC(url: navigator.currentURL)
|
||||
currentBrowserVC.scrollViewDelegate = self
|
||||
embedChild(currentBrowserVC, in: browserContainer)
|
||||
|
||||
navBarView = NavigationBarView(navigator: navigator)
|
||||
navBarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navBarView)
|
||||
NSLayoutConstraint.activate([
|
||||
navBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
navBarView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
])
|
||||
|
||||
toolbarView = ToolbarView(navigator: navigator)
|
||||
toolbarView.showTableOfContents = self.showTableOfContents
|
||||
toolbarView.showShareSheet = self.showShareSheet
|
||||
toolbarView.showPreferences = self.showPreferences
|
||||
toolbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(toolbarView)
|
||||
NSLayoutConstraint.activate([
|
||||
toolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
toolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
toolbarView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
navigator.navigationOperation
|
||||
.sink(receiveValue: self.onNavigate)
|
||||
.store(in: &cancellables)
|
||||
|
||||
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized)))
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
setBrowserVCSafeAreaInsets(currentBrowserVC)
|
||||
}
|
||||
|
||||
private func createBrowserVC(url: URL) -> BrowserWebViewController {
|
||||
let vc = BrowserWebViewController(navigator: navigator, url: url)
|
||||
setBrowserVCSafeAreaInsets(vc)
|
||||
return vc
|
||||
}
|
||||
|
||||
private func setBrowserVCSafeAreaInsets(_ vc: BrowserWebViewController) {
|
||||
guard let toolbarView = toolbarView,
|
||||
let navBarView = navBarView else { return }
|
||||
vc.additionalSafeAreaInsets = UIEdgeInsets(
|
||||
top: navBarView.bounds.height - view.safeAreaInsets.top,
|
||||
left: 0,
|
||||
bottom: toolbarView.bounds.height - view.safeAreaInsets.bottom,
|
||||
right: 0
|
||||
)
|
||||
}
|
||||
|
||||
private func onNavigate(_ operation: NavigationManager.Operation) {
|
||||
let newVC: BrowserWebViewController
|
||||
|
||||
switch operation {
|
||||
case .go:
|
||||
backBrowserVCs.append(currentBrowserVC)
|
||||
newVC = BrowserWebViewController(navigator: navigator, url: navigator.currentURL)
|
||||
|
||||
case let .backward(count: count):
|
||||
var removed = backBrowserVCs.suffix(count)
|
||||
backBrowserVCs.removeLast(count)
|
||||
forwardBrowserVCs.insert(currentBrowserVC, at: 0)
|
||||
newVC = removed.removeFirst()
|
||||
forwardBrowserVCs.insert(contentsOf: removed, at: 0)
|
||||
|
||||
case let .forward(count: count):
|
||||
var removed = forwardBrowserVCs.prefix(count)
|
||||
forwardBrowserVCs.removeFirst(count)
|
||||
backBrowserVCs.append(currentBrowserVC)
|
||||
newVC = removed.removeFirst()
|
||||
backBrowserVCs.append(contentsOf: removed)
|
||||
}
|
||||
|
||||
currentBrowserVC.removeViewAndController()
|
||||
currentBrowserVC.scrollViewDelegate = nil
|
||||
currentBrowserVC = newVC
|
||||
currentBrowserVC.scrollViewDelegate = self
|
||||
embedChild(newVC, in: browserContainer)
|
||||
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
|
||||
self.toolbarOffset = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private let startEdgeNavigationSwipeDistance: CGFloat = 75
|
||||
private let finishEdgeNavigationVelocityThreshold: CGFloat = 500
|
||||
private let edgeNavigationMaxDimmingAlpha: CGFloat = 0.35
|
||||
private let edgeNavigationParallaxFactor: CGFloat = 0.25
|
||||
private let totalEdgeNavigationTime: TimeInterval = 0.4
|
||||
|
||||
@objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||
let location = recognizer.location(in: view)
|
||||
let velocity = recognizer.velocity(in: view)
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
// swipe gestures cannot begin in navbar/toolbar bounds
|
||||
let min = view.convert(navBarView.bounds, from: navBarView).maxY
|
||||
let max = view.convert(toolbarView.bounds, from: toolbarView).minY
|
||||
if toolbarOffset == 0 && (location.y < min || location.y > max) {
|
||||
return
|
||||
}
|
||||
|
||||
if location.x < startEdgeNavigationSwipeDistance && velocity.x > 0 && navigator.backStack.count > 0 {
|
||||
let older = backBrowserVCs.last ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.last!)
|
||||
embedChild(older, in: browserContainer)
|
||||
older.view.layer.zPosition = -2
|
||||
older.view.transform = CGAffineTransform(translationX: -1 * edgeNavigationParallaxFactor * view.bounds.width, y: 0)
|
||||
|
||||
let dimmingView = UIView()
|
||||
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dimmingView.backgroundColor = .black
|
||||
dimmingView.layer.zPosition = -1
|
||||
dimmingView.alpha = edgeNavigationMaxDimmingAlpha
|
||||
browserContainer.embedSubview(dimmingView)
|
||||
let animator = UIViewPropertyAnimator(duration: totalEdgeNavigationTime, curve: .easeInOut) {
|
||||
dimmingView.alpha = 0
|
||||
older.view.transform = .identity
|
||||
self.currentBrowserVC.view.transform = CGAffineTransform(translationX: self.view.bounds.width, y: 0)
|
||||
}
|
||||
animator.addCompletion { (position) in
|
||||
dimmingView.removeFromSuperview()
|
||||
|
||||
older.view.transform = .identity
|
||||
older.view.layer.zPosition = 0
|
||||
older.removeViewAndController()
|
||||
|
||||
self.currentBrowserVC.view.transform = .identity
|
||||
|
||||
if position == .end {
|
||||
self.navigator.goBack()
|
||||
}
|
||||
}
|
||||
gestureState = .backwards(animator)
|
||||
} else if location.x > view.bounds.width - startEdgeNavigationSwipeDistance && velocity.x < 0 && navigator.forwardStack.count > 0 {
|
||||
let newer = forwardBrowserVCs.first ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.first!)
|
||||
embedChild(newer, in: browserContainer)
|
||||
newer.view.transform = CGAffineTransform(translationX: view.bounds.width, y: 0)
|
||||
newer.view.layer.zPosition = 2
|
||||
|
||||
let dimmingView = UIView()
|
||||
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dimmingView.backgroundColor = .black
|
||||
dimmingView.layer.zPosition = 1
|
||||
dimmingView.alpha = 0
|
||||
browserContainer.embedSubview(dimmingView)
|
||||
let animator = UIViewPropertyAnimator(duration: totalEdgeNavigationTime, curve: .easeInOut) {
|
||||
dimmingView.alpha = self.edgeNavigationMaxDimmingAlpha
|
||||
newer.view.transform = .identity
|
||||
self.currentBrowserVC.view.transform = CGAffineTransform(translationX: -1 * self.edgeNavigationParallaxFactor * self.view.bounds.width, y: 0)
|
||||
}
|
||||
animator.addCompletion { (position) in
|
||||
dimmingView.removeFromSuperview()
|
||||
|
||||
newer.removeViewAndController()
|
||||
newer.view.layer.zPosition = 0
|
||||
newer.view.transform = .identity
|
||||
|
||||
self.currentBrowserVC.view.transform = .identity
|
||||
|
||||
if position == .end {
|
||||
self.navigator.goForward()
|
||||
}
|
||||
}
|
||||
gestureState = .forwards(animator)
|
||||
}
|
||||
|
||||
case .changed:
|
||||
let translation = recognizer.translation(in: view)
|
||||
|
||||
switch gestureState {
|
||||
case let .backwards(animator):
|
||||
animator.fractionComplete = translation.x / view.bounds.width
|
||||
case let .forwards(animator):
|
||||
animator.fractionComplete = abs(translation.x) / view.bounds.width
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
|
||||
case .ended, .cancelled:
|
||||
switch gestureState {
|
||||
case let .backwards(animator):
|
||||
let shouldComplete = location.x > view.bounds.width / 2 || velocity.x > finishEdgeNavigationVelocityThreshold
|
||||
animator.isReversed = !shouldComplete
|
||||
animator.startAnimation()
|
||||
case let .forwards(animator):
|
||||
let shouldComplete = location.x < view.bounds.width / 2 || velocity.x < -finishEdgeNavigationVelocityThreshold
|
||||
animator.isReversed = !shouldComplete
|
||||
animator.startAnimation()
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
|
||||
gestureState = nil
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func showTableOfContents() {
|
||||
guard let doc = currentBrowserVC.document else { return }
|
||||
let view = TableOfContentsView(document: doc) { (lineIndexToJumpTo) in
|
||||
self.dismiss(animated: true) {
|
||||
if let index = lineIndexToJumpTo {
|
||||
self.currentBrowserVC.scrollToLine(index: index, animated: !UIAccessibility.isReduceMotionEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
let host = UIHostingController(rootView: view)
|
||||
present(host, animated: true)
|
||||
}
|
||||
|
||||
private func showShareSheet(_ source: UIView) {
|
||||
let vc = UIActivityViewController(activityItems: [navigator.currentURL], applicationActivities: nil)
|
||||
vc.popoverPresentationController?.sourceView = source
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
private func showPreferences() {
|
||||
let host = UIHostingController(rootView: PreferencesView())
|
||||
present(host, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension BrowserNavigationController {
|
||||
enum GestureState {
|
||||
case backwards(UIViewPropertyAnimator)
|
||||
case forwards(UIViewPropertyAnimator)
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserNavigationController: UIScrollViewDelegate {
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
trackingScroll = true
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
scrollStartedBelowEnd = scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard trackingScroll else { return }
|
||||
defer { prevScrollViewContentOffset = scrollView.contentOffset }
|
||||
guard let prevOffset = prevScrollViewContentOffset else { return }
|
||||
|
||||
let delta = scrollView.contentOffset.y - prevOffset.y
|
||||
|
||||
let belowEnd = scrollView.contentOffset.y > (scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom)
|
||||
|
||||
if belowEnd {
|
||||
if scrollStartedBelowEnd {
|
||||
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
||||
self.toolbarOffset = 0
|
||||
}
|
||||
trackingScroll = false
|
||||
}
|
||||
} else if delta > 0 || (delta < 0 && toolbarOffset < 1) {
|
||||
let normalizedDelta = delta / max(toolbarView.bounds.height, navBarView.bounds.height)
|
||||
toolbarOffset = max(0, min(1, toolbarOffset + normalizedDelta))
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard trackingScroll else { return }
|
||||
trackingScroll = false
|
||||
|
||||
if velocity.y == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let finalOffset: CGFloat = velocity.y < 0 ? 0 : 1
|
||||
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseOut) {
|
||||
self.toolbarOffset = finalOffset
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
//
|
||||
// BrowserViewController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
import Combine
|
||||
|
||||
class BrowserViewController: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
private var scrollView: UIScrollView!
|
||||
|
||||
private var browserHost: UIHostingController<BrowserView>!
|
||||
private var navBarHost: UIHostingController<NavigationBar>!
|
||||
private var toolBarHost: UIHostingController<ToolBar>!
|
||||
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
|
||||
private var barAnimator: UIViewPropertyAnimator?
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
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
|
||||
|
||||
scrollView = UIScrollView()
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
view.addSubview(scrollView)
|
||||
scrollView.delegate = self
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
browserHost = UIHostingController(rootView: BrowserView(navigator: navigator, scrollingEnabled: false))
|
||||
browserHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(browserHost.view)
|
||||
addChild(browserHost)
|
||||
browserHost.didMove(toParent: self)
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: browserHost.view.leadingAnchor),
|
||||
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: browserHost.view.trailingAnchor),
|
||||
scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: browserHost.view.topAnchor),
|
||||
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: browserHost.view.bottomAnchor),
|
||||
browserHost.view.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||
|
||||
// make sure the browser host view is at least the screen height so the loading indicator appears centered
|
||||
browserHost.view.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor),
|
||||
])
|
||||
|
||||
navBarHost = UIHostingController(rootView: NavigationBar(navigator: navigator))
|
||||
navBarHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navBarHost.view)
|
||||
addChild(navBarHost)
|
||||
navBarHost.didMove(toParent: self)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
navBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
navBarHost.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
])
|
||||
|
||||
toolBarHost = UIHostingController(rootView: ToolBar(navigator: navigator))
|
||||
toolBarHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(toolBarHost.view)
|
||||
addChild(toolBarHost)
|
||||
toolBarHost.didMove(toParent: self)
|
||||
NSLayoutConstraint.activate([
|
||||
toolBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
toolBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
toolBarHost.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
navigator.$currentURL
|
||||
.sink { (_) in
|
||||
self.scrollView.contentOffset = .zero
|
||||
self.navBarHost.view.transform = .identity
|
||||
self.toolBarHost.view.transform = .identity
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let insets = UIEdgeInsets(
|
||||
top: navBarHost.view.bounds.height - view.safeAreaInsets.top,
|
||||
left: 0,
|
||||
bottom: toolBarHost.view.bounds.height - view.safeAreaInsets.bottom,
|
||||
right: 0
|
||||
)
|
||||
scrollView.contentInset = insets
|
||||
scrollView.scrollIndicatorInsets = insets
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
var scrollViewDelta: CGFloat = 0
|
||||
if let prev = prevScrollViewContentOffset {
|
||||
scrollViewDelta = scrollView.contentOffset.y - prev.y
|
||||
}
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
|
||||
// When certain state changes happen, the scroll view seems to "scroll" by top the safe area inset.
|
||||
// It's not actually user scrolling, and this screws up our animation, so we ignore it.
|
||||
guard abs(scrollViewDelta) != view.safeAreaInsets.top,
|
||||
scrollViewDelta != 0,
|
||||
scrollView.contentOffset.y > 0 else { return }
|
||||
|
||||
let barAnimator: UIViewPropertyAnimator
|
||||
if let animator = self.barAnimator {
|
||||
barAnimator = animator
|
||||
} else {
|
||||
navBarHost.view.transform = .identity
|
||||
toolBarHost.view.transform = .identity
|
||||
barAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .linear) {
|
||||
self.navBarHost.view.transform = CGAffineTransform(translationX: 0, y: -self.navBarHost.view.frame.height)
|
||||
self.toolBarHost.view.transform = CGAffineTransform(translationX: 0, y: self.toolBarHost.view.frame.height)
|
||||
}
|
||||
if scrollViewDelta < 0 {
|
||||
barAnimator.fractionComplete = 1
|
||||
}
|
||||
barAnimator.addCompletion { (_) in
|
||||
self.barAnimator = nil
|
||||
}
|
||||
self.barAnimator = barAnimator
|
||||
}
|
||||
|
||||
let progressDelta = scrollViewDelta / navBarHost.view.bounds.height
|
||||
barAnimator.fractionComplete = max(0, min(1, barAnimator.fractionComplete + progressDelta))
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
if let barAnimator = barAnimator {
|
||||
if barAnimator.fractionComplete < 0.5 {
|
||||
barAnimator.isReversed = true
|
||||
}
|
||||
barAnimator.startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,283 +0,0 @@
|
|||
//
|
||||
// BrowserWebViewController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import WebKit
|
||||
import GeminiProtocol
|
||||
import GeminiFormat
|
||||
import GeminiRenderer
|
||||
import SafariServices
|
||||
|
||||
class BrowserWebViewController: UIViewController {
|
||||
|
||||
let navigator: NavigationManager
|
||||
let url: URL
|
||||
|
||||
weak var scrollViewDelegate: UIScrollViewDelegate? {
|
||||
didSet {
|
||||
if isViewLoaded {
|
||||
webView.scrollView.delegate = scrollViewDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var task: GeminiDataTask?
|
||||
private let renderer = GeminiHTMLRenderer()
|
||||
private(set) var document: Document?
|
||||
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()
|
||||
|
||||
let documentURL = self.url
|
||||
renderer.linkPrefix = { (url: URL) -> String in
|
||||
let symbolClass: String
|
||||
if url.scheme == "gemini" {
|
||||
if url.host == documentURL.host {
|
||||
symbolClass = "arrow-right"
|
||||
} else {
|
||||
symbolClass = "link"
|
||||
}
|
||||
} else if url.scheme == "http" || url.scheme == "https" {
|
||||
symbolClass = "safari"
|
||||
} else if url.scheme == "mailto" {
|
||||
symbolClass = "envelope"
|
||||
} else {
|
||||
symbolClass = "arrow-up-left-square"
|
||||
}
|
||||
return "<span class=\"symbol \(symbolClass)\" aria-hidden=\"true\"></span>"
|
||||
|
||||
}
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
webView = WKWebView()
|
||||
webView.backgroundColor = .systemBackground
|
||||
webView.isOpaque = false
|
||||
webView.navigationDelegate = self
|
||||
webView.uiDelegate = self
|
||||
// it is safe to set the delegate of the web view's internal scroll view becuase WebKit takes care of forwarding between its internal delegate and our own
|
||||
webView.scrollView.delegate = scrollViewDelegate
|
||||
// this doesn't default to .default :S
|
||||
webView.scrollView.indicatorStyle = .default
|
||||
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) {
|
||||
self.document = doc
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToLine(index: Int, animated: Bool) {
|
||||
if animated {
|
||||
webView.evaluateJavaScript("document.getElementById('l\(index)').getBoundingClientRect().top + window.scrollY") { (result, error) in
|
||||
guard let result = result as? CGFloat else {
|
||||
return
|
||||
}
|
||||
let scrollView = self.webView.scrollView
|
||||
let y = result * scrollView.zoomScale - scrollView.safeAreaInsets.top
|
||||
let maxY = scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom
|
||||
let finalOffsetY = min(y, maxY)
|
||||
self.webView.scrollView.setContentOffset(CGPoint(x: 0, y: finalOffsetY), animated: true)
|
||||
}
|
||||
} else {
|
||||
webView.evaluateJavaScript("document.getElementById('l\(index)').scrollIntoView();")
|
||||
}
|
||||
}
|
||||
|
||||
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)">
|
||||
<style>
|
||||
\(symbolStyles)
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
|
||||
private static let symbolStyles = SymbolCache.symbols.map { (k, v) in
|
||||
".symbol.\(k.replacingOccurrences(of: ".", with: "-")) { background-image: url(\"data:image/png;base64,\(v)\"); }"
|
||||
}.joined(separator: "\n")
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserWebViewController: WKUIDelegate {
|
||||
func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
||||
guard let url = elementInfo.linkURL,
|
||||
url.scheme == "gemini" || url.scheme == "http" || url.scheme == "https" else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let config = UIContextMenuConfiguration(identifier: nil) {
|
||||
if url.scheme == "gemini" {
|
||||
return BrowserWebViewController(navigator: self.navigator, url: url)
|
||||
} else {
|
||||
return SFSafariViewController(url: url)
|
||||
}
|
||||
} actionProvider: { (_) in
|
||||
return nil
|
||||
}
|
||||
completionHandler(config)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
|
||||
animator.preferredCommitStyle = .pop
|
||||
let url = elementInfo.linkURL!
|
||||
animator.addCompletion {
|
||||
if url.scheme == "http" || url.scheme == "https" {
|
||||
self.present(animator.previewViewController!, animated: true)
|
||||
} else {
|
||||
self.navigator.changeURL(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 7/15/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject private var navigator: NavigationManager
|
||||
@State private var urlFieldContents: String
|
||||
@State private var prevScrollOffset: CGFloat = 0
|
||||
@State private var scrollOffset: CGFloat = 0 {
|
||||
didSet {
|
||||
prevScrollOffset = oldValue
|
||||
}
|
||||
}
|
||||
@State private var barOffset: CGFloat = 0
|
||||
@State private var navBarHeight: CGFloat = 0
|
||||
@State private var toolBarHeight: CGFloat = 0
|
||||
@State private var showShareSheet = false
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
GeometryReader { (outer: GeometryProxy) in
|
||||
ScrollView(.vertical) {
|
||||
Color.clear.frame(height: navBarHeight)
|
||||
|
||||
BrowserView(navigator: navigator, scrollingEnabled: false)
|
||||
.background(GeometryReader { (inner: GeometryProxy) in
|
||||
Color.clear.preference(key: ScrollOffsetPrefKey.self, value: -inner.frame(in: .global).minY + outer.frame(in: .global).minY)
|
||||
})
|
||||
|
||||
Color.clear.frame(height: toolBarHeight)
|
||||
}
|
||||
.onPreferenceChange(ScrollOffsetPrefKey.self) {
|
||||
scrollOffset = $0
|
||||
let delta = scrollOffset - prevScrollOffset
|
||||
|
||||
// When certain state changes happen, the scroll view seems to "scroll" by the top safe area inset.
|
||||
// It's not actually user scrolling, and this screws up our animation, so we ignore it.
|
||||
guard abs(delta) != outer.safeAreaInsets.top else { return }
|
||||
|
||||
if scrollOffset < 0 {
|
||||
barOffset = 0
|
||||
} else {
|
||||
if delta != 0 {
|
||||
barOffset += delta
|
||||
}
|
||||
|
||||
print(barOffset)
|
||||
barOffset = max(0, min(navBarHeight + outer.safeAreaInsets.top, barOffset))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
NavigationBar(navigator: navigator)
|
||||
.background(GeometryReader { (geom: GeometryProxy) in
|
||||
Color.clear.preference(key: NavBarHeightPrefKey.self, value: geom.frame(in: .global).height)
|
||||
})
|
||||
.offset(y: -barOffset)
|
||||
|
||||
Spacer()
|
||||
|
||||
ToolBar(navigator: navigator, showShareSheet: $showShareSheet)
|
||||
.background(GeometryReader { (geom: GeometryProxy) in
|
||||
Color.clear.preference(key: ToolBarHeightPrefKey.self, value: geom.frame(in: .global).height)
|
||||
})
|
||||
.offset(y: barOffset)
|
||||
}
|
||||
.onPreferenceChange(NavBarHeightPrefKey.self) {
|
||||
navBarHeight = $0
|
||||
print("nav bar height: \($0)")
|
||||
}
|
||||
.onPreferenceChange(ToolBarHeightPrefKey.self) {
|
||||
toolBarHeight = $0
|
||||
print("tool bar height: \($0)")
|
||||
}
|
||||
}
|
||||
.onAppear(perform: tweakAppearance)
|
||||
.onReceive(navigator.$currentURL, perform: { (new) in
|
||||
urlFieldContents = new.absoluteString
|
||||
})
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ActivityView(items: [navigator.currentURL], activities: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func tweakAppearance() {
|
||||
UIScrollView.appearance().keyboardDismissMode = .interactive
|
||||
}
|
||||
|
||||
private func commitURL() {
|
||||
guard let url = URL(string: urlFieldContents) else { return }
|
||||
navigator.changeURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ScrollOffsetPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct NavBarHeightPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ToolBarHeightPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum ScrollDirection {
|
||||
case up, down, none
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// NavigationBar.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
struct NavigationBar: View {
|
||||
@ObservedObject var navigator: NavigationManager
|
||||
@State private var urlFieldContents: String
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
self._urlFieldContents = State(initialValue: navigator.displayURL)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextField("URL", text: $urlFieldContents, onCommit: commitURL)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
|
||||
}
|
||||
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.top))
|
||||
.onReceive(navigator.$currentURL) { (_) in
|
||||
urlFieldContents = navigator.displayURL
|
||||
}
|
||||
}
|
||||
|
||||
private func commitURL() {
|
||||
guard let url = URL(string: urlFieldContents) else { return }
|
||||
navigator.changeURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
struct NavigationBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!))
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
//
|
||||
// NavigationBarView.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/19/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import Combine
|
||||
|
||||
class NavigationBarView: UIView {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
private var border: UIView!
|
||||
private var textField: UITextField!
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundColor = .systemBackground
|
||||
|
||||
border = UIView()
|
||||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||
border.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(border)
|
||||
NSLayoutConstraint.activate([
|
||||
border.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
border.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
border.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
border.heightAnchor.constraint(equalToConstant: 1),
|
||||
])
|
||||
|
||||
textField = UITextField()
|
||||
textField.text = navigator.displayURL
|
||||
textField.borderStyle = .roundedRect
|
||||
textField.keyboardType = .URL
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.addTarget(self, action: #selector(commitURL), for: .editingDidEnd)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(textField)
|
||||
NSLayoutConstraint.activate([
|
||||
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
textField.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
textField.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -9),
|
||||
])
|
||||
|
||||
navigator.$currentURL
|
||||
.sink { (newURL) in
|
||||
// can't use navigator.displayURL because the publisher fires before the underlying value is updated, so the displayURL getter returns the old value
|
||||
var components = URLComponents(url: newURL, resolvingAgainstBaseURL: false)!
|
||||
if components.port == 1965 {
|
||||
components.port = nil
|
||||
}
|
||||
self.textField.text = components.string!
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||
}
|
||||
|
||||
@objc private func commitURL() {
|
||||
if let text = textField.text, let url = URL(string: text) {
|
||||
navigator.changeURL(url)
|
||||
} else {
|
||||
textField.text = navigator.displayURL
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -23,7 +23,6 @@ struct PreferencesView: View {
|
|||
.insetOrGroupedListStyle()
|
||||
.navigationBarItems(trailing: doneButton)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.onDisappear {
|
||||
Preferences.save()
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
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: 4px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(0, 122, 255);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
margin-right: 0.25em;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
a {
|
||||
color: rgb(10, 132, 255);
|
||||
}
|
||||
|
||||
.symbol {
|
||||
filter: invert(100%);
|
||||
}
|
||||
}
|
|
@ -40,14 +40,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
navigationManager.delegate = self
|
||||
|
||||
// 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.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||
window.rootViewController = BrowserNavigationController(navigator: navigationManager)
|
||||
// window.rootViewController = UIHostingController(rootView: contentView)
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
@ -99,8 +98,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
extension SceneDelegate: NavigationManagerDelegate {
|
||||
func loadNonGeminiURL(_ url: URL) {
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||
guard !success else { return }
|
||||
if url.scheme == "http" || url.scheme == "https" {
|
||||
if !success {
|
||||
if Preferences.shared.useInAppSafari {
|
||||
let config = SFSafariViewController.Configuration()
|
||||
config.entersReaderIfAvailable = Preferences.shared.useReaderMode
|
||||
|
@ -109,13 +107,6 @@ extension SceneDelegate: NavigationManagerDelegate {
|
|||
} else {
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
}
|
||||
} else {
|
||||
let alert = UIAlertController(title: "Cannot open '\(url.scheme!)' URL", message: url.absoluteString, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Copy URL", style: .default, handler: { (_) in
|
||||
UIPasteboard.general.setObjects([url])
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
|
||||
self.window!.rootViewController!.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// SymbolCache.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/17/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SymbolCache {
|
||||
|
||||
private(set) static var symbols = [String: String]()
|
||||
|
||||
private static let defaultSymbols = [
|
||||
"arrow.right",
|
||||
"link",
|
||||
"safari",
|
||||
"envelope",
|
||||
"arrow.up.left.square",
|
||||
]
|
||||
|
||||
static func load() {
|
||||
defaultSymbols.forEach { loadSymbol(name: $0) }
|
||||
}
|
||||
|
||||
private static func loadSymbol(name: String) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 16)
|
||||
let symbol = UIImage(systemName: name, withConfiguration: config)!
|
||||
let data = symbol.pngData()!
|
||||
symbols[name] = data.base64EncodedString()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// TableOfContentsView.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/20/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import GeminiFormat
|
||||
|
||||
struct TableOfContentsView: View {
|
||||
private let entries: [Entry]
|
||||
private let close: (Int?) -> Void
|
||||
|
||||
init(document: Document, close: @escaping (Int?) -> Void) {
|
||||
let toc = TableOfContents(document: document)
|
||||
self.entries = toc.entries.flatMap { TableOfContentsView.flattenToCEntry($0) }
|
||||
self.close = close
|
||||
}
|
||||
|
||||
private static func flattenToCEntry(_ e: TableOfContents.Entry, depth: Int = 0) -> [Entry] {
|
||||
guard case let .heading(text, level: _) = e.line else { fatalError() }
|
||||
var entries = e.children.flatMap {
|
||||
flattenToCEntry($0, depth: depth + 1)
|
||||
}
|
||||
entries.insert(Entry(title: text, lineIndex: e.lineIndex, depth: depth), at: 0)
|
||||
return entries
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(Array(entries.enumerated()), id: \.0) { (a) in
|
||||
Button {
|
||||
close(a.1.lineIndex)
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
.frame(width: CGFloat(a.1.depth * 25))
|
||||
Text(verbatim: a.1.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.navigationBarTitle("Table of Contents", displayMode: .inline)
|
||||
.navigationBarItems(trailing: Button("Done", action: {
|
||||
close(nil)
|
||||
}))
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
extension TableOfContentsView {
|
||||
struct Entry {
|
||||
let title: String
|
||||
let lineIndex: Int
|
||||
let depth: Int
|
||||
}
|
||||
}
|
||||
|
||||
struct TableOfContentsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TableOfContentsView(document: Document(url: URL(string: "gemini://example.com")!, lines: [])) { (_) in }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// ToolBar.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
struct ToolBar: View {
|
||||
@ObservedObject var navigator: NavigationManager
|
||||
@Binding var showShareSheet: Bool
|
||||
@State private var showPreferencesSheet = false
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
|
||||
|
||||
HStack {
|
||||
// use a group because this exceeds the 10 view limit :/
|
||||
Group {
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.back) {
|
||||
Image(systemName: "arrow.left")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.accessibility(label: Text("Back"))
|
||||
.hoverEffect(.highlight)
|
||||
.contextMenu {
|
||||
ForEach(Array(navigator.backStack.suffix(5).enumerated()), id: \.1) { (index, url) in
|
||||
Button {
|
||||
navigator.back(count: min(5, navigator.backStack.count) - index)
|
||||
} label: {
|
||||
Text(verbatim: urlForDisplay(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(navigator.backStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.forward) {
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.accessibility(label: Text("Forward"))
|
||||
.hoverEffect(.highlight)
|
||||
.contextMenu {
|
||||
ForEach(navigator.forwardStack.prefix(5).enumerated().reversed(), id: \.1) { (index, url) in
|
||||
Button {
|
||||
navigator.forward(count: index + 1)
|
||||
} label: {
|
||||
Text(verbatim: urlForDisplay(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(navigator.forwardStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.reload) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.accessibility(label: Text("Reload"))
|
||||
.hoverEffect(.highlight)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
showShareSheet = true
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.accessibility(label: Text("Share"))
|
||||
.hoverEffect(.highlight)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showPreferencesSheet = true
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.font(.system(size: 24))
|
||||
})
|
||||
.accessibility(label: Text("Preferences"))
|
||||
.hoverEffect(.highlight)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.bottom))
|
||||
.sheet(isPresented: $showPreferencesSheet, content: {
|
||||
PreferencesView()
|
||||
})
|
||||
}
|
||||
|
||||
private func urlForDisplay(_ url: URL) -> String {
|
||||
var str = url.host!
|
||||
if let port = url.port,
|
||||
url.scheme != "gemini" || port != 1965 {
|
||||
str += ":\(port)"
|
||||
}
|
||||
str += url.path
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
struct ToolBar_Previews: PreviewProvider {
|
||||
@State private static var showShareSheet = false
|
||||
|
||||
static var previews: some View {
|
||||
ToolBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), showShareSheet: $showShareSheet)
|
||||
}
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
//
|
||||
// ToolbarView.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/19/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import Combine
|
||||
|
||||
class ToolbarView: UIView {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
var showTableOfContents: (() -> Void)?
|
||||
var showShareSheet: ((UIView) -> Void)?
|
||||
var showPreferences: (() -> Void)?
|
||||
|
||||
private var border: UIView!
|
||||
private var backButton: UIButton!
|
||||
private var forwardsButton: UIButton!
|
||||
private var reloadButton: UIButton!
|
||||
private var tableOfContentsButton: UIButton!
|
||||
private var shareButton: UIButton!
|
||||
private var prefsButton: UIButton!
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
backgroundColor = .systemBackground
|
||||
|
||||
border = UIView()
|
||||
border.translatesAutoresizingMaskIntoConstraints = false
|
||||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||
addSubview(border)
|
||||
NSLayoutConstraint.activate([
|
||||
border.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
border.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
border.topAnchor.constraint(equalTo: topAnchor),
|
||||
border.heightAnchor.constraint(equalToConstant: 1),
|
||||
])
|
||||
|
||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24)
|
||||
|
||||
backButton = UIButton()
|
||||
backButton.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside)
|
||||
backButton.isEnabled = navigator.backStack.count > 0
|
||||
backButton.setImage(UIImage(systemName: "arrow.left", withConfiguration: symbolConfig), for: .normal)
|
||||
backButton.accessibilityLabel = "Back"
|
||||
backButton.isPointerInteractionEnabled = true
|
||||
// fallback for when UIButton.menu isn't available
|
||||
if #available(iOS 14.0, *) {
|
||||
} else {
|
||||
backButton.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
forwardsButton = UIButton()
|
||||
forwardsButton.addTarget(navigator, action: #selector(NavigationManager.goForward), for: .touchUpInside)
|
||||
forwardsButton.isEnabled = navigator.forwardStack.count > 0
|
||||
forwardsButton.setImage(UIImage(systemName: "arrow.right", withConfiguration: symbolConfig), for: .normal)
|
||||
forwardsButton.accessibilityLabel = "Forward"
|
||||
forwardsButton.isPointerInteractionEnabled = true
|
||||
if #available(iOS 14.0, *) {
|
||||
} else {
|
||||
forwardsButton.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
reloadButton = UIButton()
|
||||
reloadButton.addTarget(navigator, action: #selector(NavigationManager.reload), for: .touchUpInside)
|
||||
reloadButton.setImage(UIImage(systemName: "arrow.clockwise", withConfiguration: symbolConfig), for: .normal)
|
||||
reloadButton.accessibilityLabel = "Reload"
|
||||
reloadButton.isPointerInteractionEnabled = true
|
||||
|
||||
tableOfContentsButton = UIButton()
|
||||
tableOfContentsButton.addTarget(self, action: #selector(tableOfContentsPressed), for: .touchUpInside)
|
||||
tableOfContentsButton.setImage(UIImage(systemName: "list.bullet.indent", withConfiguration: symbolConfig), for: .normal)
|
||||
tableOfContentsButton.accessibilityLabel = "Table of Contents"
|
||||
tableOfContentsButton.isPointerInteractionEnabled = true
|
||||
|
||||
shareButton = UIButton()
|
||||
shareButton.addTarget(self, action: #selector(sharePressed), for: .touchUpInside)
|
||||
shareButton.setImage(UIImage(systemName: "square.and.arrow.up", withConfiguration: symbolConfig), for: .normal)
|
||||
shareButton.accessibilityLabel = "Share"
|
||||
shareButton.isPointerInteractionEnabled = true
|
||||
|
||||
prefsButton = UIButton()
|
||||
prefsButton.addTarget(self, action: #selector(prefsPressed), for: .touchUpInside)
|
||||
prefsButton.setImage(UIImage(systemName: "gear", withConfiguration: symbolConfig), for: .normal)
|
||||
prefsButton.accessibilityLabel = "Preferences"
|
||||
prefsButton.isPointerInteractionEnabled = true
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
backButton,
|
||||
forwardsButton,
|
||||
reloadButton,
|
||||
tableOfContentsButton,
|
||||
shareButton,
|
||||
prefsButton,
|
||||
])
|
||||
stack.axis = .horizontal
|
||||
stack.distribution = .fillEqually
|
||||
stack.alignment = .fill
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(stack)
|
||||
let safeAreaConstraint = stack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
|
||||
safeAreaConstraint.priority = .defaultHigh
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||
safeAreaConstraint,
|
||||
stack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
|
||||
])
|
||||
|
||||
updateNavigationButtons()
|
||||
|
||||
navigator.navigationOperation
|
||||
.sink { (_) in
|
||||
self.updateNavigationButtons()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||
}
|
||||
|
||||
private func urlForDisplay(_ url: URL) -> String {
|
||||
var str = url.host!
|
||||
if let port = url.port,
|
||||
url.scheme != "gemini" || port != 1965 {
|
||||
str += ":\(port)"
|
||||
}
|
||||
str += url.path
|
||||
return str
|
||||
}
|
||||
|
||||
private func updateNavigationButtons() {
|
||||
backButton.isEnabled = navigator.backStack.count > 0
|
||||
forwardsButton.isEnabled = navigator.forwardStack.count > 0
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, url) -> UIAction in
|
||||
let backCount = min(5, navigator.backStack.count) - index
|
||||
return UIAction(title: urlForDisplay(url)) { (_) in
|
||||
self.navigator.back(count: backCount)
|
||||
}
|
||||
}
|
||||
backButton.menu = UIMenu(children: back)
|
||||
|
||||
let forward = navigator.forwardStack.prefix(5).enumerated().map { (index, url) -> UIAction in
|
||||
let forwardCount = index + 1
|
||||
return UIAction(title: urlForDisplay(url)) { (_) in
|
||||
self.navigator.forward(count: forwardCount)
|
||||
}
|
||||
}
|
||||
forwardsButton.menu = UIMenu(children: forward)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tableOfContentsPressed() {
|
||||
showTableOfContents?()
|
||||
}
|
||||
|
||||
@objc private func sharePressed() {
|
||||
showShareSheet?(shareButton)
|
||||
}
|
||||
|
||||
@objc private func prefsPressed() {
|
||||
showPreferences?()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ToolbarView: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
if interaction.view == backButton {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
||||
let children = self.navigator.backStack.suffix(5).enumerated().map { (index, url) in
|
||||
UIAction(title: self.urlForDisplay(url)) { (_) in
|
||||
self.navigator.back(count: min(5, self.navigator.backStack.count) - index)
|
||||
}
|
||||
}
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
|
||||
}
|
||||
} else if interaction.view == forwardsButton {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
||||
let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, url) -> UIAction in
|
||||
let forwardCount = index + 1
|
||||
return UIAction(title: self.urlForDisplay(url)) { (_) in
|
||||
self.navigator.forward(count: forwardCount)
|
||||
}
|
||||
}
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
//
|
||||
// UIViewController+Children.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/17/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
|
||||
extension UIViewController {
|
||||
func embedChild(_ newChild: UIViewController, in container: UIView? = nil) {
|
||||
// if the view controller is already a child of something else, remove it
|
||||
if let oldParent = newChild.parent, oldParent != self {
|
||||
newChild.beginAppearanceTransition(false, animated: false)
|
||||
newChild.willMove(toParent: nil)
|
||||
newChild.removeFromParent()
|
||||
|
||||
if newChild.viewIfLoaded?.superview != nil {
|
||||
newChild.viewIfLoaded?.removeFromSuperview()
|
||||
}
|
||||
|
||||
newChild.endAppearanceTransition()
|
||||
}
|
||||
|
||||
// since .view returns an IUO, by default the type of this is "UIView?"
|
||||
// explicitly type the variable because We Know Better™
|
||||
var targetContainer: UIView = container ?? self.view
|
||||
if !targetContainer.isContainedWithin(view) {
|
||||
targetContainer = view
|
||||
}
|
||||
|
||||
// add the view controller as a child
|
||||
if newChild.parent != self {
|
||||
newChild.beginAppearanceTransition(true, animated: false)
|
||||
addChild(newChild)
|
||||
newChild.didMove(toParent: self)
|
||||
targetContainer.embedSubview(newChild.view)
|
||||
newChild.endAppearanceTransition()
|
||||
} else {
|
||||
// the view controller is already a child
|
||||
// make sure it's in the right view
|
||||
|
||||
// we don't do the appearance transition stuff here,
|
||||
// because the vc is already a child, so *presumably*
|
||||
// that transition stuff has already appened
|
||||
targetContainer.embedSubview(newChild.view)
|
||||
}
|
||||
}
|
||||
|
||||
func removeViewAndController() {
|
||||
view.removeFromSuperview()
|
||||
removeFromParent()
|
||||
}
|
||||
}
|
||||
|
||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
|
||||
extension UIView {
|
||||
func embedSubview(_ subview: UIView) {
|
||||
if subview.superview == self { return }
|
||||
|
||||
if subview.superview != nil {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
subview.frame = bounds
|
||||
addSubview(subview)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
subview.topAnchor.constraint(equalTo: topAnchor),
|
||||
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
func isContainedWithin(_ other: UIView) -> Bool {
|
||||
var current: UIView? = self
|
||||
while let proposedView = current {
|
||||
if proposedView == other { return true }
|
||||
current = proposedView.superview
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -36,32 +36,23 @@
|
|||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; };
|
||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; };
|
||||
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62BCEE1252553620031D894 /* ActivityView.swift */; };
|
||||
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.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 */; };
|
||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F649258C17F3003A0A73 /* SymbolCache.swift */; };
|
||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F659258C2256003A0A73 /* BrowserNavigationController.swift */; };
|
||||
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F662258C2479003A0A73 /* UIViewController+Children.swift */; };
|
||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A69F252242FC00348C4B /* ToolBar.swift */; };
|
||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
||||
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.swift */; };
|
||||
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9AB2258E8E13008652BC /* ToolbarView.swift */; };
|
||||
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9ABB258E9862008652BC /* NavigationBarView.swift */; };
|
||||
D6BC9AC5258F01F6008652BC /* TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9AC4258F01F6008652BC /* TableOfContents.swift */; };
|
||||
D6BC9ACE258F07BC008652BC /* TableOfContentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */; };
|
||||
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */; };
|
||||
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DA5782252396030048B65A /* View+Extensions.swift */; };
|
||||
D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; };
|
||||
D6E1529924BFAAA400FDF9D3 /* BrowserWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */; };
|
||||
D6E1529B24BFAEC700FDF9D3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */; };
|
||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */; };
|
||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */; };
|
||||
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A824BFFDF500FDF9D3 /* ContentView.swift */; };
|
||||
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */; };
|
||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E152AD24BFFDF600FDF9D3 /* Preview Assets.xcassets */; };
|
||||
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */; };
|
||||
|
@ -303,26 +294,18 @@
|
|||
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; };
|
||||
D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; };
|
||||
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; };
|
||||
D62BCEE1252553620031D894 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.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>"; };
|
||||
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>"; };
|
||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolCache.swift; sourceTree = "<group>"; };
|
||||
D688F659258C2256003A0A73 /* BrowserNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationController.swift; sourceTree = "<group>"; };
|
||||
D688F662258C2479003A0A73 /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.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>"; };
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||
D691A68625223A4600348C4B /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = "<group>"; };
|
||||
D691A69F252242FC00348C4B /* ToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolBar.swift; sourceTree = "<group>"; };
|
||||
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = "<group>"; };
|
||||
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; };
|
||||
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = "<group>"; };
|
||||
D6BC9AB2258E8E13008652BC /* ToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarView.swift; sourceTree = "<group>"; };
|
||||
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarView.swift; sourceTree = "<group>"; };
|
||||
D6BC9AC4258F01F6008652BC /* TableOfContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOfContents.swift; sourceTree = "<group>"; };
|
||||
D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOfContentsTests.swift; sourceTree = "<group>"; };
|
||||
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOfContentsView.swift; sourceTree = "<group>"; };
|
||||
D6DA5782252396030048B65A /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowController.swift; sourceTree = "<group>"; };
|
||||
D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowserWindowController.xib; sourceTree = "<group>"; };
|
||||
|
@ -330,6 +313,7 @@
|
|||
D6E152A224BFFDF500FDF9D3 /* Gemini-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gemini-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D6E152AD24BFFDF600FDF9D3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
D6E152B024BFFDF600FDF9D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
|
@ -389,7 +373,6 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */,
|
||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -522,7 +505,6 @@
|
|||
D62664AB24BBF26A00DF9B88 /* Info.plist */,
|
||||
D62664C724BBF2C600DF9B88 /* Document.swift */,
|
||||
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
|
||||
D6BC9AC4258F01F6008652BC /* TableOfContents.swift */,
|
||||
);
|
||||
path = GeminiFormat;
|
||||
sourceTree = "<group>";
|
||||
|
@ -532,7 +514,6 @@
|
|||
children = (
|
||||
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */,
|
||||
D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */,
|
||||
D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */,
|
||||
D62664B924BBF26A00DF9B88 /* Info.plist */,
|
||||
);
|
||||
path = GeminiFormatTests;
|
||||
|
@ -549,7 +530,6 @@
|
|||
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
||||
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */,
|
||||
);
|
||||
path = GeminiRenderer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -570,30 +550,18 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D688F618258AD231003A0A73 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D688F5FE258ACE6B003A0A73 /* browser.css */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */,
|
||||
D688F659258C2256003A0A73 /* BrowserNavigationController.swift */,
|
||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
||||
D688F662258C2479003A0A73 /* UIViewController+Children.swift */,
|
||||
D6BC9AB2258E8E13008652BC /* ToolbarView.swift */,
|
||||
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
|
||||
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||
D691A69F252242FC00348C4B /* ToolBar.swift */,
|
||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||
D688F618258AD231003A0A73 /* Resources */,
|
||||
D62BCEE1252553620031D894 /* ActivityView.swift */,
|
||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
||||
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
||||
|
@ -780,9 +748,6 @@
|
|||
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
||||
);
|
||||
name = GeminiRenderer;
|
||||
packageProductDependencies = (
|
||||
D688F58F258AC814003A0A73 /* HTMLEntities */,
|
||||
);
|
||||
productName = GeminiRenderer;
|
||||
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
|
@ -925,9 +890,6 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = D626645224BBF1C200DF9B88;
|
||||
packageReferences = (
|
||||
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
|
||||
);
|
||||
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
@ -1006,7 +968,6 @@
|
|||
files = (
|
||||
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */,
|
||||
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1066,7 +1027,6 @@
|
|||
files = (
|
||||
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
|
||||
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
|
||||
D6BC9AC5258F01F6008652BC /* TableOfContents.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1075,7 +1035,6 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */,
|
||||
D6BC9ACE258F07BC008652BC /* TableOfContentsTests.swift in Sources */,
|
||||
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1089,7 +1048,6 @@
|
|||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
||||
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
||||
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
||||
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */,
|
||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1107,17 +1065,13 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */,
|
||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
|
||||
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
||||
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */,
|
||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
|
||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
||||
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */,
|
||||
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1995,25 +1949,6 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "HTMLEntities",
|
||||
"repositoryURL": "https://github.com/Kitura/swift-html-entities",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2b14531d0c36dbb7c1c45a4d38db9c2e7898a307",
|
||||
"version": "3.0.200"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -137,11 +137,11 @@ extension BrowserWindowController: NSToolbarDelegate {
|
|||
}
|
||||
|
||||
@objc private func back() {
|
||||
navigator.goBack()
|
||||
navigator.back()
|
||||
}
|
||||
|
||||
@objc private func forward() {
|
||||
navigator.goForward()
|
||||
navigator.forward()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,13 +21,6 @@ public struct Document {
|
|||
// todo: should this be \r\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 {
|
||||
|
@ -70,7 +63,7 @@ public extension Document {
|
|||
}
|
||||
|
||||
public extension Document {
|
||||
enum HeadingLevel: Int, Comparable {
|
||||
enum HeadingLevel: Int {
|
||||
case h1 = 1, h2 = 2, h3 = 3
|
||||
|
||||
var geminiText: String {
|
||||
|
@ -83,9 +76,5 @@ public extension Document {
|
|||
return "###"
|
||||
}
|
||||
}
|
||||
|
||||
public static func < (lhs: Document.HeadingLevel, rhs: Document.HeadingLevel) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
//
|
||||
// TableOfContents.swift
|
||||
// GeminiFormat
|
||||
//
|
||||
// Created by Shadowfacts on 12/19/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct TableOfContents {
|
||||
public let entries: [Entry]
|
||||
|
||||
public init(document: Document) {
|
||||
self.entries = TableOfContents.entries(lines: document.lines)
|
||||
}
|
||||
|
||||
private static func entries(lines: [Document.Line]) -> [Entry] {
|
||||
var topLevelEntries = [Entry]()
|
||||
|
||||
var currentEntries = [Entry]()
|
||||
|
||||
var index = 0
|
||||
while index < lines.count {
|
||||
defer { index += 1 }
|
||||
|
||||
let line = lines[index]
|
||||
guard case let .heading(_, level: level) = line else {
|
||||
continue
|
||||
}
|
||||
|
||||
let newEntry = Entry(line: line, lineIndex: index)
|
||||
|
||||
while !currentEntries.isEmpty && level <= currentEntries.last!.level {
|
||||
currentEntries.removeLast()
|
||||
}
|
||||
|
||||
if let last = currentEntries.last {
|
||||
last.children.append(newEntry)
|
||||
currentEntries.append(newEntry)
|
||||
} else {
|
||||
topLevelEntries.append(newEntry)
|
||||
currentEntries.append(newEntry)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return topLevelEntries
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension TableOfContents {
|
||||
class Entry: Equatable {
|
||||
public let line: Document.Line
|
||||
let level: Document.HeadingLevel
|
||||
public let lineIndex: Int
|
||||
public fileprivate(set) var children: [Entry]
|
||||
|
||||
init(line: Document.Line, lineIndex: Int) {
|
||||
guard case let .heading(_, level: level) = line else { fatalError() }
|
||||
self.line = line
|
||||
self.level = level
|
||||
self.lineIndex = lineIndex
|
||||
self.children = []
|
||||
}
|
||||
|
||||
public static func ==(lhs: Entry, rhs: Entry) -> Bool {
|
||||
return lhs.line == rhs.line && lhs.lineIndex == rhs.lineIndex && lhs.children == rhs.children
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
//
|
||||
// TableOfContentsTests.swift
|
||||
// GeminiFormatTests
|
||||
//
|
||||
// Created by Shadowfacts on 12/19/20.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import GeminiFormat
|
||||
|
||||
class TableOfContentsTests: XCTestCase {
|
||||
|
||||
func testOneHeading() {
|
||||
let one = Document(url: URL(string: "gemini://example.com/")!, lines: [
|
||||
.heading("Heading", level: .h1)
|
||||
])
|
||||
XCTAssertEqual(TableOfContents(document: one).entries, [
|
||||
.init(line: .heading("Heading", level: .h1), lineIndex: 0)
|
||||
])
|
||||
|
||||
let two = Document(url: URL(string: "gemini://example.com/")!, lines: [
|
||||
.heading("Heading", level: .h2)
|
||||
])
|
||||
XCTAssertEqual(TableOfContents(document: two).entries, [
|
||||
.init(line: .heading("Heading", level: .h2), lineIndex: 0)
|
||||
])
|
||||
|
||||
let three = Document(url: URL(string: "gemini://example.com/")!, lines: [
|
||||
.heading("Heading", level: .h3)
|
||||
])
|
||||
XCTAssertEqual(TableOfContents(document: three).entries, [
|
||||
.init(line: .heading("Heading", level: .h3), lineIndex: 0)
|
||||
])
|
||||
}
|
||||
|
||||
func testMultipleTopLevelHeadings() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("One", level: .h1),
|
||||
.heading("Two", level: .h1),
|
||||
])
|
||||
XCTAssertEqual(TableOfContents(document: doc).entries, [
|
||||
.init(line: .heading("One", level: .h1), lineIndex: 0),
|
||||
.init(line: .heading("Two", level: .h1), lineIndex: 1)
|
||||
])
|
||||
}
|
||||
|
||||
func testNestedHeadings() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("One", level: .h1),
|
||||
.heading("Two", level: .h2),
|
||||
])
|
||||
let entries = TableOfContents(document: doc).entries
|
||||
XCTAssertEqual(entries.count, 1)
|
||||
XCTAssertEqual(entries[0].line, .heading("One", level: .h1))
|
||||
XCTAssertEqual(entries[0].lineIndex, 0)
|
||||
XCTAssertEqual(entries[0].children, [
|
||||
.init(line: .heading("Two", level: .h2), lineIndex: 1)
|
||||
])
|
||||
}
|
||||
|
||||
func testTriplyNestedHeadings() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("One", level: .h1),
|
||||
.heading("Two", level: .h2),
|
||||
.heading("Three", level: .h3),
|
||||
])
|
||||
let entries = TableOfContents(document: doc).entries
|
||||
XCTAssertEqual(entries.count, 1)
|
||||
XCTAssertEqual(entries[0].line, .heading("One", level: .h1))
|
||||
XCTAssertEqual(entries[0].lineIndex, 0)
|
||||
XCTAssertEqual(entries[0].children.count, 1)
|
||||
XCTAssertEqual(entries[0].children[0].line, .heading("Two", level: .h2))
|
||||
XCTAssertEqual(entries[0].children[0].lineIndex, 1)
|
||||
XCTAssertEqual(entries[0].children[0].children, [
|
||||
.init(line: .heading("Three", level: .h3), lineIndex: 2)
|
||||
])
|
||||
}
|
||||
|
||||
func testMultipleTopLevelSections() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("Top Level One", level: .h1),
|
||||
.heading("A", level: .h2),
|
||||
.heading("Top Level Two", level: .h1),
|
||||
.heading("B", level: .h2),
|
||||
])
|
||||
let entries = TableOfContents(document: doc).entries
|
||||
XCTAssertEqual(entries.count, 2)
|
||||
let first = entries[0]
|
||||
XCTAssertEqual(first.line, .heading("Top Level One", level: .h1))
|
||||
XCTAssertEqual(first.lineIndex, 0)
|
||||
XCTAssertEqual(first.children, [
|
||||
.init(line: .heading("A", level: .h2), lineIndex: 1)
|
||||
])
|
||||
let second = entries[1]
|
||||
XCTAssertEqual(second.line, .heading("Top Level Two", level: .h1))
|
||||
XCTAssertEqual(second.lineIndex, 2)
|
||||
XCTAssertEqual(second.children, [
|
||||
.init(line: .heading("B", level: .h2), lineIndex: 3)
|
||||
])
|
||||
}
|
||||
|
||||
func testMultipleNestedSections() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("Top Level", level: .h1),
|
||||
.heading("A", level: .h2),
|
||||
.heading("Third Level", level: .h3),
|
||||
.heading("B", level: .h2),
|
||||
.heading("Third Level 2", level: .h3),
|
||||
])
|
||||
let entries = TableOfContents(document: doc).entries
|
||||
XCTAssertEqual(entries.count, 1)
|
||||
let topLevel = entries[0]
|
||||
XCTAssertEqual(topLevel.line, .heading("Top Level", level: .h1))
|
||||
XCTAssertEqual(topLevel.lineIndex, 0)
|
||||
let children = topLevel.children
|
||||
XCTAssertEqual(children.count, 2)
|
||||
let first = children[0]
|
||||
XCTAssertEqual(first.line, .heading("A", level: .h2))
|
||||
XCTAssertEqual(first.lineIndex, 1)
|
||||
XCTAssertEqual(first.children, [
|
||||
.init(line: .heading("Third Level", level: .h3), lineIndex: 2)
|
||||
])
|
||||
let second = children[1]
|
||||
XCTAssertEqual(second.line, .heading("B", level: .h2))
|
||||
XCTAssertEqual(second.lineIndex, 3)
|
||||
XCTAssertEqual(second.children, [
|
||||
.init(line: .heading("Third Level 2", level: .h3), lineIndex: 4)
|
||||
])
|
||||
}
|
||||
|
||||
func testNonH1TopLevelSections() {
|
||||
let doc = Document(url: URL(string: "gemini://example.com")!, lines: [
|
||||
.heading("A", level: .h2),
|
||||
.heading("B", level: .h3),
|
||||
.heading("C", level: .h1),
|
||||
])
|
||||
let entries = TableOfContents(document: doc).entries
|
||||
XCTAssertEqual(entries.count, 2)
|
||||
let first = entries[0]
|
||||
XCTAssertEqual(first.line, .heading("A", level: .h2))
|
||||
XCTAssertEqual(first.lineIndex, 0)
|
||||
XCTAssertEqual(first.children, [
|
||||
.init(line: .heading("B", level: .h3), lineIndex: 1)
|
||||
])
|
||||
XCTAssertEqual(entries[1], .init(line: .heading("C", level: .h1), lineIndex: 2))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
//
|
||||
// GeminiHTMLRenderer.swift
|
||||
// GeminiRenderer
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GeminiFormat
|
||||
import HTMLEntities
|
||||
|
||||
public class GeminiHTMLRenderer {
|
||||
|
||||
public var linkPrefix: ((URL) -> String?)?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func renderDocumentToHTML(_ doc: Document) -> String {
|
||||
var str = ""
|
||||
|
||||
var inPreformatting = false
|
||||
var inList = false
|
||||
|
||||
for (index, line) in doc.lines.enumerated() {
|
||||
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
|
||||
let linkPrefix = self.linkPrefix?(url) ?? ""
|
||||
str += "<p class=\"link\">\(linkPrefix)<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) id=\"l\(index)\">\(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