Compare commits
6 Commits
255e5d7ff4
...
fccce078b2
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fccce078b2 | |
Shadowfacts | 984ecc8879 | |
Shadowfacts | 1ce48bc77e | |
Shadowfacts | 8c43bc8a44 | |
Shadowfacts | de68ecbe4b | |
Shadowfacts | 5d2fb53510 |
|
@ -128,11 +128,13 @@ class BrowserNavigationController: UIViewController {
|
||||||
|
|
||||||
private func onNavigate(_ operation: NavigationManager.Operation) {
|
private func onNavigate(_ operation: NavigationManager.Operation) {
|
||||||
let newVC: BrowserWebViewController
|
let newVC: BrowserWebViewController
|
||||||
|
var postAccessibilityNotification = false
|
||||||
|
|
||||||
switch operation {
|
switch operation {
|
||||||
case .go:
|
case .go:
|
||||||
backBrowserVCs.append(currentBrowserVC)
|
backBrowserVCs.append(currentBrowserVC)
|
||||||
newVC = BrowserWebViewController(navigator: navigator, url: navigator.currentURL)
|
newVC = BrowserWebViewController(navigator: navigator, url: navigator.currentURL)
|
||||||
|
postAccessibilityNotification = true
|
||||||
|
|
||||||
case .reload:
|
case .reload:
|
||||||
currentBrowserVC.reload()
|
currentBrowserVC.reload()
|
||||||
|
@ -163,6 +165,10 @@ class BrowserNavigationController: UIViewController {
|
||||||
self.toolbarOffset = 0
|
self.toolbarOffset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if postAccessibilityNotification {
|
||||||
|
// this moves focus to the nav bar, which isn't ideal, but it's better than nothing
|
||||||
|
UIAccessibility.post(notification: .screenChanged, argument: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let startEdgeNavigationSwipeDistance: CGFloat = 75
|
private let startEdgeNavigationSwipeDistance: CGFloat = 75
|
||||||
|
|
|
@ -151,6 +151,7 @@ class BrowserWebViewController: UIViewController {
|
||||||
func reload() {
|
func reload() {
|
||||||
loaded = false
|
loaded = false
|
||||||
loadedFallback = false
|
loadedFallback = false
|
||||||
|
document = nil
|
||||||
loadDocument()
|
loadDocument()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +190,10 @@ class BrowserWebViewController: UIViewController {
|
||||||
} else {
|
} else {
|
||||||
self.renderFallback(response: response)
|
self.renderFallback(response: response)
|
||||||
}
|
}
|
||||||
|
} else if response.status.isInput {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showInputPrompt(response: response)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.showError(message: "Unknown error: \(response.header)")
|
self.showError(message: "Unknown error: \(response.header)")
|
||||||
|
@ -208,6 +213,20 @@ class BrowserWebViewController: UIViewController {
|
||||||
errorMessageLabel.text = message
|
errorMessageLabel.text = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showInputPrompt(response: GeminiResponse) {
|
||||||
|
let alert = UIAlertController(title: "Input Requested", message: response.meta, preferredStyle: .alert)
|
||||||
|
alert.addTextField { field in
|
||||||
|
field.isSecureTextEntry = response.status == .sensitiveInput
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Submit", style: .default, handler: { _ in
|
||||||
|
guard var components = URLComponents(url: self.navigator.currentURL, resolvingAgainstBaseURL: false) else { return }
|
||||||
|
components.query = alert.textFields!.first!.text
|
||||||
|
self.navigator.changeURL(components.url!)
|
||||||
|
}))
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func renderDocument(_ doc: Document) {
|
private func renderDocument(_ doc: Document) {
|
||||||
self.document = doc
|
self.document = doc
|
||||||
|
|
||||||
|
@ -240,14 +259,20 @@ class BrowserWebViewController: UIViewController {
|
||||||
|
|
||||||
self.loadedFallback = true
|
self.loadedFallback = true
|
||||||
|
|
||||||
// todo: probably shouldn't assume this is UTF-8
|
if mimeType == "text/plain",
|
||||||
self.webView.load(body, mimeType: mimeType, characterEncodingName: "utf-8", baseURL: self.url)
|
let bodyText = response.bodyText {
|
||||||
|
let html = BrowserWebViewController.preamble + "<pre class='plaintext'>" + bodyText + "</pre>" + BrowserWebViewController.postamble
|
||||||
|
self.webView.loadHTMLString(html, baseURL: Bundle.main.bundleURL)
|
||||||
|
} else {
|
||||||
|
self.webView.load(body, mimeType: mimeType, characterEncodingName: response.encodingName ?? "utf-8", baseURL: self.url)
|
||||||
|
|
||||||
// When showing an image, the safe area insets seem to be ignored. This isn't perfect
|
// When showing an image, the safe area insets seem to be ignored. This isn't perfect
|
||||||
// (there's a little extra space between the bottom of the nav bar and the top of the image),
|
// (there's a little extra space between the bottom of the nav bar and the top of the image),
|
||||||
// but it's better than the image being obscured.
|
// but it's better than the image being obscured.
|
||||||
self.webView.scrollView.contentInset = self.webView.safeAreaInsets
|
self.webView.scrollView.contentInset = self.webView.safeAreaInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scrollToLine(index: Int, animated: Bool) {
|
func scrollToLine(index: Int, animated: Bool) {
|
||||||
if animated {
|
if animated {
|
||||||
|
@ -259,11 +284,21 @@ class BrowserWebViewController: UIViewController {
|
||||||
let y = result * scrollView.zoomScale - scrollView.safeAreaInsets.top
|
let y = result * scrollView.zoomScale - scrollView.safeAreaInsets.top
|
||||||
let maxY = scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom
|
let maxY = scrollView.contentSize.height - scrollView.bounds.height + scrollView.safeAreaInsets.bottom
|
||||||
let finalOffsetY = min(y, maxY)
|
let finalOffsetY = min(y, maxY)
|
||||||
self.webView.scrollView.setContentOffset(CGPoint(x: 0, y: finalOffsetY), animated: true)
|
UIView.animate(withDuration: 0.25, delay: 0, options: []) {
|
||||||
|
self.webView.scrollView.setContentOffset(CGPoint(x: 0, y: finalOffsetY), animated: false)
|
||||||
|
} completion: { _ in
|
||||||
|
// calling focus() causes VoiceOver to move to that element
|
||||||
|
self.webView.evaluateJavaScript("document.getElementById('l\(index)').focus();")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
webView.evaluateJavaScript("document.getElementById('l\(index)').scrollIntoView();")
|
webView.evaluateJavaScript("""
|
||||||
|
const el = document.getElementById('l\(index)');
|
||||||
|
el.scrollIntoView();
|
||||||
|
el.focus();
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let preamble = """
|
private static let preamble = """
|
||||||
|
|
|
@ -39,6 +39,7 @@ class NavigationBarView: UIView {
|
||||||
textField = UITextField()
|
textField = UITextField()
|
||||||
textField.text = navigator.displayURL
|
textField.text = navigator.displayURL
|
||||||
textField.borderStyle = .roundedRect
|
textField.borderStyle = .roundedRect
|
||||||
|
textField.textContentType = .URL
|
||||||
textField.keyboardType = .URL
|
textField.keyboardType = .URL
|
||||||
textField.returnKeyType = .go
|
textField.returnKeyType = .go
|
||||||
textField.autocapitalizationType = .none
|
textField.autocapitalizationType = .none
|
||||||
|
|
|
@ -14,6 +14,11 @@ pre {
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre.plaintext {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3 {
|
h1, h2, h3 {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
|
@ -14,6 +14,8 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
|
|
||||||
private var tempStatusCode: GeminiResponseHeader.StatusCode?
|
private var tempStatusCode: GeminiResponseHeader.StatusCode?
|
||||||
private var tempMeta: String?
|
private var tempMeta: String?
|
||||||
|
private var lastAttemptedMetaLength: Int?
|
||||||
|
private var lastFoundCR = false
|
||||||
|
|
||||||
required init(framer: NWProtocolFramer.Instance) {
|
required init(framer: NWProtocolFramer.Instance) {
|
||||||
}
|
}
|
||||||
|
@ -45,13 +47,21 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
|
|
||||||
var attemptedMetaLength: Int?
|
|
||||||
if tempMeta == nil {
|
if tempMeta == nil {
|
||||||
|
let min: Int
|
||||||
|
// if we previously tried to get the meta but failed (because the <CR><LF> was not found,
|
||||||
|
// the minimum amount we need before trying to parse is at least 1 or 2 (depending on whether we found the <CR>) bytes more
|
||||||
|
if let lastAttemptedMetaLength = lastAttemptedMetaLength {
|
||||||
|
min = lastAttemptedMetaLength + (lastFoundCR ? 1 : 2)
|
||||||
|
} else {
|
||||||
// Minimum length is 2 bytes, spec does not say meta string is required
|
// Minimum length is 2 bytes, spec does not say meta string is required
|
||||||
_ = framer.parseInput(minimumIncompleteLength: 2, maximumLength: 1024 + 2) { (buffer, isComplete) -> Int in
|
min = 2
|
||||||
|
}
|
||||||
|
_ = 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 }
|
||||||
attemptedMetaLength = buffer.count
|
print("got count: \(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))
|
||||||
var index = buffer.startIndex
|
var index = buffer.startIndex
|
||||||
|
@ -66,6 +76,10 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
|
if buffer[index] == 13 {
|
||||||
|
// if we found <CR>, but not <LF>, save that info so that next time we only wait for 1 more byte instead of 2
|
||||||
|
self.lastFoundCR = true
|
||||||
|
}
|
||||||
if buffer.count < 1026 {
|
if buffer.count < 1026 {
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,8 +92,8 @@ class GeminiProtocol: NWProtocolFramerImplementation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard let meta = tempMeta else {
|
guard let meta = tempMeta else {
|
||||||
if let attempted = attemptedMetaLength {
|
if let attempted = self.lastAttemptedMetaLength {
|
||||||
return attempted + 1
|
return attempted + (lastFoundCR ? 1 : 2)
|
||||||
} else {
|
} else {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,17 @@ public struct GeminiResponse {
|
||||||
return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first
|
return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var encodingName: String? {
|
||||||
|
guard let parameters = mimeTypeParameters else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return parameters["charset"]
|
||||||
|
}
|
||||||
|
|
||||||
public var bodyText: String? {
|
public var bodyText: String? {
|
||||||
guard let body = body, let parameters = mimeTypeParameters else { return nil }
|
guard let body = body else { return nil }
|
||||||
let encoding: String.Encoding
|
let encoding: String.Encoding
|
||||||
switch parameters["charset"]?.lowercased() {
|
switch encodingName?.lowercased() {
|
||||||
case nil, "utf-8":
|
case nil, "utf-8":
|
||||||
// The Gemini spec defines UTF-8 to be the default charset.
|
// The Gemini spec defines UTF-8 to be the default charset.
|
||||||
encoding = .utf8
|
encoding = .utf8
|
||||||
|
|
Loading…
Reference in New Issue