Compare commits
21 Commits
13462d2307
...
fe9c42a2f8
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fe9c42a2f8 | |
Shadowfacts | bea574d7bb | |
Shadowfacts | 32648808cb | |
Shadowfacts | 29e2589d5c | |
Shadowfacts | e470d7b58e | |
Shadowfacts | 86719b4528 | |
Shadowfacts | b313f37170 | |
Shadowfacts | e872af04a3 | |
Shadowfacts | 0d85c6f6ea | |
Shadowfacts | d6ff2141dc | |
Shadowfacts | 806149ec1b | |
Shadowfacts | a9327249eb | |
Shadowfacts | c59fe5b319 | |
Shadowfacts | 2b82e3b545 | |
Shadowfacts | 144695cc96 | |
Shadowfacts | 7dd4f3aa4c | |
Shadowfacts | ebdd06738a | |
Shadowfacts | afa2a7b771 | |
Shadowfacts | c391a274e1 | |
Shadowfacts | e2cc4c4caa | |
Shadowfacts | 8c53685d21 |
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// BrowserHelper.swift
|
||||||
|
// BrowserCore
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct BrowserHelper {
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
public static func urlForDisplay(_ url: URL) -> String {
|
||||||
|
var str = url.host!
|
||||||
|
if let port = url.port,
|
||||||
|
url.scheme != "gemini" || port != 1965 {
|
||||||
|
str += ":\(port)"
|
||||||
|
}
|
||||||
|
if url.path != "/" {
|
||||||
|
str += url.path
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,19 +6,19 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
public protocol NavigationManagerDelegate: AnyObject {
|
public protocol NavigationManagerDelegate: AnyObject {
|
||||||
func loadNonGeminiURL(_ url: URL)
|
func loadNonGeminiURL(_ url: URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NavigationManager: NSObject, ObservableObject {
|
public class NavigationManager: NSObject, ObservableObject, Codable {
|
||||||
|
|
||||||
public weak var delegate: NavigationManagerDelegate?
|
public weak var delegate: NavigationManagerDelegate?
|
||||||
|
|
||||||
@Published public var currentURL: URL
|
@Published public var currentURL: URL
|
||||||
@Published public var backStack = [URL]()
|
@Published public var backStack = [HistoryEntry]()
|
||||||
@Published public var forwardStack = [URL]()
|
@Published public var forwardStack = [HistoryEntry]()
|
||||||
|
|
||||||
public let navigationOperation = PassthroughSubject<Operation, Never>()
|
public let navigationOperation = PassthroughSubject<Operation, Never>()
|
||||||
|
|
||||||
|
@ -30,8 +30,30 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
return components.string!
|
return components.string!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var currentHistoryEntry: HistoryEntry
|
||||||
|
|
||||||
public init(url: URL) {
|
public init(url: URL) {
|
||||||
self.currentURL = url
|
self.currentURL = url
|
||||||
|
self.currentHistoryEntry = HistoryEntry(url: url, title: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.currentHistoryEntry = try container.decode(HistoryEntry.self, forKey: .currentHistoryEntry)
|
||||||
|
self.currentURL = self.currentHistoryEntry.url
|
||||||
|
self.backStack = try container.decode([HistoryEntry].self, forKey: .backStack)
|
||||||
|
self.forwardStack = try container.decode([HistoryEntry].self, forKey: .forwardStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(currentHistoryEntry, forKey: .currentHistoryEntry)
|
||||||
|
try container.encode(backStack, forKey: .backStack)
|
||||||
|
try container.encode(forwardStack, forKey: .forwardStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setTitleForCurrentURL(_ title: String?) {
|
||||||
|
currentHistoryEntry.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
public func changeURL(_ url: URL) {
|
public func changeURL(_ url: URL) {
|
||||||
|
@ -58,8 +80,9 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
let url = components.url!
|
let url = components.url!
|
||||||
|
|
||||||
backStack.append(currentURL)
|
backStack.append(currentHistoryEntry)
|
||||||
currentURL = url
|
currentURL = url
|
||||||
|
currentHistoryEntry = HistoryEntry(url: url, title: nil)
|
||||||
forwardStack = []
|
forwardStack = []
|
||||||
|
|
||||||
navigationOperation.send(.go)
|
navigationOperation.send(.go)
|
||||||
|
@ -79,8 +102,9 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
guard count <= backStack.count else { return }
|
guard count <= backStack.count else { return }
|
||||||
var removed = backStack.suffix(count)
|
var removed = backStack.suffix(count)
|
||||||
backStack.removeLast(count)
|
backStack.removeLast(count)
|
||||||
forwardStack.insert(currentURL, at: 0)
|
forwardStack.insert(currentHistoryEntry, at: 0)
|
||||||
currentURL = removed.removeFirst()
|
currentHistoryEntry = removed.removeFirst()
|
||||||
|
currentURL = currentHistoryEntry.url
|
||||||
forwardStack.insert(contentsOf: removed, at: 0)
|
forwardStack.insert(contentsOf: removed, at: 0)
|
||||||
|
|
||||||
navigationOperation.send(.backward(count: count))
|
navigationOperation.send(.backward(count: count))
|
||||||
|
@ -94,8 +118,9 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
guard count <= forwardStack.count else { return }
|
guard count <= forwardStack.count else { return }
|
||||||
var removed = forwardStack.prefix(count)
|
var removed = forwardStack.prefix(count)
|
||||||
forwardStack.removeFirst(count)
|
forwardStack.removeFirst(count)
|
||||||
backStack.append(currentURL)
|
backStack.append(currentHistoryEntry)
|
||||||
currentURL = removed.removeLast()
|
currentHistoryEntry = removed.removeLast()
|
||||||
|
currentURL = currentHistoryEntry.url
|
||||||
backStack.append(contentsOf: removed)
|
backStack.append(contentsOf: removed)
|
||||||
|
|
||||||
navigationOperation.send(.forward(count: count))
|
navigationOperation.send(.forward(count: count))
|
||||||
|
@ -103,8 +128,23 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NavigationManager {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case currentHistoryEntry
|
||||||
|
case backStack
|
||||||
|
case forwardStack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public extension NavigationManager {
|
public extension NavigationManager {
|
||||||
enum Operation {
|
enum Operation {
|
||||||
case go, reload, forward(count: Int), backward(count: Int)
|
case go, reload, forward(count: Int), backward(count: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension NavigationManager {
|
||||||
|
struct HistoryEntry: Codable {
|
||||||
|
public let url: URL
|
||||||
|
public internal(set) var title: String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Intents
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
@ -13,6 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
static let defaultHomepage = URL(string: "gemini://gemini.circumlunar.space/")!
|
static let defaultHomepage = URL(string: "gemini://gemini.circumlunar.space/")!
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
|
||||||
SymbolCache.load()
|
SymbolCache.load()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -32,6 +34,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
|
||||||
|
switch intent {
|
||||||
|
case is OpenURLIntent:
|
||||||
|
return OpenURLIntentHandler()
|
||||||
|
|
||||||
|
case is OpenHomepageIntent:
|
||||||
|
return OpenHomepageIntentHandler()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,13 @@ class BrowserNavigationController: UIViewController {
|
||||||
.slide
|
.slide
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var userActivity: NSUserActivity? {
|
||||||
|
get {
|
||||||
|
currentBrowserVC?.userActivity
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
init(navigator: NavigationManager) {
|
init(navigator: NavigationManager) {
|
||||||
|
@ -75,6 +82,9 @@ class BrowserNavigationController: UIViewController {
|
||||||
currentBrowserVC.scrollViewDelegate = self
|
currentBrowserVC.scrollViewDelegate = self
|
||||||
embedChild(currentBrowserVC, in: browserContainer)
|
embedChild(currentBrowserVC, in: browserContainer)
|
||||||
|
|
||||||
|
backBrowserVCs = navigator.backStack.map { createBrowserVC(url: $0.url) }
|
||||||
|
forwardBrowserVCs = navigator.forwardStack.map { createBrowserVC(url: $0.url) }
|
||||||
|
|
||||||
navBarView = NavigationBarView(navigator: navigator)
|
navBarView = NavigationBarView(navigator: navigator)
|
||||||
navBarView.translatesAutoresizingMaskIntoConstraints = false
|
navBarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(navBarView)
|
view.addSubview(navBarView)
|
||||||
|
@ -101,6 +111,17 @@ class BrowserNavigationController: UIViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized)))
|
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized)))
|
||||||
|
|
||||||
|
Preferences.shared.$hideToolbarsWhenScrolling
|
||||||
|
.sink { [unowned self] (hideToolbarsWhenScrolling) in
|
||||||
|
if !hideToolbarsWhenScrolling {
|
||||||
|
// animate just in case the preference change came from another window and we're currently visible
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.toolbarOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
|
@ -191,7 +212,7 @@ class BrowserNavigationController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if location.x < startEdgeNavigationSwipeDistance && velocity.x > 0 && navigator.backStack.count > 0 {
|
if location.x < startEdgeNavigationSwipeDistance && velocity.x > 0 && navigator.backStack.count > 0 {
|
||||||
let older = backBrowserVCs.last ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.last!)
|
let older = backBrowserVCs.last ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.last!.url)
|
||||||
embedChild(older, in: browserContainer)
|
embedChild(older, in: browserContainer)
|
||||||
older.view.layer.zPosition = -2
|
older.view.layer.zPosition = -2
|
||||||
older.view.transform = CGAffineTransform(translationX: -1 * edgeNavigationParallaxFactor * view.bounds.width, y: 0)
|
older.view.transform = CGAffineTransform(translationX: -1 * edgeNavigationParallaxFactor * view.bounds.width, y: 0)
|
||||||
|
@ -226,7 +247,7 @@ class BrowserNavigationController: UIViewController {
|
||||||
}
|
}
|
||||||
gestureState = .backwards(animator)
|
gestureState = .backwards(animator)
|
||||||
} else if location.x > view.bounds.width - startEdgeNavigationSwipeDistance && velocity.x < 0 && navigator.forwardStack.count > 0 {
|
} 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!)
|
let newer = forwardBrowserVCs.first ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.first!.url)
|
||||||
embedChild(newer, in: browserContainer)
|
embedChild(newer, in: browserContainer)
|
||||||
newer.view.transform = CGAffineTransform(translationX: view.bounds.width, y: 0)
|
newer.view.transform = CGAffineTransform(translationX: view.bounds.width, y: 0)
|
||||||
newer.view.layer.zPosition = 2
|
newer.view.layer.zPosition = 2
|
||||||
|
@ -331,6 +352,7 @@ extension BrowserNavigationController {
|
||||||
|
|
||||||
extension BrowserNavigationController: UIScrollViewDelegate {
|
extension BrowserNavigationController: UIScrollViewDelegate {
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
guard Preferences.shared.hideToolbarsWhenScrolling else { return }
|
||||||
trackingScroll = true
|
trackingScroll = true
|
||||||
prevScrollViewContentOffset = scrollView.contentOffset
|
prevScrollViewContentOffset = scrollView.contentOffset
|
||||||
scrollStartedBelowEnd = scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom)
|
scrollStartedBelowEnd = scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import BrowserCore
|
||||||
import WebKit
|
import WebKit
|
||||||
import GeminiProtocol
|
import GeminiProtocol
|
||||||
import GeminiFormat
|
import GeminiFormat
|
||||||
import GeminiRenderer
|
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@ -45,6 +44,13 @@ class BrowserWebViewController: UIViewController {
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
userActivity = NSUserActivity(geminiURL: url)
|
||||||
|
userActivity!.isEligibleForPrediction = true
|
||||||
|
userActivity!.title = BrowserHelper.urlForDisplay(url)
|
||||||
|
// set the persistent identifier to the url, so that we don't get duplicate shortcuts for the same url
|
||||||
|
// (at least, i think that's how it works)
|
||||||
|
userActivity!.persistentIdentifier = url.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -56,11 +62,10 @@ class BrowserWebViewController: UIViewController {
|
||||||
|
|
||||||
configureRenderer()
|
configureRenderer()
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
webView = WKWebView()
|
webView = WKWebView()
|
||||||
webView.backgroundColor = .systemBackground
|
webView.backgroundColor = .systemBackground
|
||||||
webView.isOpaque = false
|
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
webView.uiDelegate = 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
|
// 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
|
||||||
|
@ -68,6 +73,7 @@ class BrowserWebViewController: UIViewController {
|
||||||
// this doesn't default to .default :S
|
// this doesn't default to .default :S
|
||||||
webView.scrollView.indicatorStyle = .default
|
webView.scrollView.indicatorStyle = .default
|
||||||
webView.scrollView.keyboardDismissMode = .interactive
|
webView.scrollView.keyboardDismissMode = .interactive
|
||||||
|
configureWebViewRefreshControl()
|
||||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(webView)
|
view.addSubview(webView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -148,14 +154,28 @@ class BrowserWebViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureWebViewRefreshControl(pullToRefreshEnabled: Bool = Preferences.shared.pullToRefreshEnabled) {
|
||||||
|
if pullToRefreshEnabled {
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
refreshControl.addTarget(navigator, action: #selector(NavigationManager.reload), for: .valueChanged)
|
||||||
|
webView.scrollView.refreshControl = refreshControl
|
||||||
|
} else {
|
||||||
|
webView.scrollView.refreshControl = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reload() {
|
func reload() {
|
||||||
loaded = false
|
loaded = false
|
||||||
loadedFallback = false
|
loadedFallback = false
|
||||||
document = nil
|
document = nil
|
||||||
loadDocument()
|
loadDocument() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.webView.scrollView.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadDocument() {
|
private func loadDocument(completion: (() -> Void)? = nil) {
|
||||||
guard !loaded else { return }
|
guard !loaded else { return }
|
||||||
|
|
||||||
webView.isHidden = true
|
webView.isHidden = true
|
||||||
|
@ -186,7 +206,8 @@ class BrowserWebViewController: UIViewController {
|
||||||
} else if response.status.isSuccess {
|
} else if response.status.isSuccess {
|
||||||
if response.mimeType == "text/gemini",
|
if response.mimeType == "text/gemini",
|
||||||
let text = response.bodyText {
|
let text = response.bodyText {
|
||||||
self.renderDocument(GeminiParser.parse(text: text, baseURL: url))
|
let doc = GeminiParser.parse(text: text, baseURL: url)
|
||||||
|
self.renderDocument(doc)
|
||||||
} else {
|
} else {
|
||||||
self.renderFallback(response: response)
|
self.renderFallback(response: response)
|
||||||
}
|
}
|
||||||
|
@ -200,6 +221,11 @@ class BrowserWebViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
task!.attribution = .user
|
||||||
}
|
}
|
||||||
task!.resume()
|
task!.resume()
|
||||||
}
|
}
|
||||||
|
@ -230,6 +256,15 @@ class BrowserWebViewController: UIViewController {
|
||||||
private func renderDocument(_ doc: Document) {
|
private func renderDocument(_ doc: Document) {
|
||||||
self.document = doc
|
self.document = doc
|
||||||
|
|
||||||
|
if navigator.currentURL == doc.url {
|
||||||
|
navigator.setTitleForCurrentURL(doc.title)
|
||||||
|
}
|
||||||
|
if let title = doc.title {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.userActivity!.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let html = BrowserWebViewController.preamble + renderer.renderDocumentToHTML(doc) + BrowserWebViewController.postamble
|
let html = BrowserWebViewController.preamble + renderer.renderDocumentToHTML(doc) + BrowserWebViewController.postamble
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.webView.isHidden = false
|
self.webView.isHidden = false
|
||||||
|
@ -355,7 +390,18 @@ extension BrowserWebViewController: WKUIDelegate {
|
||||||
return SFSafariViewController(url: url)
|
return SFSafariViewController(url: url)
|
||||||
}
|
}
|
||||||
} actionProvider: { (_) in
|
} actionProvider: { (_) in
|
||||||
return nil
|
guard #available(iOS 15.0, *),
|
||||||
|
url.scheme == "gemini" else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UIMenu(children: [
|
||||||
|
UIWindowScene.ActivationAction({ (_) in
|
||||||
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
|
// automatic presents in the prominent style even when a fullscreen window is the only existing one
|
||||||
|
options.preferredPresentationStyle = .standard
|
||||||
|
return UIWindowScene.ActivationConfiguration(userActivity: NSUserActivity(geminiURL: url), options: options, preview: nil)
|
||||||
|
})
|
||||||
|
])
|
||||||
}
|
}
|
||||||
completionHandler(config)
|
completionHandler(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Rocketeer</string>
|
<string>Rocketeer</string>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict/>
|
||||||
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
@ -33,12 +37,29 @@
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>INIntentsSupported</key>
|
||||||
|
<array>
|
||||||
|
<string>OpenURLIntent</string>
|
||||||
|
<string>OpenHomepageIntent</string>
|
||||||
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>$(PRODUCE_BUNDLE_IDENTIFIER).activity.browse</string>
|
||||||
|
<string>$(PRODUCE_BUNDLE_IDENTIFIER).activity.homepage</string>
|
||||||
|
<string>GemtextToHTMLIntent</string>
|
||||||
|
<string>GemtextToMarkdownIntent</string>
|
||||||
|
<string>GetBodyIntent</string>
|
||||||
|
<string>GetMetaIntent</string>
|
||||||
|
<string>GetStatusIntent</string>
|
||||||
|
<string>MakeRequestIntent</string>
|
||||||
|
<string>OpenURLIntent</string>
|
||||||
|
</array>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
<false/>
|
<true/>
|
||||||
<key>UISceneConfigurations</key>
|
<key>UISceneConfigurations</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
@ -52,6 +73,17 @@
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>UIApplicationShortcutItems</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationShortcutItemIconType</key>
|
||||||
|
<string>UIApplicationShortcutIconTypeHome</string>
|
||||||
|
<key>UIApplicationShortcutItemTitle</key>
|
||||||
|
<string>Homepage</string>
|
||||||
|
<key>UIApplicationShortcutItemType</key>
|
||||||
|
<string>home</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// OpenHomepageIntentHandler.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/2/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class OpenHomepageIntentHandler: NSObject, OpenHomepageIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: OpenHomepageIntent, completion: @escaping (OpenHomepageIntentResponse) -> Void) {
|
||||||
|
completion(OpenHomepageIntentResponse(code: .continueInApp, userActivity: .homepage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// OpenURLIntentHandler.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class OpenURLIntentHandler: NSObject, OpenURLIntentHandling {
|
||||||
|
|
||||||
|
func resolveUrl(for intent: OpenURLIntent, with completion: @escaping (INURLResolutionResult) -> Void) {
|
||||||
|
guard let url = intent.url,
|
||||||
|
url.scheme?.lowercased() == "gemini" else {
|
||||||
|
completion(.unsupported())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.success(with: url))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(intent: OpenURLIntent, completion: @escaping (OpenURLIntentResponse) -> Void) {
|
||||||
|
guard let url = intent.url,
|
||||||
|
url.scheme?.lowercased() == "gemini" else {
|
||||||
|
completion(OpenURLIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(OpenURLIntentResponse(code: .continueInApp, userActivity: NSUserActivity(geminiURL: url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// UserActivities.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/28/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import BrowserCore
|
||||||
|
|
||||||
|
private let browseType = "space.vaccor.Gemini.activity.browse"
|
||||||
|
private let homepageType = "space.vaccor.Gemini.activity.homepage"
|
||||||
|
private let encoder = PropertyListEncoder()
|
||||||
|
private let decoder = PropertyListDecoder()
|
||||||
|
|
||||||
|
extension NSUserActivity {
|
||||||
|
static func homepage() -> NSUserActivity {
|
||||||
|
return NSUserActivity(activityType: homepageType)
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(geminiURL url: URL) {
|
||||||
|
self.init(activityType: browseType)
|
||||||
|
self.userInfo = [
|
||||||
|
"url": url,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(navigationManager manager: NavigationManager) {
|
||||||
|
self.init(activityType: browseType)
|
||||||
|
self.userInfo = [
|
||||||
|
"url": manager.currentURL,
|
||||||
|
]
|
||||||
|
if let data = try? encoder.encode(manager) {
|
||||||
|
self.userInfo!["manager"] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var navigationManager: NavigationManager? {
|
||||||
|
guard activityType == browseType,
|
||||||
|
let data = userInfo?["manager"] as? Data,
|
||||||
|
let manager = try? decoder.decode(NavigationManager.self, from: data) else { return nil }
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
var geminiURL: URL? {
|
||||||
|
guard activityType == browseType,
|
||||||
|
let url = userInfo?["url"] as? URL else { return nil }
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
var isHomepage: Bool {
|
||||||
|
return activityType == homepageType
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,17 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
useReaderMode = try container.decode(Bool.self, forKey: .useReaderMode)
|
useReaderMode = try container.decode(Bool.self, forKey: .useReaderMode)
|
||||||
|
|
||||||
|
if let stored = try container.decodeIfPresent(Bool.self, forKey: .hideToolbarsWhenScrolling) {
|
||||||
|
hideToolbarsWhenScrolling = stored
|
||||||
|
}
|
||||||
|
if let stored = try container.decodeIfPresent(Bool.self, forKey: .pullToRefreshEnabled) {
|
||||||
|
pullToRefreshEnabled = stored
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stored = try container.decodeIfPresent([ToolbarItem].self, forKey: .toolbar) {
|
||||||
|
toolbar = stored
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
@ -57,6 +68,11 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
try container.encode(useReaderMode, forKey: .useReaderMode)
|
try container.encode(useReaderMode, forKey: .useReaderMode)
|
||||||
|
|
||||||
|
try container.encode(hideToolbarsWhenScrolling, forKey: .hideToolbarsWhenScrolling)
|
||||||
|
try container.encode(pullToRefreshEnabled, forKey: .pullToRefreshEnabled)
|
||||||
|
|
||||||
|
try container.encode(toolbar, forKey: .toolbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var homepage = AppDelegate.defaultHomepage
|
@Published var homepage = AppDelegate.defaultHomepage
|
||||||
|
@ -67,6 +83,11 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var useInAppSafari = false
|
@Published var useInAppSafari = false
|
||||||
@Published var useReaderMode = false
|
@Published var useReaderMode = false
|
||||||
|
|
||||||
|
@Published var hideToolbarsWhenScrolling = true
|
||||||
|
@Published var pullToRefreshEnabled = true
|
||||||
|
|
||||||
|
@Published var toolbar: [ToolbarItem] = [.back, .forward, .reload, .tableOfContents, .share, .preferences]
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case homepage
|
case homepage
|
||||||
|
|
||||||
|
@ -75,6 +96,11 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
case useReaderMode
|
case useReaderMode
|
||||||
|
|
||||||
|
case hideToolbarsWhenScrolling
|
||||||
|
case pullToRefreshEnabled
|
||||||
|
|
||||||
|
case toolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,9 @@ import SwiftUI
|
||||||
struct PreferencesView: View {
|
struct PreferencesView: View {
|
||||||
let dismiss: () -> Void
|
let dismiss: () -> Void
|
||||||
|
|
||||||
@ObservedObject var preferences: Preferences = .shared
|
@ObservedObject private var preferences: Preferences = .shared
|
||||||
|
// todo: this should really be a @StateObject on ToolbarPrefView
|
||||||
|
@State private var toolbarViewModel = CustomizeToolbarViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
@ -20,6 +22,8 @@ struct PreferencesView: View {
|
||||||
appearanceSection
|
appearanceSection
|
||||||
|
|
||||||
safariSection
|
safariSection
|
||||||
|
|
||||||
|
behaviorSection
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Preferences")
|
.navigationBarTitle("Preferences")
|
||||||
.insetOrGroupedListStyle()
|
.insetOrGroupedListStyle()
|
||||||
|
@ -69,6 +73,18 @@ struct PreferencesView: View {
|
||||||
.disabled(!preferences.useInAppSafari)
|
.disabled(!preferences.useInAppSafari)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var behaviorSection: some View {
|
||||||
|
Section(header: Text("Behavior")) {
|
||||||
|
Toggle("Pull to Refresh", isOn: $preferences.pullToRefreshEnabled)
|
||||||
|
Toggle("Hide Toolbars When Scrolling", isOn: $preferences.hideToolbarsWhenScrolling)
|
||||||
|
NavigationLink {
|
||||||
|
ToolbarPrefView(model: toolbarViewModel)
|
||||||
|
} label: {
|
||||||
|
Text("Customize Toolbar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension View {
|
fileprivate extension View {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
||||||
import BrowserCore
|
import BrowserCore
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Combine
|
import Combine
|
||||||
|
import Intents
|
||||||
|
|
||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
@ -23,20 +24,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||||
|
|
||||||
let initialURL: URL
|
|
||||||
|
|
||||||
if let context = connectionOptions.urlContexts.first {
|
navigationManager = createNavigationManager(for: session, with: connectionOptions)
|
||||||
initialURL = context.url
|
|
||||||
} else {
|
|
||||||
if ProcessInfo.processInfo.environment.keys.contains("DEFAULT_URL") {
|
|
||||||
initialURL = URL(string: ProcessInfo.processInfo.environment["DEFAULT_URL"]!)!
|
|
||||||
} else {
|
|
||||||
initialURL = Preferences.shared.homepage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationManager = NavigationManager(url: initialURL)
|
|
||||||
navigationManager.delegate = self
|
navigationManager.delegate = self
|
||||||
|
|
||||||
// Create the SwiftUI view that provides the window contents.
|
// Create the SwiftUI view that provides the window contents.
|
||||||
|
@ -54,7 +43,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
Preferences.shared.$theme
|
Preferences.shared.$theme
|
||||||
.sink { (newStyle) in
|
.sink { [unowned self] (newStyle) in
|
||||||
self.window!.overrideUserInterfaceStyle = newStyle
|
self.window!.overrideUserInterfaceStyle = newStyle
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
@ -65,6 +54,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
navigationManager.changeURL(context.url)
|
navigationManager.changeURL(context.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||||
|
if userActivity.isHomepage {
|
||||||
|
navigationManager.changeURL(Preferences.shared.homepage)
|
||||||
|
} else if let url = userActivity.geminiURL {
|
||||||
|
navigationManager.changeURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||||
|
if shortcutItem.type == "home" {
|
||||||
|
navigationManager.changeURL(Preferences.shared.homepage)
|
||||||
|
completionHandler(true)
|
||||||
|
} else {
|
||||||
|
completionHandler(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sceneDidDisconnect(_ scene: UIScene) {
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
// Called as the scene is being released by the system.
|
// Called as the scene is being released by the system.
|
||||||
|
@ -93,6 +99,58 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
// to restore the scene back to its current state.
|
// to restore the scene back to its current state.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||||
|
return NSUserActivity(navigationManager: navigationManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createNavigationManager(for session: UISceneSession, with connectionOptions: UIScene.ConnectionOptions) -> NavigationManager {
|
||||||
|
// try to restore the existing nav manager if there is one
|
||||||
|
if let manager = session.stateRestorationActivity?.navigationManager {
|
||||||
|
// if there's a user activity with a gemini url (e.g., from OpenURLIntentHandler),
|
||||||
|
// navigate the existing manager to that URL
|
||||||
|
if let activity = connectionOptions.userActivities.first {
|
||||||
|
if let newURL = activity.geminiURL {
|
||||||
|
manager.changeURL(newURL)
|
||||||
|
} else if activity.isHomepage {
|
||||||
|
manager.changeURL(Preferences.shared.homepage)
|
||||||
|
}
|
||||||
|
} else if connectionOptions.shortcutItem?.type == "home" {
|
||||||
|
manager.changeURL(Preferences.shared.homepage)
|
||||||
|
}
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, work out the initial URL and create a new manager
|
||||||
|
|
||||||
|
var initialURL: URL? = nil
|
||||||
|
|
||||||
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
|
if let manager = activity.navigationManager {
|
||||||
|
return manager
|
||||||
|
} else if let url = activity.geminiURL {
|
||||||
|
initialURL = url
|
||||||
|
} else if activity.isHomepage {
|
||||||
|
initialURL = Preferences.shared.homepage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if initialURL == nil {
|
||||||
|
initialURL = connectionOptions.urlContexts.first?.url
|
||||||
|
}
|
||||||
|
|
||||||
|
if initialURL == nil || connectionOptions.shortcutItem?.type == "home" {
|
||||||
|
initialURL = Preferences.shared.homepage
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if ProcessInfo.processInfo.environment.keys.contains("DEFAULT_URL") {
|
||||||
|
initialURL = URL(string: ProcessInfo.processInfo.environment["DEFAULT_URL"]!)!
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return NavigationManager(url: initialURL!)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
//
|
||||||
|
// ToolbarItem.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/10/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ToolbarItem: String, Codable, CaseIterable {
|
||||||
|
case back
|
||||||
|
case forward
|
||||||
|
case reload
|
||||||
|
case share
|
||||||
|
case home
|
||||||
|
case tableOfContents
|
||||||
|
case preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ToolbarItem {
|
||||||
|
var imageName: String {
|
||||||
|
switch self {
|
||||||
|
case .back:
|
||||||
|
return "arrow.left"
|
||||||
|
case .forward:
|
||||||
|
return "arrow.right"
|
||||||
|
case .reload:
|
||||||
|
return "arrow.clockwise"
|
||||||
|
case .tableOfContents:
|
||||||
|
return "list.bullet.indent"
|
||||||
|
case .share:
|
||||||
|
return "square.and.arrow.up"
|
||||||
|
case .preferences:
|
||||||
|
return "gear"
|
||||||
|
case .home:
|
||||||
|
return "house"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .back:
|
||||||
|
return "Go Back"
|
||||||
|
case .forward:
|
||||||
|
return "Go Forward"
|
||||||
|
case .reload:
|
||||||
|
return "Reload"
|
||||||
|
case .tableOfContents:
|
||||||
|
return "Table of Contents"
|
||||||
|
case .share:
|
||||||
|
return "Share"
|
||||||
|
case .preferences:
|
||||||
|
return "Preferences"
|
||||||
|
case .home:
|
||||||
|
return "Home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,336 @@
|
||||||
|
//
|
||||||
|
// ToolbarPrefView.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/9/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
let toolbarItemType = "space.vaccor.Gemini.toolbar-item"
|
||||||
|
|
||||||
|
struct ToolbarPrefView: View {
|
||||||
|
// todo: this should really be a @StateObject and shouldn't be passed in from the outside, but that requires iOS 14
|
||||||
|
@ObservedObject private var model: CustomizeToolbarViewModel
|
||||||
|
|
||||||
|
init(model: CustomizeToolbarViewModel) {
|
||||||
|
self.model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("Drag and drop items to change your toolbar")
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
separator
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ForEach(model.items) { (toolbarItem) in
|
||||||
|
Image(systemName: toolbarItem.imageName)
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.foregroundColor(toolbarItem.isPlaceholder ? .gray : .blue)
|
||||||
|
.overlay(GeometryReader { (proxy) in
|
||||||
|
Color.clear
|
||||||
|
.preference(key: FloatPrefKey.self, value: proxy.size.width)
|
||||||
|
.onPreferenceChange(FloatPrefKey.self) { newValue in
|
||||||
|
model.itemWidths[toolbarItem] = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onDrag {
|
||||||
|
guard !toolbarItem.isPlaceholder else {
|
||||||
|
return NSItemProvider()
|
||||||
|
}
|
||||||
|
model.beginDragging(item: toolbarItem.item)
|
||||||
|
return NSItemProvider(item: nil, typeIdentifier: toolbarItemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.animation(.default, value: model.items)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.overlay(GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.preference(key: FloatPrefKey.self, value: proxy.size.width)
|
||||||
|
.onPreferenceChange(FloatPrefKey.self) { newValue in
|
||||||
|
model.totalWidth = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
// this onDrop is needed because the one on the .background doesn't fire when the drag item is held above one of the items themselves
|
||||||
|
.onDrop(of: [toolbarItemType], delegate: model)
|
||||||
|
.background(
|
||||||
|
Color(UIColor.systemBackground)
|
||||||
|
// this onDrop is needed because the one on the ForEach doesn't fire when is the drag item held in between two of the ForEach items (i.e., above one of the spacers)
|
||||||
|
.onDrop(of: [toolbarItemType], delegate: model)
|
||||||
|
)
|
||||||
|
separator
|
||||||
|
|
||||||
|
Spacer(minLength: 16)
|
||||||
|
|
||||||
|
VStack(alignment: .textLeading, spacing: 8) {
|
||||||
|
ForEach(ToolbarItem.allCases, id: \.rawValue) { (item) in
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: item.imageName)
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.foregroundColor(canAdd(item: item) ? .blue : .gray)
|
||||||
|
.disabled(!canAdd(item: item))
|
||||||
|
.animation(.default, value: canAdd(item: item))
|
||||||
|
.onDrag {
|
||||||
|
model.beginDragging(item: item)
|
||||||
|
return NSItemProvider(item: nil, typeIdentifier: toolbarItemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(item.displayName)
|
||||||
|
.alignmentGuide(.textLeading) { d in d[HorizontalAlignment.leading] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
Color(.secondarySystemBackground)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
// this stupid onDrop is necessary because sometimes when quickly dragging an item out of the toolbar, dropExited is never called on the main delegate
|
||||||
|
// so the BackgroundDropDelegate ensures that any placeholders are removed
|
||||||
|
.onDrop(of: [toolbarItemType], delegate: BackgroundDropDelegate(toolbarDropDelegate: model))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var separator: some View {
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 0.5)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func canAdd(item: ToolbarItem) -> Bool {
|
||||||
|
return !model.items.contains(.item(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HorizontalAlignment {
|
||||||
|
struct TextLeadingAlignment: AlignmentID {
|
||||||
|
static func defaultValue(in context: ViewDimensions) -> CGFloat {
|
||||||
|
return context[HorizontalAlignment.center]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The alignment used for aligning the leading edges of all the text in the toolbar item list.
|
||||||
|
static let textLeading = HorizontalAlignment(TextLeadingAlignment.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CustomizeToolbarItem: Identifiable, Equatable, Hashable {
|
||||||
|
case item(ToolbarItem)
|
||||||
|
case placeholder(ToolbarItem)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .item(item):
|
||||||
|
return item.rawValue
|
||||||
|
case let .placeholder(item):
|
||||||
|
return "placeholder_\(item.rawValue)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageName: String {
|
||||||
|
switch self {
|
||||||
|
case .item(let item), .placeholder(let item):
|
||||||
|
return item.imageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPlaceholder: Bool {
|
||||||
|
switch self {
|
||||||
|
case .item(_):
|
||||||
|
return false
|
||||||
|
case .placeholder(_):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item: ToolbarItem {
|
||||||
|
switch self {
|
||||||
|
case .item(let item), .placeholder(let item):
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomizeToolbarViewModel: ObservableObject, DropDelegate {
|
||||||
|
|
||||||
|
@Published fileprivate var items: [CustomizeToolbarItem]
|
||||||
|
|
||||||
|
fileprivate var totalWidth: CGFloat = 0
|
||||||
|
fileprivate var itemWidths: [CustomizeToolbarItem: CGFloat] = [:]
|
||||||
|
|
||||||
|
fileprivate var draggedItem: ToolbarItem?
|
||||||
|
private var isDraggingExisting = false
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
items = Preferences.shared.toolbar.map { .item($0) }
|
||||||
|
|
||||||
|
Preferences.shared.$toolbar
|
||||||
|
.sink { [unowned self] (newValue) in
|
||||||
|
self.items = newValue.map { .item($0) }
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func beginDragging(item: ToolbarItem) {
|
||||||
|
draggedItem = item
|
||||||
|
if let index = items.firstIndex(where: { $0.item == item }) {
|
||||||
|
items[index] = .placeholder(item)
|
||||||
|
isDraggingExisting = true
|
||||||
|
} else {
|
||||||
|
isDraggingExisting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func cleanupCustomizeItems() {
|
||||||
|
items = items.compactMap {
|
||||||
|
switch $0 {
|
||||||
|
case .item(_):
|
||||||
|
return $0
|
||||||
|
case .placeholder(let item):
|
||||||
|
if canRemove(item: item) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return .item(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func canRemove(item: ToolbarItem) -> Bool {
|
||||||
|
// preferences can't be removed because the user would lose access to the toolbar preferences
|
||||||
|
return item != .preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePreferences() {
|
||||||
|
Preferences.shared.toolbar = self.items.compactMap {
|
||||||
|
switch $0 {
|
||||||
|
case let .item(item):
|
||||||
|
return item
|
||||||
|
case .placeholder:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func proposedDropIndex(info: DropInfo) -> Int {
|
||||||
|
let totalItemWidth = itemWidths.filter { items.contains($0.key) }.reduce(0, { $0 + $1.value })
|
||||||
|
let remainingWidth = totalWidth - totalItemWidth
|
||||||
|
let spacerWidth = remainingWidth / CGFloat(items.count + 1)
|
||||||
|
var accumulatedWidth = spacerWidth
|
||||||
|
for (index, item) in items.enumerated() {
|
||||||
|
let itemWidth = itemWidths[item]!
|
||||||
|
if info.location.x < accumulatedWidth + itemWidth / 2 {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
accumulatedWidth += itemWidth + spacerWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items.count
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: DropDelegate
|
||||||
|
|
||||||
|
func validateDrop(info: DropInfo) -> Bool {
|
||||||
|
return draggedItem != nil && (isDraggingExisting || items.filter { !$0.isPlaceholder }.count < 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dropEntered(info: DropInfo) {
|
||||||
|
items.removeAll(where: \.isPlaceholder)
|
||||||
|
// if we just exited the other onDrop handler, we need to re-add the placeholder
|
||||||
|
if !items.contains(where: { $0.item == draggedItem! }) {
|
||||||
|
items.insert(.placeholder(draggedItem!), at: proposedDropIndex(info: info))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dropUpdated(info: DropInfo) -> DropProposal? {
|
||||||
|
if isDraggingExisting {
|
||||||
|
let index = items.firstIndex { $0.item == draggedItem! }!
|
||||||
|
items[index] = .placeholder(draggedItem!)
|
||||||
|
items.move(fromOffsets: IndexSet(integer: index), toOffset: proposedDropIndex(info: info))
|
||||||
|
|
||||||
|
return DropProposal(operation: .move)
|
||||||
|
} else {
|
||||||
|
items.removeAll(where: \.isPlaceholder)
|
||||||
|
|
||||||
|
let index = proposedDropIndex(info: info)
|
||||||
|
items.insert(.placeholder(draggedItem!), at: index)
|
||||||
|
|
||||||
|
return DropProposal(operation: .copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dropExited(info: DropInfo) {
|
||||||
|
cleanupCustomizeItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
func performDrop(info: DropInfo) -> Bool {
|
||||||
|
let placeholderIndex = items.firstIndex(where: \.isPlaceholder)!
|
||||||
|
items[placeholderIndex] = .item(draggedItem!)
|
||||||
|
self.updatePreferences()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct BackgroundDropDelegate: DropDelegate {
|
||||||
|
let toolbarDropDelegate: CustomizeToolbarViewModel
|
||||||
|
|
||||||
|
func dropUpdated(info: DropInfo) -> DropProposal? {
|
||||||
|
if toolbarDropDelegate.draggedItem != nil {
|
||||||
|
toolbarDropDelegate.cleanupCustomizeItems()
|
||||||
|
}
|
||||||
|
return DropProposal(operation: .move)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performDrop(info: DropInfo) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FloatPrefKey: PreferenceKey {
|
||||||
|
static var defaultValue: CGFloat = 0
|
||||||
|
|
||||||
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
|
value = nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
@ViewBuilder
|
||||||
|
func onDragWithPreviewIfPossible<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
self.onDrag(data, preview: preview)
|
||||||
|
} else {
|
||||||
|
self.onDrag(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func onDragIf(condition: () -> Bool, data: @escaping () -> NSItemProvider) -> some View {
|
||||||
|
if condition() {
|
||||||
|
self.onDrag(data)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ToolbarPrefView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ToolbarPrefView(model: CustomizeToolbarViewModel())
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,12 +18,8 @@ class ToolbarView: UIView {
|
||||||
var showPreferences: (() -> Void)?
|
var showPreferences: (() -> Void)?
|
||||||
|
|
||||||
private var border: UIView!
|
private var border: UIView!
|
||||||
private var backButton: UIButton!
|
private var buttonsStack: UIStackView!
|
||||||
private var forwardsButton: UIButton!
|
private var toolbarButtons: [ToolbarItem: UIButton] = [:]
|
||||||
private var reloadButton: UIButton!
|
|
||||||
private var tableOfContentsButton: UIButton!
|
|
||||||
private var shareButton: UIButton!
|
|
||||||
private var prefsButton: UIButton!
|
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
|
@ -45,85 +41,35 @@ class ToolbarView: UIView {
|
||||||
border.heightAnchor.constraint(equalToConstant: 1),
|
border.heightAnchor.constraint(equalToConstant: 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24)
|
buttonsStack = UIStackView()
|
||||||
|
buttonsStack.axis = .horizontal
|
||||||
backButton = UIButton()
|
buttonsStack.distribution = .fillEqually
|
||||||
backButton.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside)
|
buttonsStack.alignment = .fill
|
||||||
backButton.isEnabled = navigator.backStack.count > 0
|
buttonsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
backButton.setImage(UIImage(systemName: "arrow.left", withConfiguration: symbolConfig), for: .normal)
|
addSubview(buttonsStack)
|
||||||
backButton.accessibilityLabel = "Back"
|
let safeAreaConstraint = buttonsStack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
|
||||||
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
|
safeAreaConstraint.priority = .defaultHigh
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
buttonsStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
buttonsStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
stack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
buttonsStack.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||||
safeAreaConstraint,
|
safeAreaConstraint,
|
||||||
stack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
|
buttonsStack.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8)
|
||||||
])
|
])
|
||||||
|
|
||||||
updateNavigationButtons()
|
updateNavigationButtons()
|
||||||
|
|
||||||
navigator.navigationOperation
|
navigator.navigationOperation
|
||||||
.sink { (_) in
|
.sink { [unowned self] (_) in
|
||||||
self.updateNavigationButtons()
|
self.updateNavigationButtons()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Preferences.shared.$toolbar
|
||||||
|
.sink { [unowned self] (newValue) in
|
||||||
|
self.createToolbarButtons(newValue)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -136,36 +82,100 @@ class ToolbarView: UIView {
|
||||||
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
border.backgroundColor = UIColor(white: traitCollection.userInterfaceStyle == .dark ? 0.25 : 0.75, alpha: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func urlForDisplay(_ url: URL) -> String {
|
private func createToolbarButtons(_ items: [ToolbarItem] = Preferences.shared.toolbar) {
|
||||||
var str = url.host!
|
toolbarButtons = [:]
|
||||||
if let port = url.port,
|
buttonsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
url.scheme != "gemini" || port != 1965 {
|
|
||||||
str += ":\(port)"
|
for item in items {
|
||||||
|
let button = createButton(item)
|
||||||
|
toolbarButtons[item] = button
|
||||||
|
buttonsStack.addArrangedSubview(button)
|
||||||
}
|
}
|
||||||
str += url.path
|
|
||||||
return str
|
updateNavigationButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createButton(_ item: ToolbarItem) -> UIButton {
|
||||||
|
let button = UIButton()
|
||||||
|
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 24)
|
||||||
|
button.setImage(UIImage(systemName: item.imageName, withConfiguration: symbolConfig)!, for: .normal)
|
||||||
|
button.accessibilityLabel = item.displayName
|
||||||
|
button.isPointerInteractionEnabled = true
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .back:
|
||||||
|
button.addTarget(navigator, action: #selector(NavigationManager.goBack), for: .touchUpInside)
|
||||||
|
// fallback for when UIButton.menu isn't available
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
} else {
|
||||||
|
button.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .forward:
|
||||||
|
button.addTarget(navigator, action: #selector(NavigationManager.goForward), for: .touchUpInside)
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
} else {
|
||||||
|
button.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .reload:
|
||||||
|
button.addTarget(navigator, action: #selector(NavigationManager.reload), for: .touchUpInside)
|
||||||
|
|
||||||
|
case .share:
|
||||||
|
button.addTarget(self, action: #selector(sharePressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
case .home:
|
||||||
|
button.addTarget(self, action: #selector(homePressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
case .tableOfContents:
|
||||||
|
button.addTarget(self, action: #selector(tableOfContentsPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
case .preferences:
|
||||||
|
button.addTarget(self, action: #selector(prefsPressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNavigationButtons() {
|
private func updateNavigationButtons() {
|
||||||
backButton.isEnabled = navigator.backStack.count > 0
|
if let backButton = toolbarButtons[.back] {
|
||||||
forwardsButton.isEnabled = navigator.forwardStack.count > 0
|
backButton.isEnabled = navigator.backStack.count > 0
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, entry) -> UIAction in
|
||||||
|
let backCount = min(5, navigator.backStack.count) - index
|
||||||
|
if #available(iOS 15.0, *),
|
||||||
|
let title = entry.title {
|
||||||
|
return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
|
||||||
|
self.navigator.back(count: backCount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
|
||||||
|
self.navigator.back(count: backCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backButton.menu = UIMenu(children: back)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
if let forwardsButton = toolbarButtons[.forward] {
|
||||||
let back = navigator.backStack.suffix(5).enumerated().reversed().map { (index, url) -> UIAction in
|
forwardsButton.isEnabled = navigator.forwardStack.count > 0
|
||||||
let backCount = min(5, navigator.backStack.count) - index
|
if #available(iOS 14.0, *) {
|
||||||
return UIAction(title: urlForDisplay(url)) { (_) in
|
let forward = navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in
|
||||||
self.navigator.back(count: backCount)
|
let forwardCount = index + 1
|
||||||
|
if #available(iOS 15.0, *),
|
||||||
|
let title = entry.title {
|
||||||
|
return UIAction(title: title, subtitle: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
|
||||||
|
self.navigator.forward(count: forwardCount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { [unowned self] (_) in
|
||||||
|
self.navigator.forward(count: forwardCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
forwardsButton.menu = UIMenu(children: forward)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,31 +184,38 @@ class ToolbarView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sharePressed() {
|
@objc private func sharePressed() {
|
||||||
showShareSheet?(shareButton)
|
showShareSheet?(toolbarButtons[.share]!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func prefsPressed() {
|
@objc private func prefsPressed() {
|
||||||
showPreferences?()
|
showPreferences?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func homePressed() {
|
||||||
|
navigator.changeURL(Preferences.shared.homepage)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToolbarView: UIContextMenuInteractionDelegate {
|
extension ToolbarView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
if interaction.view == backButton {
|
// this path is only used on <iOS 14, on >=iOS 14, we don't create a UIContextMenuInteraction
|
||||||
|
if let backButton = toolbarButtons[.back],
|
||||||
|
interaction.view == backButton {
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
||||||
let children = self.navigator.backStack.suffix(5).enumerated().map { (index, url) in
|
let children = self.navigator.backStack.suffix(5).enumerated().map { (index, entry) in
|
||||||
UIAction(title: self.urlForDisplay(url)) { (_) in
|
UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { (_) in
|
||||||
self.navigator.back(count: min(5, self.navigator.backStack.count) - index)
|
self.navigator.back(count: min(5, self.navigator.backStack.count) - index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
|
||||||
}
|
}
|
||||||
} else if interaction.view == forwardsButton {
|
} else if let forwardsButton = toolbarButtons[.forward],
|
||||||
|
interaction.view == forwardsButton {
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: { nil }) { (_) -> UIMenu? in
|
||||||
let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, url) -> UIAction in
|
let children = self.navigator.forwardStack.prefix(5).enumerated().map { (index, entry) -> UIAction in
|
||||||
let forwardCount = index + 1
|
let forwardCount = index + 1
|
||||||
return UIAction(title: self.urlForDisplay(url)) { (_) in
|
return UIAction(title: BrowserHelper.urlForDisplay(entry.url)) { (_) in
|
||||||
self.navigator.forward(count: forwardCount)
|
self.navigator.forward(count: forwardCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,20 +37,43 @@
|
||||||
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 */; };
|
||||||
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6376A6F26DDAF65005AD89C /* URIFixup.swift */; };
|
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6376A6F26DDAF65005AD89C /* URIFixup.swift */; };
|
||||||
|
D640A2322711DC7700177E85 /* ToolbarPrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D640A2312711DC7700177E85 /* ToolbarPrefView.swift */; };
|
||||||
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40A267996FF004E32B1 /* ActivityItemSource.swift */; };
|
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40A267996FF004E32B1 /* ActivityItemSource.swift */; };
|
||||||
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; };
|
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; };
|
||||||
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */; };
|
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.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 */; };
|
D664E4FA2713DF72005BAF55 /* ToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664E4F92713DF72005BAF55 /* ToolbarItem.swift */; };
|
||||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
||||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
||||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F649258C17F3003A0A73 /* SymbolCache.swift */; };
|
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F649258C17F3003A0A73 /* SymbolCache.swift */; };
|
||||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F659258C2256003A0A73 /* BrowserNavigationController.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 */; };
|
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F662258C2479003A0A73 /* UIViewController+Children.swift */; };
|
||||||
|
D68C1E1927055E09002D642B /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E1827055E09002D642B /* Intents.intentdefinition */; };
|
||||||
|
D68C1E1E270605A7002D642B /* BrowserHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E1D270605A7002D642B /* BrowserHelper.swift */; };
|
||||||
|
D68C1E25270614F9002D642B /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D68C1E24270614F9002D642B /* Intents.framework */; platformFilter = maccatalyst; };
|
||||||
|
D68C1E28270614F9002D642B /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E27270614F9002D642B /* IntentHandler.swift */; };
|
||||||
|
D68C1E2C270614F9002D642B /* GeminiIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D68C1E23270614F9002D642B /* GeminiIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
D68C1E312706150E002D642B /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E1827055E09002D642B /* Intents.intentdefinition */; };
|
||||||
|
D68C1E3227061557002D642B /* OpenURLIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E1B27055EB0002D642B /* OpenURLIntentHandler.swift */; };
|
||||||
|
D68C1E3327061568002D642B /* UserActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1DFF2703EA13002D642B /* UserActivities.swift */; };
|
||||||
|
D68C1E34270615D3002D642B /* OpenURLIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E1B27055EB0002D642B /* OpenURLIntentHandler.swift */; };
|
||||||
|
D68C1E35270615D3002D642B /* UserActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1DFF2703EA13002D642B /* UserActivities.swift */; };
|
||||||
|
D68C1E372706215A002D642B /* MakeRequestIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E362706215A002D642B /* MakeRequestIntentHandler.swift */; };
|
||||||
|
D68C1E392707A1E5002D642B /* GetBodyIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E382707A1E5002D642B /* GetBodyIntentHandler.swift */; };
|
||||||
|
D68C1E3B2707BF25002D642B /* GetStatusIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E3A2707BF25002D642B /* GetStatusIntentHandler.swift */; };
|
||||||
|
D68C1E3D2707C002002D642B /* GetMetaIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E3C2707C002002D642B /* GetMetaIntentHandler.swift */; };
|
||||||
|
D68C1E3F2707C3B2002D642B /* GemtextToHTMLIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E3E2707C3B2002D642B /* GemtextToHTMLIntentHandler.swift */; };
|
||||||
|
D68C1E412707CB03002D642B /* GemtextToMarkdownIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E402707CB03002D642B /* GemtextToMarkdownIntentHandler.swift */; };
|
||||||
|
D68C1E482708A958002D642B /* GeminiMarkdownRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E422707CB69002D642B /* GeminiMarkdownRenderer.swift */; };
|
||||||
|
D68C1E492708A958002D642B /* GeminiHTMLRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */; };
|
||||||
|
D68C1E4A2708A95D002D642B /* GeminiMarkdownRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E462707D109002D642B /* GeminiMarkdownRendererTests.swift */; };
|
||||||
|
D68C1E4B2708A95D002D642B /* GeminiHTMLRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E442707CCDB002D642B /* GeminiHTMLRendererTests.swift */; };
|
||||||
|
D68C1E4D2708A981002D642B /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D68C1E4C2708A981002D642B /* HTMLEntities */; };
|
||||||
|
D68C1E512708B276002D642B /* OpenHomepageIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E502708B276002D642B /* OpenHomepageIntentHandler.swift */; };
|
||||||
|
D68C1E522708B2D3002D642B /* OpenHomepageIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C1E502708B276002D642B /* OpenHomepageIntentHandler.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 */; };
|
||||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
||||||
|
@ -207,6 +230,13 @@
|
||||||
remoteGlobalIDString = D62664A724BBF26A00DF9B88;
|
remoteGlobalIDString = D62664A724BBF26A00DF9B88;
|
||||||
remoteInfo = GeminiFormat;
|
remoteInfo = GeminiFormat;
|
||||||
};
|
};
|
||||||
|
D68C1E2A270614F9002D642B /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = D68C1E22270614F9002D642B;
|
||||||
|
remoteInfo = GeminiIntents;
|
||||||
|
};
|
||||||
D6E152DA24C0007200FDF9D3 /* PBXContainerItemProxy */ = {
|
D6E152DA24C0007200FDF9D3 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
|
containerPortal = D626645324BBF1C200DF9B88 /* Project object */;
|
||||||
|
@ -255,6 +285,17 @@
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D68C1E30270614F9002D642B /* Embed App Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
D68C1E2C270614F9002D642B /* GeminiIntents.appex in Embed App Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed App Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6E152C224BFFE2500FDF9D3 /* Embed Frameworks */ = {
|
D6E152C224BFFE2500FDF9D3 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -308,12 +349,14 @@
|
||||||
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>"; };
|
||||||
D6376A6F26DDAF65005AD89C /* URIFixup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIFixup.swift; sourceTree = "<group>"; };
|
D6376A6F26DDAF65005AD89C /* URIFixup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIFixup.swift; sourceTree = "<group>"; };
|
||||||
|
D640A2312711DC7700177E85 /* ToolbarPrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarPrefView.swift; sourceTree = "<group>"; };
|
||||||
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; };
|
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepagePrefView.swift; sourceTree = "<group>"; };
|
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepagePrefView.swift; sourceTree = "<group>"; };
|
||||||
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetHomepageActivity.swift; sourceTree = "<group>"; };
|
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetHomepageActivity.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>"; };
|
||||||
|
D664E4F92713DF72005BAF55 /* ToolbarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItem.swift; sourceTree = "<group>"; };
|
||||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.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>"; };
|
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>"; };
|
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
||||||
|
@ -321,6 +364,24 @@
|
||||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolCache.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>"; };
|
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>"; };
|
D688F662258C2479003A0A73 /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
D68C1DFF2703EA13002D642B /* UserActivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivities.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E1827055E09002D642B /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = "<group>"; };
|
||||||
|
D68C1E1B27055EB0002D642B /* OpenURLIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenURLIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E1D270605A7002D642B /* BrowserHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHelper.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E23270614F9002D642B /* GeminiIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GeminiIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
D68C1E24270614F9002D642B /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
|
||||||
|
D68C1E27270614F9002D642B /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E29270614F9002D642B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
D68C1E362706215A002D642B /* MakeRequestIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeRequestIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E382707A1E5002D642B /* GetBodyIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBodyIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E3A2707BF25002D642B /* GetStatusIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStatusIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E3C2707C002002D642B /* GetMetaIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetMetaIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E3E2707C3B2002D642B /* GemtextToHTMLIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GemtextToHTMLIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E402707CB03002D642B /* GemtextToMarkdownIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GemtextToMarkdownIntentHandler.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E422707CB69002D642B /* GeminiMarkdownRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiMarkdownRenderer.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E442707CCDB002D642B /* GeminiHTMLRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRendererTests.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E462707D109002D642B /* GeminiMarkdownRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiMarkdownRendererTests.swift; sourceTree = "<group>"; };
|
||||||
|
D68C1E502708B276002D642B /* OpenHomepageIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHomepageIntentHandler.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>"; };
|
||||||
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>"; };
|
||||||
|
@ -382,6 +443,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D68C1E4D2708A981002D642B /* HTMLEntities in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -397,7 +459,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;
|
||||||
|
@ -410,6 +471,14 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D68C1E20270614F9002D642B /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D68C1E25270614F9002D642B /* Intents.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6E1529F24BFFDF500FDF9D3 /* Frameworks */ = {
|
D6E1529F24BFFDF500FDF9D3 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -444,6 +513,7 @@
|
||||||
children = (
|
children = (
|
||||||
D626645D24BBF1C200DF9B88 /* Gemini */,
|
D626645D24BBF1C200DF9B88 /* Gemini */,
|
||||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */,
|
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */,
|
||||||
|
D68C1E26270614F9002D642B /* GeminiIntents */,
|
||||||
D6E152D124C0007200FDF9D3 /* BrowserCore */,
|
D6E152D124C0007200FDF9D3 /* BrowserCore */,
|
||||||
D6E152DE24C0007200FDF9D3 /* BrowserCoreTests */,
|
D6E152DE24C0007200FDF9D3 /* BrowserCoreTests */,
|
||||||
D626647824BBF22E00DF9B88 /* GeminiProtocol */,
|
D626647824BBF22E00DF9B88 /* GeminiProtocol */,
|
||||||
|
@ -470,6 +540,7 @@
|
||||||
D6E152A224BFFDF500FDF9D3 /* Gemini-iOS.app */,
|
D6E152A224BFFDF500FDF9D3 /* Gemini-iOS.app */,
|
||||||
D6E152D024C0007200FDF9D3 /* BrowserCore.framework */,
|
D6E152D024C0007200FDF9D3 /* BrowserCore.framework */,
|
||||||
D6E152D824C0007200FDF9D3 /* BrowserCoreTests.xctest */,
|
D6E152D824C0007200FDF9D3 /* BrowserCoreTests.xctest */,
|
||||||
|
D68C1E23270614F9002D642B /* GeminiIntents.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -531,6 +602,8 @@
|
||||||
D62664C724BBF2C600DF9B88 /* Document.swift */,
|
D62664C724BBF2C600DF9B88 /* Document.swift */,
|
||||||
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
|
D62664C524BBF27300DF9B88 /* GeminiParser.swift */,
|
||||||
D6BC9AC4258F01F6008652BC /* TableOfContents.swift */,
|
D6BC9AC4258F01F6008652BC /* TableOfContents.swift */,
|
||||||
|
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */,
|
||||||
|
D68C1E422707CB69002D642B /* GeminiMarkdownRenderer.swift */,
|
||||||
);
|
);
|
||||||
path = GeminiFormat;
|
path = GeminiFormat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -541,6 +614,8 @@
|
||||||
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */,
|
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */,
|
||||||
D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */,
|
D62664B724BBF26A00DF9B88 /* GeminiParserTests.swift */,
|
||||||
D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */,
|
D6BC9ACD258F07BC008652BC /* TableOfContentsTests.swift */,
|
||||||
|
D68C1E442707CCDB002D642B /* GeminiHTMLRendererTests.swift */,
|
||||||
|
D68C1E462707D109002D642B /* GeminiMarkdownRendererTests.swift */,
|
||||||
D62664B924BBF26A00DF9B88 /* Info.plist */,
|
D62664B924BBF26A00DF9B88 /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = GeminiFormatTests;
|
path = GeminiFormatTests;
|
||||||
|
@ -557,7 +632,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>";
|
||||||
|
@ -574,6 +648,7 @@
|
||||||
D62664EF24BC0D7700DF9B88 /* Frameworks */ = {
|
D62664EF24BC0D7700DF9B88 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D68C1E24270614F9002D642B /* Intents.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -594,6 +669,31 @@
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D68C1E1A27055EA0002D642B /* Intents */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D68C1DFF2703EA13002D642B /* UserActivities.swift */,
|
||||||
|
D68C1E1B27055EB0002D642B /* OpenURLIntentHandler.swift */,
|
||||||
|
D68C1E502708B276002D642B /* OpenHomepageIntentHandler.swift */,
|
||||||
|
);
|
||||||
|
path = Intents;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D68C1E26270614F9002D642B /* GeminiIntents */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D68C1E27270614F9002D642B /* IntentHandler.swift */,
|
||||||
|
D68C1E362706215A002D642B /* MakeRequestIntentHandler.swift */,
|
||||||
|
D68C1E382707A1E5002D642B /* GetBodyIntentHandler.swift */,
|
||||||
|
D68C1E3A2707BF25002D642B /* GetStatusIntentHandler.swift */,
|
||||||
|
D68C1E3C2707C002002D642B /* GetMetaIntentHandler.swift */,
|
||||||
|
D68C1E3E2707C3B2002D642B /* GemtextToHTMLIntentHandler.swift */,
|
||||||
|
D68C1E402707CB03002D642B /* GemtextToMarkdownIntentHandler.swift */,
|
||||||
|
D68C1E29270614F9002D642B /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = GeminiIntents;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -608,13 +708,17 @@
|
||||||
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
|
D6BC9ABB258E9862008652BC /* NavigationBarView.swift */,
|
||||||
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
|
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
|
||||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||||
|
D664E4F92713DF72005BAF55 /* ToolbarItem.swift */,
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||||
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */,
|
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */,
|
||||||
|
D640A2312711DC7700177E85 /* ToolbarPrefView.swift */,
|
||||||
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */,
|
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */,
|
||||||
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */,
|
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */,
|
||||||
|
D68C1E1A27055EA0002D642B /* Intents */,
|
||||||
D688F618258AD231003A0A73 /* Resources */,
|
D688F618258AD231003A0A73 /* Resources */,
|
||||||
D6376A6E26DDAF57005AD89C /* Vendor */,
|
D6376A6E26DDAF57005AD89C /* Vendor */,
|
||||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||||
|
D68C1E1827055E09002D642B /* Intents.intentdefinition */,
|
||||||
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
||||||
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
||||||
D6E152AC24BFFDF600FDF9D3 /* Preview Content */,
|
D6E152AC24BFFDF600FDF9D3 /* Preview Content */,
|
||||||
|
@ -636,6 +740,7 @@
|
||||||
D6E152D224C0007200FDF9D3 /* BrowserCore.h */,
|
D6E152D224C0007200FDF9D3 /* BrowserCore.h */,
|
||||||
D6E152D324C0007200FDF9D3 /* Info.plist */,
|
D6E152D324C0007200FDF9D3 /* Info.plist */,
|
||||||
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */,
|
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */,
|
||||||
|
D68C1E1D270605A7002D642B /* BrowserHelper.swift */,
|
||||||
D626646024BBF1C200DF9B88 /* BrowserView.swift */,
|
D626646024BBF1C200DF9B88 /* BrowserView.swift */,
|
||||||
);
|
);
|
||||||
path = BrowserCore;
|
path = BrowserCore;
|
||||||
|
@ -761,6 +866,9 @@
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = GeminiFormat;
|
name = GeminiFormat;
|
||||||
|
packageProductDependencies = (
|
||||||
|
D68C1E4C2708A981002D642B /* HTMLEntities */,
|
||||||
|
);
|
||||||
productName = GeminiFormat;
|
productName = GeminiFormat;
|
||||||
productReference = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */;
|
productReference = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */;
|
||||||
productType = "com.apple.product-type.framework";
|
productType = "com.apple.product-type.framework";
|
||||||
|
@ -801,7 +909,6 @@
|
||||||
);
|
);
|
||||||
name = GeminiRenderer;
|
name = GeminiRenderer;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D688F58F258AC814003A0A73 /* HTMLEntities */,
|
|
||||||
);
|
);
|
||||||
productName = GeminiRenderer;
|
productName = GeminiRenderer;
|
||||||
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
||||||
|
@ -826,6 +933,23 @@
|
||||||
productReference = D62664D624BC081B00DF9B88 /* GeminiRendererTests.xctest */;
|
productReference = D62664D624BC081B00DF9B88 /* GeminiRendererTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
|
D68C1E22270614F9002D642B /* GeminiIntents */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = D68C1E2D270614F9002D642B /* Build configuration list for PBXNativeTarget "GeminiIntents" */;
|
||||||
|
buildPhases = (
|
||||||
|
D68C1E1F270614F9002D642B /* Sources */,
|
||||||
|
D68C1E20270614F9002D642B /* Frameworks */,
|
||||||
|
D68C1E21270614F9002D642B /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = GeminiIntents;
|
||||||
|
productName = GeminiIntents;
|
||||||
|
productReference = D68C1E23270614F9002D642B /* GeminiIntents.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
D6E152A124BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
D6E152A124BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = D6E152B324BFFDF600FDF9D3 /* Build configuration list for PBXNativeTarget "Gemini-iOS" */;
|
buildConfigurationList = D6E152B324BFFDF600FDF9D3 /* Build configuration list for PBXNativeTarget "Gemini-iOS" */;
|
||||||
|
@ -834,6 +958,7 @@
|
||||||
D6E1529F24BFFDF500FDF9D3 /* Frameworks */,
|
D6E1529F24BFFDF500FDF9D3 /* Frameworks */,
|
||||||
D6E152A024BFFDF500FDF9D3 /* Resources */,
|
D6E152A024BFFDF500FDF9D3 /* Resources */,
|
||||||
D6E152C224BFFE2500FDF9D3 /* Embed Frameworks */,
|
D6E152C224BFFE2500FDF9D3 /* Embed Frameworks */,
|
||||||
|
D68C1E30270614F9002D642B /* Embed App Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -842,6 +967,7 @@
|
||||||
D68543FC2522DEF5004C4AE0 /* PBXTargetDependency */,
|
D68543FC2522DEF5004C4AE0 /* PBXTargetDependency */,
|
||||||
D68543FA2522DEF3004C4AE0 /* PBXTargetDependency */,
|
D68543FA2522DEF3004C4AE0 /* PBXTargetDependency */,
|
||||||
D68543F82522DEF0004C4AE0 /* PBXTargetDependency */,
|
D68543F82522DEF0004C4AE0 /* PBXTargetDependency */,
|
||||||
|
D68C1E2B270614F9002D642B /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = "Gemini-iOS";
|
name = "Gemini-iOS";
|
||||||
productName = "Gemini-iOS";
|
productName = "Gemini-iOS";
|
||||||
|
@ -894,7 +1020,7 @@
|
||||||
D626645324BBF1C200DF9B88 /* Project object */ = {
|
D626645324BBF1C200DF9B88 /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1200;
|
LastSwiftUpdateCheck = 1300;
|
||||||
LastUpgradeCheck = 1220;
|
LastUpgradeCheck = 1220;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D626645A24BBF1C200DF9B88 = {
|
D626645A24BBF1C200DF9B88 = {
|
||||||
|
@ -924,6 +1050,9 @@
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
TestTargetID = D626645A24BBF1C200DF9B88;
|
TestTargetID = D626645A24BBF1C200DF9B88;
|
||||||
};
|
};
|
||||||
|
D68C1E22270614F9002D642B = {
|
||||||
|
CreatedOnToolsVersion = 13.0;
|
||||||
|
};
|
||||||
D6E152A124BFFDF500FDF9D3 = {
|
D6E152A124BFFDF500FDF9D3 = {
|
||||||
CreatedOnToolsVersion = 12.0;
|
CreatedOnToolsVersion = 12.0;
|
||||||
};
|
};
|
||||||
|
@ -962,6 +1091,7 @@
|
||||||
D62664D524BC081B00DF9B88 /* GeminiRendererTests */,
|
D62664D524BC081B00DF9B88 /* GeminiRendererTests */,
|
||||||
D6E152CF24C0007200FDF9D3 /* BrowserCore */,
|
D6E152CF24C0007200FDF9D3 /* BrowserCore */,
|
||||||
D6E152D724C0007200FDF9D3 /* BrowserCoreTests */,
|
D6E152D724C0007200FDF9D3 /* BrowserCoreTests */,
|
||||||
|
D68C1E22270614F9002D642B /* GeminiIntents */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -1020,6 +1150,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D68C1E21270614F9002D642B /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6E152A024BFFDF500FDF9D3 /* Resources */ = {
|
D6E152A024BFFDF500FDF9D3 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1086,6 +1223,8 @@
|
||||||
files = (
|
files = (
|
||||||
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
|
D62664C824BBF2C600DF9B88 /* Document.swift in Sources */,
|
||||||
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
|
D62664C624BBF27300DF9B88 /* GeminiParser.swift in Sources */,
|
||||||
|
D68C1E492708A958002D642B /* GeminiHTMLRenderer.swift in Sources */,
|
||||||
|
D68C1E482708A958002D642B /* GeminiMarkdownRenderer.swift in Sources */,
|
||||||
D6BC9AC5258F01F6008652BC /* TableOfContents.swift in Sources */,
|
D6BC9AC5258F01F6008652BC /* TableOfContents.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1096,7 +1235,9 @@
|
||||||
files = (
|
files = (
|
||||||
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */,
|
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */,
|
||||||
D6BC9ACE258F07BC008652BC /* TableOfContentsTests.swift in Sources */,
|
D6BC9ACE258F07BC008652BC /* TableOfContentsTests.swift in Sources */,
|
||||||
|
D68C1E4A2708A95D002D642B /* GeminiMarkdownRendererTests.swift in Sources */,
|
||||||
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */,
|
D62664B824BBF26A00DF9B88 /* GeminiParserTests.swift in Sources */,
|
||||||
|
D68C1E4B2708A95D002D642B /* GeminiHTMLRendererTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1109,7 +1250,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;
|
||||||
|
@ -1122,6 +1262,24 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
D68C1E1F270614F9002D642B /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D68C1E3227061557002D642B /* OpenURLIntentHandler.swift in Sources */,
|
||||||
|
D68C1E3B2707BF25002D642B /* GetStatusIntentHandler.swift in Sources */,
|
||||||
|
D68C1E312706150E002D642B /* Intents.intentdefinition in Sources */,
|
||||||
|
D68C1E392707A1E5002D642B /* GetBodyIntentHandler.swift in Sources */,
|
||||||
|
D68C1E522708B2D3002D642B /* OpenHomepageIntentHandler.swift in Sources */,
|
||||||
|
D68C1E3D2707C002002D642B /* GetMetaIntentHandler.swift in Sources */,
|
||||||
|
D68C1E3F2707C3B2002D642B /* GemtextToHTMLIntentHandler.swift in Sources */,
|
||||||
|
D68C1E28270614F9002D642B /* IntentHandler.swift in Sources */,
|
||||||
|
D68C1E412707CB03002D642B /* GemtextToMarkdownIntentHandler.swift in Sources */,
|
||||||
|
D68C1E372706215A002D642B /* MakeRequestIntentHandler.swift in Sources */,
|
||||||
|
D68C1E3327061568002D642B /* UserActivities.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6E1529E24BFFDF500FDF9D3 /* Sources */ = {
|
D6E1529E24BFFDF500FDF9D3 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1130,17 +1288,23 @@
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||||
|
D640A2322711DC7700177E85 /* ToolbarPrefView.swift in Sources */,
|
||||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||||
|
D68C1E35270615D3002D642B /* UserActivities.swift in Sources */,
|
||||||
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */,
|
D6376A7026DDAF65005AD89C /* URIFixup.swift in Sources */,
|
||||||
|
D68C1E34270615D3002D642B /* OpenURLIntentHandler.swift in Sources */,
|
||||||
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */,
|
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */,
|
||||||
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */,
|
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */,
|
||||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
||||||
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */,
|
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */,
|
||||||
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||||
|
D664E4FA2713DF72005BAF55 /* ToolbarItem.swift in Sources */,
|
||||||
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
|
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
|
||||||
|
D68C1E1927055E09002D642B /* Intents.intentdefinition in Sources */,
|
||||||
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
||||||
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */,
|
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */,
|
||||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
||||||
|
D68C1E512708B276002D642B /* OpenHomepageIntentHandler.swift in Sources */,
|
||||||
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */,
|
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1149,6 +1313,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D68C1E1E270605A7002D642B /* BrowserHelper.swift in Sources */,
|
||||||
D6E152F124C000A000FDF9D3 /* NavigationManager.swift in Sources */,
|
D6E152F124C000A000FDF9D3 /* NavigationManager.swift in Sources */,
|
||||||
D6E152F224C000CD00FDF9D3 /* BrowserView.swift in Sources */,
|
D6E152F224C000CD00FDF9D3 /* BrowserView.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
@ -1250,6 +1415,11 @@
|
||||||
target = D62664A724BBF26A00DF9B88 /* GeminiFormat */;
|
target = D62664A724BBF26A00DF9B88 /* GeminiFormat */;
|
||||||
targetProxy = D685442F2522E10F004C4AE0 /* PBXContainerItemProxy */;
|
targetProxy = D685442F2522E10F004C4AE0 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
D68C1E2B270614F9002D642B /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = D68C1E22270614F9002D642B /* GeminiIntents */;
|
||||||
|
targetProxy = D68C1E2A270614F9002D642B /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
D6E152DB24C0007200FDF9D3 /* PBXTargetDependency */ = {
|
D6E152DB24C0007200FDF9D3 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D6E152CF24C0007200FDF9D3 /* BrowserCore */;
|
target = D6E152CF24C0007200FDF9D3 /* BrowserCore */;
|
||||||
|
@ -1449,6 +1619,7 @@
|
||||||
D626648F24BBF22E00DF9B88 /* Debug */ = {
|
D626648F24BBF22E00DF9B88 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1482,6 +1653,7 @@
|
||||||
D626649024BBF22E00DF9B88 /* Release */ = {
|
D626649024BBF22E00DF9B88 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1554,6 +1726,7 @@
|
||||||
D62664C024BBF26A00DF9B88 /* Debug */ = {
|
D62664C024BBF26A00DF9B88 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1587,6 +1760,7 @@
|
||||||
D62664C124BBF26A00DF9B88 /* Release */ = {
|
D62664C124BBF26A00DF9B88 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1659,6 +1833,7 @@
|
||||||
D62664E624BC081B00DF9B88 /* Debug */ = {
|
D62664E624BC081B00DF9B88 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1692,6 +1867,7 @@
|
||||||
D62664E724BC081B00DF9B88 /* Release */ = {
|
D62664E724BC081B00DF9B88 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
@ -1761,6 +1937,63 @@
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
D68C1E2E270614F9002D642B /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = GeminiIntents/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = GeminiIntents;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 2021.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Gemini.GeminiIntents;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
D68C1E2F270614F9002D642B /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = GeminiIntents/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = GeminiIntents;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 2021.1;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Gemini.GeminiIntents;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
D6E152B424BFFDF600FDF9D3 /* Debug */ = {
|
D6E152B424BFFDF600FDF9D3 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
@ -1817,6 +2050,7 @@
|
||||||
D6E152E824C0007200FDF9D3 /* Debug */ = {
|
D6E152E824C0007200FDF9D3 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
@ -1848,6 +2082,7 @@
|
||||||
D6E152E924C0007200FDF9D3 /* Release */ = {
|
D6E152E924C0007200FDF9D3 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
@ -1991,6 +2226,15 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
D68C1E2D270614F9002D642B /* Build configuration list for PBXNativeTarget "GeminiIntents" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
D68C1E2E270614F9002D642B /* Debug */,
|
||||||
|
D68C1E2F270614F9002D642B /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
D6E152B324BFFDF600FDF9D3 /* Build configuration list for PBXNativeTarget "Gemini-iOS" */ = {
|
D6E152B324BFFDF600FDF9D3 /* Build configuration list for PBXNativeTarget "Gemini-iOS" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
@ -2032,7 +2276,7 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
D688F58F258AC814003A0A73 /* HTMLEntities */ = {
|
D68C1E4C2708A981002D642B /* HTMLEntities */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
package = D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
||||||
productName = HTMLEntities;
|
productName = HTMLEntities;
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1300"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D68C1E22270614F9002D642B"
|
||||||
|
BuildableName = "GeminiIntents.appex"
|
||||||
|
BlueprintName = "GeminiIntents"
|
||||||
|
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||||
|
BuildableName = "Gemini-iOS.app"
|
||||||
|
BlueprintName = "Gemini-iOS"
|
||||||
|
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||||
|
BuildableName = "Gemini-iOS.app"
|
||||||
|
BlueprintName = "Gemini-iOS"
|
||||||
|
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||||
|
BuildableName = "Gemini-iOS.app"
|
||||||
|
BlueprintName = "Gemini-iOS"
|
||||||
|
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -7,7 +7,7 @@
|
||||||
<key>BrowserCore.xcscheme_^#shared#^_</key>
|
<key>BrowserCore.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>4</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Gemini-iOS.xcscheme_^#shared#^_</key>
|
<key>Gemini-iOS.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -24,15 +24,20 @@
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>GeminiIntents.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
<key>GeminiProtocol.xcscheme_^#shared#^_</key>
|
<key>GeminiProtocol.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>4</integer>
|
<integer>5</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>GeminiRenderer.xcscheme_^#shared#^_</key>
|
<key>GeminiRenderer.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>5</integer>
|
<integer>6</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
@ -62,6 +67,11 @@
|
||||||
<key>primary</key>
|
<key>primary</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>D68C1E22270614F9002D642B</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>D6E152A124BFFDF500FDF9D3</key>
|
<key>D6E152A124BFFDF500FDF9D3</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>primary</key>
|
<key>primary</key>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"object": {
|
"object": {
|
||||||
"pins": [
|
"pins": [
|
||||||
{
|
{
|
||||||
"package": "swift-html-entities",
|
"package": "HTMLEntities",
|
||||||
"repositoryURL": "https://github.com/Kitura/swift-html-entities",
|
"repositoryURL": "https://github.com/Kitura/swift-html-entities",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GeminiFormat
|
|
||||||
import HTMLEntities
|
import HTMLEntities
|
||||||
|
|
||||||
public class GeminiHTMLRenderer {
|
public class GeminiHTMLRenderer {
|
||||||
|
|
||||||
public var linkPrefix: ((URL) -> String?)?
|
public var linkPrefix: ((URL) -> String?)?
|
||||||
|
public var addHeadingLineIDs = true
|
||||||
|
public var addLinkClass = true
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
@ -25,6 +26,7 @@ public class GeminiHTMLRenderer {
|
||||||
for (index, line) in doc.lines.enumerated() {
|
for (index, line) in doc.lines.enumerated() {
|
||||||
if inList && !line.isListItem {
|
if inList && !line.isListItem {
|
||||||
str += "</ul>"
|
str += "</ul>"
|
||||||
|
inList = false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch line {
|
switch line {
|
||||||
|
@ -33,7 +35,11 @@ public class GeminiHTMLRenderer {
|
||||||
case let .link(url, text: maybeText):
|
case let .link(url, text: maybeText):
|
||||||
let text = maybeText ?? url.absoluteString
|
let text = maybeText ?? url.absoluteString
|
||||||
let linkPrefix = self.linkPrefix?(url) ?? ""
|
let linkPrefix = self.linkPrefix?(url) ?? ""
|
||||||
str += "<p class=\"link\">\(linkPrefix)<a href=\"\(url.absoluteString)\">\(text.htmlEscape())</a></p>"
|
str += "<p"
|
||||||
|
if addLinkClass {
|
||||||
|
str += " class=\"link\""
|
||||||
|
}
|
||||||
|
str += ">\(linkPrefix)<a href=\"\(url.absoluteString)\">\(text.htmlEscape())</a></p>"
|
||||||
case .preformattedToggle(alt: _):
|
case .preformattedToggle(alt: _):
|
||||||
inPreformatting = !inPreformatting
|
inPreformatting = !inPreformatting
|
||||||
if inPreformatting {
|
if inPreformatting {
|
||||||
|
@ -46,7 +52,11 @@ public class GeminiHTMLRenderer {
|
||||||
str += "\n"
|
str += "\n"
|
||||||
case let .heading(text, level: level):
|
case let .heading(text, level: level):
|
||||||
let tag = "h\(level.rawValue)"
|
let tag = "h\(level.rawValue)"
|
||||||
str += "<\(tag) id=\"l\(index)\">\(text.htmlEscape())</\(tag)>"
|
str += "<\(tag)"
|
||||||
|
if addHeadingLineIDs {
|
||||||
|
str += " id=\"l\(index)\""
|
||||||
|
}
|
||||||
|
str += ">\(text.htmlEscape())</\(tag)>"
|
||||||
case let .unorderedListItem(text):
|
case let .unorderedListItem(text):
|
||||||
if !inList {
|
if !inList {
|
||||||
inList = true
|
inList = true
|
||||||
|
@ -63,7 +73,7 @@ public class GeminiHTMLRenderer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension Document.Line {
|
extension Document.Line {
|
||||||
var isListItem: Bool {
|
var isListItem: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .unorderedListItem(_):
|
case .unorderedListItem(_):
|
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// GeminiMarkdownRenderer.swift
|
||||||
|
// GeminiRenderer
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTMLEntities
|
||||||
|
|
||||||
|
public class GeminiMarkdownRenderer {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderDocumentToMarkdown(_ doc: Document) -> String {
|
||||||
|
var str = ""
|
||||||
|
|
||||||
|
var inPreformatting = false
|
||||||
|
var inList = false
|
||||||
|
|
||||||
|
for line in doc.lines {
|
||||||
|
if inList && !line.isListItem {
|
||||||
|
str += "\n"
|
||||||
|
inList = false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch line {
|
||||||
|
case let .text(text):
|
||||||
|
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .link(url, text: maybeText):
|
||||||
|
let text = maybeText ?? url.absoluteString
|
||||||
|
// todo: do ] in the text need to be escaped?
|
||||||
|
str += "[\(text.htmlEscape())](\(url))"
|
||||||
|
str += "\n\n"
|
||||||
|
|
||||||
|
case let .preformattedToggle(alt: alt):
|
||||||
|
inPreformatting = !inPreformatting
|
||||||
|
if inPreformatting {
|
||||||
|
str += "```"
|
||||||
|
if let alt = alt {
|
||||||
|
str += alt
|
||||||
|
}
|
||||||
|
str += "\n"
|
||||||
|
} else {
|
||||||
|
str += "```"
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .preformattedText(text):
|
||||||
|
str += text
|
||||||
|
str += "\n"
|
||||||
|
|
||||||
|
case let .heading(text, level: level):
|
||||||
|
str += String(repeating: "#", count: level.rawValue)
|
||||||
|
str += " "
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
|
||||||
|
case let .unorderedListItem(text):
|
||||||
|
if !inList {
|
||||||
|
inList = true
|
||||||
|
}
|
||||||
|
str += "* \(text.htmlEscape())"
|
||||||
|
str += "\n"
|
||||||
|
|
||||||
|
case let .quote(text):
|
||||||
|
str += "> "
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
//
|
||||||
|
// GeminiHTMLRendererTests.swift
|
||||||
|
// GeminiRendererTests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import GeminiFormat
|
||||||
|
@testable import GeminiRenderer
|
||||||
|
|
||||||
|
class GeminiHTMLRendererTests: XCTestCase {
|
||||||
|
|
||||||
|
private var doc: Document!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
doc = Document(url: URL(string: "gemini://example.com/")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEscapeToEntities() {
|
||||||
|
doc.lines = [.text("<b>hello</b>")]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<p><b>hello</b></p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParagraph() {
|
||||||
|
doc.lines = [.text("Hello, world!")]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<p>Hello, world!</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLink() {
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: "text")]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<p class=\"link\"><a href=\"gemini://example.com/\">text</a></p>")
|
||||||
|
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: nil)]
|
||||||
|
let noText = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(noText, "<p class=\"link\"><a href=\"gemini://example.com/\">gemini://example.com/</a></p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisableLinkClass() {
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: "test")]
|
||||||
|
let renderer = GeminiHTMLRenderer()
|
||||||
|
renderer.addLinkClass = false
|
||||||
|
let html = renderer.renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<p><a href=\"gemini://example.com/\">test</a></p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPreformatting() {
|
||||||
|
doc.lines = [
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
.preformattedText("foo"),
|
||||||
|
.preformattedText("* bar"),
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<pre>foo\n* bar\n</pre>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHeading() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("One", level: .h1),
|
||||||
|
.heading("Two", level: .h2),
|
||||||
|
]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<h1 id=\"l0\">One</h1><h2 id=\"l1\">Two</h2>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisableHeadingIDs() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("One", level: .h1),
|
||||||
|
.heading("Two", level: .h2),
|
||||||
|
]
|
||||||
|
let renderer = GeminiHTMLRenderer()
|
||||||
|
renderer.addHeadingLineIDs = false
|
||||||
|
let html = renderer.renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<h1>One</h1><h2>Two</h2>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnorderedList() {
|
||||||
|
doc.lines = [
|
||||||
|
.text("before"),
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.unorderedListItem("c"),
|
||||||
|
.text("after"),
|
||||||
|
]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<p>before</p><ul><li>a</li><li>b</li><li>c</li></ul><p>after</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testQuote() {
|
||||||
|
doc.lines = [
|
||||||
|
.quote("quoted")
|
||||||
|
]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<blockquote>quoted</blockquote>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStuffAfterList() {
|
||||||
|
doc.lines = [
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.text("c"),
|
||||||
|
.text("d"),
|
||||||
|
]
|
||||||
|
let html = GeminiHTMLRenderer().renderDocumentToHTML(doc)
|
||||||
|
XCTAssertEqual(html, "<ul><li>a</li><li>b</li></ul><p>c</p><p>d</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
//
|
||||||
|
// GeminiMarkdownRendererTests.swift
|
||||||
|
// GeminiRendererTests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import GeminiFormat
|
||||||
|
@testable import GeminiRenderer
|
||||||
|
|
||||||
|
class GeminiMarkdownRendererTests: XCTestCase {
|
||||||
|
|
||||||
|
private var doc: Document!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
doc = Document(url: URL(string: "gemini://example.com/")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEscapeToEntities() {
|
||||||
|
doc.lines = [.text("<b>hello</b>")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "<b>hello</b>\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParagraph() {
|
||||||
|
doc.lines = [.text("Hello, world!")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "Hello, world!\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLink() {
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: "text")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "[text](gemini://example.com/)\n\n")
|
||||||
|
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: nil)]
|
||||||
|
let noText = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(noText, "[gemini://example.com/](gemini://example.com/)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLinksAfterList() {
|
||||||
|
doc.lines = [
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.link(URL(string: "gemini://example.com")!, text: "one"),
|
||||||
|
.link(URL(string: "gemini://example.com")!, text: "two"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "* a\n* b\n\n[one](gemini://example.com)\n\n[two](gemini://example.com)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPreformatting() {
|
||||||
|
doc.lines = [
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
.preformattedText("foo"),
|
||||||
|
.preformattedText("* bar"),
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "```\nfoo\n* bar\n```\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHeading() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("One", level: .h1),
|
||||||
|
.heading("Two", level: .h2),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "# One\n\n## Two\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnorderedList() {
|
||||||
|
doc.lines = [
|
||||||
|
.text("before"),
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.unorderedListItem("c"),
|
||||||
|
.text("after"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "before\n\n* a\n* b\n* c\n\nafter\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testQuote() {
|
||||||
|
doc.lines = [
|
||||||
|
.quote("quoted")
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "> quoted\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSkipBlankLines() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("Hello", level: .h1),
|
||||||
|
.text(""),
|
||||||
|
.text("World"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "# Hello\n\nWorld\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// GemtextToHTMLIntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
import GeminiFormat
|
||||||
|
|
||||||
|
class GemtextToHTMLIntentHandler: NSObject, GemtextToHTMLIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: GemtextToHTMLIntent, completion: @escaping (GemtextToHTMLIntentResponse) -> Void) {
|
||||||
|
guard let response = intent.response,
|
||||||
|
let text = response.body,
|
||||||
|
let url = response.url else {
|
||||||
|
completion(GemtextToHTMLIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let doc = GeminiParser.parse(text: text, baseURL: url)
|
||||||
|
let renderer = GeminiHTMLRenderer()
|
||||||
|
renderer.addLinkClass = false
|
||||||
|
renderer.addHeadingLineIDs = false
|
||||||
|
let html = renderer.renderDocumentToHTML(doc)
|
||||||
|
let intentResp = GemtextToHTMLIntentResponse(code: .success, userActivity: nil)
|
||||||
|
intentResp.html = INFile(data: html.data(using: .utf8)!, filename: "converted_gemtext.html", typeIdentifier: "public.html")
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// GemtextToMarkdownIntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
import GeminiFormat
|
||||||
|
|
||||||
|
class GemtextToMarkdownIntentHandler: NSObject, GemtextToMarkdownIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: GemtextToMarkdownIntent, completion: @escaping (GemtextToMarkdownIntentResponse) -> Void) {
|
||||||
|
guard let response = intent.response,
|
||||||
|
let text = response.body,
|
||||||
|
let url = response.url else {
|
||||||
|
completion(GemtextToMarkdownIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let doc = GeminiParser.parse(text: text, baseURL: url)
|
||||||
|
let renderer = GeminiMarkdownRenderer()
|
||||||
|
let html = renderer.renderDocumentToMarkdown(doc)
|
||||||
|
let intentResp = GemtextToMarkdownIntentResponse(code: .success, userActivity: nil)
|
||||||
|
intentResp.markdown = INFile(data: html.data(using: .utf8)!, filename: "converted_gemtext.md", typeIdentifier: "net.daringfireball.markdown")
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// GetBodyIntenHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class GetBodyIntentHandler: NSObject, GetBodyIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: GetBodyIntent, completion: @escaping (GetBodyIntentResponse) -> Void) {
|
||||||
|
guard let response = intent.response else {
|
||||||
|
completion(GetBodyIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let intentResp = GetBodyIntentResponse(code: .success, userActivity: nil)
|
||||||
|
intentResp.text = response.body
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// GetMetaIntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class GetMetaIntentHandler: NSObject, GetMetaIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: GetMetaIntent, completion: @escaping (GetMetaIntentResponse) -> Void) {
|
||||||
|
guard let response = intent.response else {
|
||||||
|
completion(GetMetaIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let intentResp = GetMetaIntentResponse(code: .success, userActivity: nil)
|
||||||
|
intentResp.meta = response.meta
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// GetStatusIntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class GetStatusIntentHandler: NSObject, GetStatusIntentHandling {
|
||||||
|
|
||||||
|
func handle(intent: GetStatusIntent, completion: @escaping (GetStatusIntentResponse) -> Void) {
|
||||||
|
guard let response = intent.response else {
|
||||||
|
completion(GetStatusIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let intentResp = GetStatusIntentResponse(code: .success, userActivity: nil)
|
||||||
|
intentResp.status = response.status.rawValue as NSNumber
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>IntentsRestrictedWhileLocked</key>
|
||||||
|
<array/>
|
||||||
|
<key>IntentsRestrictedWhileProtectedDataUnavailable</key>
|
||||||
|
<array/>
|
||||||
|
<key>IntentsSupported</key>
|
||||||
|
<array>
|
||||||
|
<string>GemtextToHTMLIntent</string>
|
||||||
|
<string>GemtextToMarkdownIntent</string>
|
||||||
|
<string>GetBodyIntent</string>
|
||||||
|
<string>GetMetaIntent</string>
|
||||||
|
<string>GetStatusIntent</string>
|
||||||
|
<string>MakeRequestIntent</string>
|
||||||
|
<string>OpenURLIntent</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.intents-service</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).IntentHandler</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,43 @@
|
||||||
|
//
|
||||||
|
// IntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
|
||||||
|
class IntentHandler: INExtension {
|
||||||
|
|
||||||
|
override func handler(for intent: INIntent) -> Any? {
|
||||||
|
switch intent {
|
||||||
|
// we also need to support extension-based handling because in-app handling isn't support <iOS 14
|
||||||
|
case is OpenURLIntent:
|
||||||
|
return OpenURLIntentHandler()
|
||||||
|
case is OpenHomepageIntent:
|
||||||
|
return OpenHomepageIntentHandler()
|
||||||
|
|
||||||
|
case is MakeRequestIntent:
|
||||||
|
return MakeRequestIntentHandler()
|
||||||
|
|
||||||
|
case is GetBodyIntent:
|
||||||
|
return GetBodyIntentHandler()
|
||||||
|
|
||||||
|
case is GetStatusIntent:
|
||||||
|
return GetStatusIntentHandler()
|
||||||
|
|
||||||
|
case is GetMetaIntent:
|
||||||
|
return GetMetaIntentHandler()
|
||||||
|
|
||||||
|
case is GemtextToHTMLIntent:
|
||||||
|
return GemtextToHTMLIntentHandler()
|
||||||
|
|
||||||
|
case is GemtextToMarkdownIntent:
|
||||||
|
return GemtextToMarkdownIntentHandler()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
//
|
||||||
|
// MakeRequestIntentHandler.swift
|
||||||
|
// GeminiIntents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Intents
|
||||||
|
import GeminiProtocol
|
||||||
|
import BrowserCore
|
||||||
|
|
||||||
|
class MakeRequestIntentHandler: NSObject, MakeRequestIntentHandling {
|
||||||
|
|
||||||
|
private var task: GeminiDataTask?
|
||||||
|
|
||||||
|
func resolveUrl(for intent: MakeRequestIntent, with completion: @escaping (INURLResolutionResult) -> Void) {
|
||||||
|
guard let url = intent.url,
|
||||||
|
url.scheme?.lowercased() == "gemini" else {
|
||||||
|
completion(.unsupported())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(.success(with: url))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(intent: MakeRequestIntent, completion: @escaping (MakeRequestIntentResponse) -> Void) {
|
||||||
|
guard let url = intent.url else {
|
||||||
|
completion(MakeRequestIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
makeRequest(for: url, followRedirects: intent.followRedirects?.boolValue ?? true, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeRequest(for url: URL, followRedirects: Bool, completion: @escaping (MakeRequestIntentResponse) -> Void) {
|
||||||
|
guard let request = try? GeminiRequest(url: url) else {
|
||||||
|
completion(MakeRequestIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// foo(bar(baz))
|
||||||
|
// a
|
||||||
|
|
||||||
|
task = GeminiDataTask(request: request) { [unowned self] (result) in
|
||||||
|
switch result {
|
||||||
|
case let .success(response):
|
||||||
|
if response.status.isRedirect && followRedirects {
|
||||||
|
if let newURL = URL(string: response.meta, relativeTo: url) {
|
||||||
|
self.makeRequest(for: newURL, followRedirects: followRedirects, completion: completion)
|
||||||
|
} else {
|
||||||
|
completion(MakeRequestIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let intentResp = MakeRequestIntentResponse(code: .success, userActivity: nil)
|
||||||
|
let displayStr = "\(response.status), \(BrowserHelper.urlForDisplay(url)), '\(response.meta)'"
|
||||||
|
intentResp.response = Response(identifier: url.absoluteString, display: displayStr)
|
||||||
|
intentResp.response!.url = url
|
||||||
|
intentResp.response!.body = response.bodyText
|
||||||
|
intentResp.response!.status = ResponseStatus(rawValue: response.status.rawValue)!
|
||||||
|
intentResp.response!.meta = response.meta
|
||||||
|
// intentResp.status = ResponseStatus(rawValue: response.status.rawValue)!
|
||||||
|
// intentResp.status = NSNumber(integerLiteral: response.status.rawValue)
|
||||||
|
// intentResp.statusCategory = ResponseStatusCategory(rawValue: response.status.rawValue / 10)!
|
||||||
|
// intentResp.meta = response.meta
|
||||||
|
// intentResp.mimeType = response.mimeType
|
||||||
|
// intentResp.body = response.bodyText
|
||||||
|
completion(intentResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(_):
|
||||||
|
completion(MakeRequestIntentResponse(code: .failure, userActivity: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
task!.attribution = .user
|
||||||
|
}
|
||||||
|
task!.resume()
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,19 @@ public class GeminiDataTask {
|
||||||
private let completion: Completion
|
private let completion: Completion
|
||||||
private var state = State.unstarted
|
private var state = State.unstarted
|
||||||
private let connection: NWConnection
|
private let connection: NWConnection
|
||||||
|
// todo: remove stupid hack when deployment target is >= iOS 15/macOS 12
|
||||||
|
private var _attribution: Any? = nil
|
||||||
|
#if os(iOS)
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
public var attribution: NWParameters.Attribution {
|
||||||
|
get {
|
||||||
|
_attribution as? NWParameters.Attribution ?? .developer
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_attribution = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public init(request: GeminiRequest, completion: @escaping Completion) {
|
public init(request: GeminiRequest, completion: @escaping Completion) {
|
||||||
self.request = request
|
self.request = request
|
||||||
|
@ -50,6 +63,13 @@ public class GeminiDataTask {
|
||||||
|
|
||||||
public func resume() {
|
public func resume() {
|
||||||
guard state == .unstarted else { return }
|
guard state == .unstarted else { return }
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
connection.parameters.attribution = attribution
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
state = .started
|
state = .started
|
||||||
connection.start(queue: GeminiDataTask.queue)
|
connection.start(queue: GeminiDataTask.queue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,6 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
_ = framer.parseInput(minimumIncompleteLength: min, maximumLength: 1024 + 2) { (buffer, isComplete) -> Int in
|
_ = framer.parseInput(minimumIncompleteLength: min, maximumLength: 1024 + 2) { (buffer, isComplete) -> Int in
|
||||||
guard let buffer = buffer,
|
guard let buffer = buffer,
|
||||||
buffer.count >= 2 else { return 0 }
|
buffer.count >= 2 else { return 0 }
|
||||||
print("got count: \(buffer.count)")
|
|
||||||
self.lastAttemptedMetaLength = buffer.count
|
self.lastAttemptedMetaLength = buffer.count
|
||||||
|
|
||||||
let lastPossibleCRIndex = buffer.index(before: buffer.index(before: buffer.endIndex))
|
let lastPossibleCRIndex = buffer.index(before: buffer.index(before: buffer.endIndex))
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
extension NWParameters {
|
extension NWParameters {
|
||||||
|
/// An NWParameters configured with the GeminiProtocol and appropriate TLS options.
|
||||||
|
/// This property always returns a new NWParameters instance.
|
||||||
static var gemini: NWParameters {
|
static var gemini: NWParameters {
|
||||||
let tlsOptions = geminiTLSOptions
|
let tlsOptions = geminiTLSOptions
|
||||||
let tcpOptions = NWProtocolTCP.Options()
|
let tcpOptions = NWProtocolTCP.Options()
|
||||||
|
|
Loading…
Reference in New Issue