Compare commits
No commits in common. "92fe14cd9bc9bbfb3d87942401735998b5b41f8b" and "9d1d8828a0fb319053355f8d4a454d0b30e6b11d" have entirely different histories.
92fe14cd9b
...
9d1d8828a0
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
|
|
||||||
public protocol NavigationManagerDelegate: class {
|
public protocol NavigationManagerDelegate: class {
|
||||||
func loadNonGeminiURL(_ url: URL)
|
func loadNonGeminiURL(_ url: URL)
|
||||||
|
@ -20,8 +19,6 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
@Published public var backStack = [URL]()
|
@Published public var backStack = [URL]()
|
||||||
@Published public var forwardStack = [URL]()
|
@Published public var forwardStack = [URL]()
|
||||||
|
|
||||||
public let navigationOperation = PassthroughSubject<Operation, Never>()
|
|
||||||
|
|
||||||
public var displayURL: String {
|
public var displayURL: String {
|
||||||
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
|
||||||
if components.port == 1965 {
|
if components.port == 1965 {
|
||||||
|
@ -61,18 +58,17 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
backStack.append(currentURL)
|
backStack.append(currentURL)
|
||||||
currentURL = url
|
currentURL = url
|
||||||
forwardStack = []
|
forwardStack = []
|
||||||
|
|
||||||
navigationOperation.send(.go)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func reload() {
|
public func reload() {
|
||||||
let url = currentURL
|
let url = currentURL
|
||||||
currentURL = url
|
currentURL = url
|
||||||
// todo: send navigation op
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func goBack() {
|
@objc public func back() {
|
||||||
back(count: 1)
|
guard !backStack.isEmpty else { return }
|
||||||
|
forwardStack.insert(currentURL, at: 0)
|
||||||
|
currentURL = backStack.removeLast()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func back(count: Int) {
|
public func back(count: Int) {
|
||||||
|
@ -82,12 +78,12 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
forwardStack.insert(currentURL, at: 0)
|
forwardStack.insert(currentURL, at: 0)
|
||||||
currentURL = removed.removeFirst()
|
currentURL = removed.removeFirst()
|
||||||
forwardStack.insert(contentsOf: removed, at: 0)
|
forwardStack.insert(contentsOf: removed, at: 0)
|
||||||
|
|
||||||
navigationOperation.send(.backward(count: count))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func goForward() {
|
@objc public func forward() {
|
||||||
forward(count: 1)
|
guard !forwardStack.isEmpty else { return }
|
||||||
|
backStack.append(currentURL)
|
||||||
|
currentURL = forwardStack.removeFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func forward(count: Int) {
|
public func forward(count: Int) {
|
||||||
|
@ -97,14 +93,6 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
backStack.append(currentURL)
|
backStack.append(currentURL)
|
||||||
currentURL = removed.removeLast()
|
currentURL = removed.removeLast()
|
||||||
backStack.append(contentsOf: removed)
|
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 {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
SymbolCache.load()
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
return true
|
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()
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarItems(trailing: doneButton)
|
.navigationBarItems(trailing: doneButton)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
Preferences.save()
|
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
|
navigationManager.delegate = self
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
// let contentView = ContentView(navigator: navigationManager)
|
let contentView = ContentView(navigator: navigationManager)
|
||||||
|
|
||||||
// Use a UIHostingController as window root view controller.
|
// Use a UIHostingController as window root view controller.
|
||||||
if let windowScene = scene as? UIWindowScene {
|
if let windowScene = scene as? UIWindowScene {
|
||||||
let window = UIWindow(windowScene: windowScene)
|
let window = UIWindow(windowScene: windowScene)
|
||||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
window.rootViewController = BrowserNavigationController(navigator: navigationManager)
|
window.rootViewController = UIHostingController(rootView: contentView)
|
||||||
// window.rootViewController = UIHostingController(rootView: contentView)
|
|
||||||
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
||||||
self.window = window
|
self.window = window
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
@ -99,8 +98,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
extension SceneDelegate: NavigationManagerDelegate {
|
extension SceneDelegate: NavigationManagerDelegate {
|
||||||
func loadNonGeminiURL(_ url: URL) {
|
func loadNonGeminiURL(_ url: URL) {
|
||||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||||
guard !success else { return }
|
if !success {
|
||||||
if url.scheme == "http" || url.scheme == "https" {
|
|
||||||
if Preferences.shared.useInAppSafari {
|
if Preferences.shared.useInAppSafari {
|
||||||
let config = SFSafariViewController.Configuration()
|
let config = SFSafariViewController.Configuration()
|
||||||
config.entersReaderIfAvailable = Preferences.shared.useReaderMode
|
config.entersReaderIfAvailable = Preferences.shared.useReaderMode
|
||||||
|
@ -109,13 +107,6 @@ extension SceneDelegate: NavigationManagerDelegate {
|
||||||
} else {
|
} else {
|
||||||
UIApplication.shared.open(url, options: [:])
|
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;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 52;
|
objectVersion = 50;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
@ -36,32 +36,23 @@
|
||||||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; };
|
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; };
|
||||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
|
||||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; };
|
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 */; };
|
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
||||||
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
||||||
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */; };
|
|
||||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
|
||||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
|
||||||
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 */; };
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||||
|
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||||
|
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A69F252242FC00348C4B /* ToolBar.swift */; };
|
||||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
||||||
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.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 */; };
|
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DA5782252396030048B65A /* View+Extensions.swift */; };
|
||||||
D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; };
|
D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; };
|
||||||
D6E1529924BFAAA400FDF9D3 /* BrowserWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */; };
|
D6E1529924BFAAA400FDF9D3 /* BrowserWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */; };
|
||||||
D6E1529B24BFAEC700FDF9D3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */; };
|
D6E1529B24BFAEC700FDF9D3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */; };
|
||||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */; };
|
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */; };
|
||||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E152A624BFFDF500FDF9D3 /* SceneDelegate.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 */; };
|
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */; };
|
||||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E152AD24BFFDF600FDF9D3 /* Preview 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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
|
||||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
|
||||||
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
|
|
||||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
|
||||||
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>"; };
|
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
|
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6E152B024BFFDF600FDF9D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
@ -389,7 +373,6 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */,
|
|
||||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -522,7 +505,6 @@
|
||||||
D62664AB24BBF26A00DF9B88 /* Info.plist */,
|
D62664AB24BBF26A00DF9B88 /* Info.plist */,
|
||||||
D62664C724BBF2C600DF9B88 /* Document.swift */,
|
D62664C724BBF2C600DF9B88 /* Document.swift */,
|
||||||
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
|
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
|
||||||
D6BC9AC4258F01F6008652BC /* TableOfContents.swift */,
|
|
||||||
);
|
);
|
||||||
path = GeminiFormat;
|
path = GeminiFormat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -532,7 +514,6 @@
|
||||||
children = (
|
children = (
|
||||||
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */,
|
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */,
|
||||||
D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */,
|
D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */,
|
||||||
D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */,
|
|
||||||
D62664B924BBF26A00DF9B88 /* Info.plist */,
|
D62664B924BBF26A00DF9B88 /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = GeminiFormatTests;
|
path = GeminiFormatTests;
|
||||||
|
@ -549,7 +530,6 @@
|
||||||
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
||||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
||||||
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
||||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */,
|
|
||||||
);
|
);
|
||||||
path = GeminiRenderer;
|
path = GeminiRenderer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -570,30 +550,18 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D688F618258AD231003A0A73 /* Resources */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D688F5FE258ACE6B003A0A73 /* browser.css */,
|
|
||||||
);
|
|
||||||
path = Resources;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */,
|
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||||
D688F659258C2256003A0A73 /* BrowserNavigationController.swift */,
|
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
D691A69F252242FC00348C4B /* ToolBar.swift */,
|
||||||
D688F662258C2479003A0A73 /* UIViewController+Children.swift */,
|
|
||||||
D6BC9AB2258E8E13008652BC /* ToolbarView.swift */,
|
|
||||||
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
|
|
||||||
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
|
|
||||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||||
D688F618258AD231003A0A73 /* Resources */,
|
D62BCEE1252553620031D894 /* ActivityView.swift */,
|
||||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||||
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
||||||
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
||||||
|
@ -780,9 +748,6 @@
|
||||||
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = GeminiRenderer;
|
name = GeminiRenderer;
|
||||||
packageProductDependencies = (
|
|
||||||
D688F58F258AC814003A0A73 /* HTMLEntities */,
|
|
||||||
);
|
|
||||||
productName = GeminiRenderer;
|
productName = GeminiRenderer;
|
||||||
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
||||||
productType = "com.apple.product-type.framework";
|
productType = "com.apple.product-type.framework";
|
||||||
|
@ -925,9 +890,6 @@
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = D626645224BBF1C200DF9B88;
|
mainGroup = D626645224BBF1C200DF9B88;
|
||||||
packageReferences = (
|
|
||||||
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
|
|
||||||
);
|
|
||||||
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
|
@ -1006,7 +968,6 @@
|
||||||
files = (
|
files = (
|
||||||
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
||||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
||||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */,
|
|
||||||
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1066,7 +1027,6 @@
|
||||||
files = (
|
files = (
|
||||||
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
|
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
|
||||||
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
|
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
|
||||||
D6BC9AC5258F01F6008652BC /* TableOfContents.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1075,7 +1035,6 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */,
|
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */,
|
||||||
D6BC9ACE258F07BC008652BC /* TableOfContentsTests.swift in Sources */,
|
|
||||||
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */,
|
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1089,7 +1048,6 @@
|
||||||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
||||||
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
||||||
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
||||||
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */,
|
|
||||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1107,17 +1065,13 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
|
||||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
|
||||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||||
|
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
||||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||||
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */,
|
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */,
|
||||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
|
||||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
|
||||||
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
|
|
||||||
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
|
||||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
||||||
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */,
|
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1995,25 +1949,6 @@
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
|
||||||
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/Kitura/swift-html-entities";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 3.0.200;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
D688F58F258AC814003A0A73 /* HTMLEntities */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
|
||||||
productName = HTMLEntities;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
};
|
};
|
||||||
rootObject = D626645324BBF1C200DF9B88 /* Project object */;
|
rootObject = D626645324BBF1C200DF9B88 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
@objc private func back() {
|
||||||
navigator.goBack()
|
navigator.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func forward() {
|
@objc private func forward() {
|
||||||
navigator.goForward()
|
navigator.forward()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,6 @@ public struct Document {
|
||||||
// todo: should this be \r\n
|
// todo: should this be \r\n
|
||||||
return lines.map { $0.geminiText() }.joined(separator: "\n")
|
return lines.map { $0.geminiText() }.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
public var title: String? {
|
|
||||||
for case let .heading(text, level: _) in lines {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Document {
|
public extension Document {
|
||||||
|
@ -70,7 +63,7 @@ public extension Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Document {
|
public extension Document {
|
||||||
enum HeadingLevel: Int, Comparable {
|
enum HeadingLevel: Int {
|
||||||
case h1 = 1, h2 = 2, h3 = 3
|
case h1 = 1, h2 = 2, h3 = 3
|
||||||
|
|
||||||
var geminiText: String {
|
var geminiText: String {
|
||||||
|
@ -83,9 +76,5 @@ public extension Document {
|
||||||
return "###"
|
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