Compare commits

...

15 Commits

Author SHA1 Message Date
Shadowfacts 203bd1804f
iOS: Fix crash showing share sheet on iPad 2020-09-30 20:28:08 -04:00
Shadowfacts 364ffe9f94
iOS: Add pointer effects to various buttons 2020-09-30 18:14:38 -04:00
Shadowfacts 4f3e1432e7
iOS: Add accessibility labels to toolbar buttons 2020-09-30 18:10:36 -04:00
Shadowfacts 89b226e321
iOS: Improve link contrast in dark mode 2020-09-30 18:03:07 -04:00
Shadowfacts 107c4b0d72
macOS: Fix compilation 2020-09-30 18:02:22 -04:00
Shadowfacts 83dad76b82
iOS: Add theme override setting 2020-09-29 23:44:44 -04:00
Shadowfacts b000f1c2b3
iOS: Add back/forward history context menus 2020-09-29 23:36:24 -04:00
Shadowfacts 01a3eaf17f
Bump build number 2020-09-29 16:47:00 -04:00
Shadowfacts 8a895b70c8
Tweak link icons 2020-09-29 16:28:37 -04:00
Shadowfacts 19848ba8e4
Don't send requests with empty paths 2020-09-29 16:28:17 -04:00
Shadowfacts 182bb4b79b
Prevent potential redirect loops 2020-09-29 16:28:05 -04:00
Shadowfacts 71b6352395
Renderer: Add link icons 2020-09-29 15:30:10 -04:00
Shadowfacts 1449dc215b
iOS: Fix toolbars disappearing when scrolling too far up 2020-09-29 15:29:58 -04:00
Shadowfacts 012ada4af7
iOS: Actually save preferences 2020-09-29 15:29:12 -04:00
Shadowfacts 57023d204d
iOS: Go back to full SwiftUI
Embedding a UIHostingController inside a UIScrollView prevents
LazyVStack from working, causing very bad performance on some pages
2020-09-29 15:28:33 -04:00
17 changed files with 264 additions and 72 deletions

View File

@ -39,6 +39,7 @@ public struct BrowserView: View {
Text("An error occurred") Text("An error occurred")
.font(.headline) .font(.headline)
Text(message) Text(message)
.lineLimit(nil)
case let .document(doc): case let .document(doc):
DocumentView(document: doc, scrollingEnabled: scrollingEnabled, changeURL: navigator.changeURL) DocumentView(document: doc, scrollingEnabled: scrollingEnabled, changeURL: navigator.changeURL)
Spacer() Spacer()

View File

@ -19,6 +19,14 @@ public class NavigationManager: NSObject, ObservableObject {
@Published public var backStack = [URL]() @Published public var backStack = [URL]()
@Published public var forwardStack = [URL]() @Published public var forwardStack = [URL]()
public var displayURL: String {
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
if components.port == 1965 {
components.port = nil
}
return components.string!
}
public init(url: URL) { public init(url: URL) {
self.currentURL = url self.currentURL = url
} }
@ -30,9 +38,6 @@ public class NavigationManager: NSObject, ObservableObject {
delegate?.loadNonGeminiURL(url) delegate?.loadNonGeminiURL(url)
return return
} }
if components.port == 1965 {
components.port = nil
}
} else { } else {
components.scheme = "gemini" components.scheme = "gemini"
} }
@ -43,6 +48,11 @@ public class NavigationManager: NSObject, ObservableObject {
components.path = "/" components.path = "/"
} }
// Some Gemini servers break on empty paths
if components.path.isEmpty {
components.path = "/"
}
let url = components.url! let url = components.url!
backStack.append(currentURL) backStack.append(currentURL)
@ -61,10 +71,28 @@ public class NavigationManager: NSObject, ObservableObject {
currentURL = backStack.removeLast() currentURL = backStack.removeLast()
} }
public func back(count: Int) {
guard count <= backStack.count else { return }
var removed = backStack.suffix(count)
backStack.removeLast(count)
forwardStack.insert(currentURL, at: 0)
currentURL = removed.removeFirst()
forwardStack.insert(contentsOf: removed, at: 0)
}
@objc public func forward() { @objc public func forward() {
guard !forwardStack.isEmpty else { return } guard !forwardStack.isEmpty else { return }
backStack.append(currentURL) backStack.append(currentURL)
currentURL = forwardStack.removeFirst() currentURL = forwardStack.removeFirst()
} }
public func forward(count: Int) {
guard count <= forwardStack.count else { return }
var removed = forwardStack.prefix(count)
forwardStack.removeFirst(count)
backStack.append(currentURL)
currentURL = removed.removeLast()
backStack.append(contentsOf: removed)
}
} }

View File

@ -0,0 +1,24 @@
//
// ActivityView.swift
// Gemini-iOS
//
// Created by Shadowfacts on 9/30/20.
//
import SwiftUI
struct ActivityView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIActivityViewController
let items: [Any]
let activities: [UIActivity]?
func makeUIViewController(context: Context) -> UIActivityViewController {
return UIActivityViewController(activityItems: items, applicationActivities: activities)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
}
}

View File

@ -81,10 +81,7 @@ class BrowserViewController: UIViewController, UIScrollViewDelegate {
navBarHost.view.topAnchor.constraint(equalTo: view.topAnchor), navBarHost.view.topAnchor.constraint(equalTo: view.topAnchor),
]) ])
toolBarHost = UIHostingController(rootView: ToolBar(navigator: navigator, shareCurrentURL: { toolBarHost = UIHostingController(rootView: ToolBar(navigator: navigator))
let vc = UIActivityViewController(activityItems: [self.navigator.currentURL], applicationActivities: nil)
self.present(vc, animated: true)
}))
toolBarHost.view.translatesAutoresizingMaskIntoConstraints = false toolBarHost.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(toolBarHost.view) view.addSubview(toolBarHost.view)
addChild(toolBarHost) addChild(toolBarHost)

View File

@ -8,13 +8,9 @@
import SwiftUI import SwiftUI
import BrowserCore import BrowserCore
// This is not currently used as SwiftUI's ScrollView has no mechanism for detecting when it stops deceleraing,
// which is necessary to preven tthe bars from being left in a partially visible state.
struct ContentView: View { struct ContentView: View {
@ObservedObject private var navigator: NavigationManager @ObservedObject private var navigator: NavigationManager
@State private var urlFieldContents: String @State private var urlFieldContents: String
@State private var showPreferencesSheet = false
private let shareCurrentURL: () -> Void
@State private var prevScrollOffset: CGFloat = 0 @State private var prevScrollOffset: CGFloat = 0
@State private var scrollOffset: CGFloat = 0 { @State private var scrollOffset: CGFloat = 0 {
didSet { didSet {
@ -24,11 +20,11 @@ struct ContentView: View {
@State private var barOffset: CGFloat = 0 @State private var barOffset: CGFloat = 0
@State private var navBarHeight: CGFloat = 0 @State private var navBarHeight: CGFloat = 0
@State private var toolBarHeight: CGFloat = 0 @State private var toolBarHeight: CGFloat = 0
@State private var showShareSheet = false
init(navigator: NavigationManager, shareCurrentURL: @escaping () -> Void) { init(navigator: NavigationManager) {
self.navigator = navigator self.navigator = navigator
self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString) self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString)
self.shareCurrentURL = shareCurrentURL
} }
var body: some View { var body: some View {
@ -52,11 +48,16 @@ struct ContentView: View {
// It's not actually user scrolling, and this screws up our animation, so we ignore it. // It's not actually user scrolling, and this screws up our animation, so we ignore it.
guard abs(delta) != outer.safeAreaInsets.top else { return } guard abs(delta) != outer.safeAreaInsets.top else { return }
if delta != 0 { if scrollOffset < 0 {
barOffset += delta barOffset = 0
} else {
if delta != 0 {
barOffset += delta
}
print(barOffset)
barOffset = max(0, min(navBarHeight + outer.safeAreaInsets.top, barOffset))
} }
barOffset = max(0, min(navBarHeight + outer.safeAreaInsets.top, barOffset))
} }
} }
@ -66,10 +67,10 @@ struct ContentView: View {
Color.clear.preference(key: NavBarHeightPrefKey.self, value: geom.frame(in: .global).height) Color.clear.preference(key: NavBarHeightPrefKey.self, value: geom.frame(in: .global).height)
}) })
.offset(y: -barOffset) .offset(y: -barOffset)
Spacer() Spacer()
ToolBar(navigator: navigator, shareCurrentURL: shareCurrentURL) ToolBar(navigator: navigator, showShareSheet: $showShareSheet)
.background(GeometryReader { (geom: GeometryProxy) in .background(GeometryReader { (geom: GeometryProxy) in
Color.clear.preference(key: ToolBarHeightPrefKey.self, value: geom.frame(in: .global).height) Color.clear.preference(key: ToolBarHeightPrefKey.self, value: geom.frame(in: .global).height)
}) })
@ -88,9 +89,9 @@ struct ContentView: View {
.onReceive(navigator.$currentURL, perform: { (new) in .onReceive(navigator.$currentURL, perform: { (new) in
urlFieldContents = new.absoluteString urlFieldContents = new.absoluteString
}) })
.sheet(isPresented: $showPreferencesSheet, content: { .sheet(isPresented: $showShareSheet) {
PreferencesView(presented: $showPreferencesSheet) ActivityView(items: [navigator.currentURL], activities: nil)
}) }
} }
private func tweakAppearance() { private func tweakAppearance() {
@ -133,6 +134,6 @@ fileprivate enum ScrollDirection {
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ContentView(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), shareCurrentURL: {}) ContentView(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!))
} }
} }

View File

@ -16,7 +16,7 @@ struct NavigationBar: View {
init(navigator: NavigationManager) { init(navigator: NavigationManager) {
self.navigator = navigator self.navigator = navigator
self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString) self._urlFieldContents = State(initialValue: navigator.displayURL)
} }
var body: some View { var body: some View {
@ -33,8 +33,8 @@ struct NavigationBar: View {
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75)) .foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
} }
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.top)) .background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.top))
.onReceive(navigator.$currentURL) { (newURL) in .onReceive(navigator.$currentURL) { (_) in
urlFieldContents = newURL.absoluteString urlFieldContents = navigator.displayURL
} }
} }

View File

@ -5,7 +5,7 @@
// Created by Shadowfacts on 9/27/20. // Created by Shadowfacts on 9/27/20.
// //
import Foundation import UIKit
class Preferences: Codable, ObservableObject { class Preferences: Codable, ObservableObject {
@ -34,6 +34,8 @@ class Preferences: Codable, ObservableObject {
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
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)
} }
@ -41,16 +43,25 @@ class Preferences: Codable, ObservableObject {
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(theme, forKey: .theme)
try container.encode(useInAppSafari, forKey: .useInAppSafari) try container.encode(useInAppSafari, forKey: .useInAppSafari)
try container.encode(useReaderMode, forKey: .useReaderMode) try container.encode(useReaderMode, forKey: .useReaderMode)
} }
@Published var theme = UIUserInterfaceStyle.unspecified
@Published var useInAppSafari = false @Published var useInAppSafari = false
@Published var useReaderMode = false @Published var useReaderMode = false
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case theme
case useInAppSafari case useInAppSafari
case useReaderMode case useReaderMode
} }
} }
extension UIUserInterfaceStyle: Codable {}

View File

@ -10,25 +10,41 @@ import SwiftUI
struct PreferencesView: View { struct PreferencesView: View {
@ObservedObject var preferences: Preferences = .shared @ObservedObject var preferences: Preferences = .shared
@Binding var presented: Bool @Environment(\.presentationMode) @Binding var presentationMode: PresentationMode
var body: some View { var body: some View {
NavigationView { NavigationView {
List { List {
appearanceSection
safariSection safariSection
} }
.navigationBarTitle("Preferences") .navigationBarTitle("Preferences")
.insetOrGroupedListStyle() .insetOrGroupedListStyle()
.navigationBarItems(trailing: doneButton) .navigationBarItems(trailing: doneButton)
} }
.onDisappear {
Preferences.save()
}
} }
private var doneButton: some View { private var doneButton: some View {
Button(action: { Button(action: {
presented = false presentationMode.dismiss()
}, label: { }, label: {
Text("Done") Text("Done")
}) })
.hoverEffect(.highlight)
}
private var appearanceSection: some View {
Section(header: Text("Appearance")) {
Picker(selection: $preferences.theme, label: Text("Theme")) {
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
Text("Always Light").tag(UIUserInterfaceStyle.light)
Text("Always Dark").tag(UIUserInterfaceStyle.dark)
}
}
} }
private var safariSection: some View { private var safariSection: some View {
@ -56,6 +72,6 @@ struct PreferencesView_Previews: PreviewProvider {
@State static var presented = true @State static var presented = true
static var previews: some View { static var previews: some View {
PreferencesView(presented: $presented) PreferencesView()
} }
} }

View File

@ -9,12 +9,15 @@ import UIKit
import SwiftUI import SwiftUI
import BrowserCore import BrowserCore
import SafariServices import SafariServices
import Combine
class SceneDelegate: UIResponder, UIWindowSceneDelegate { class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? var window: UIWindow?
var navigationManager: NavigationManager! var navigationManager: NavigationManager!
private var cancellables = [AnyCancellable]()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// 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`.
@ -37,16 +40,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
navigationManager.delegate = self navigationManager.delegate = self
// Create the SwiftUI view that provides the window contents. // Create the SwiftUI view that provides the window contents.
// let contentView = ContentView(navigator: navigationManager, shareCurrentURL: self.shareCurrentURL) let contentView = ContentView(navigator: navigationManager)
// Use a UIHostingController as window root view controller. // Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene { if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene) let window = UIWindow(windowScene: windowScene)
// window.rootViewController = UIHostingController(rootView: contentView) window.overrideUserInterfaceStyle = Preferences.shared.theme
window.rootViewController = BrowserViewController(navigator: navigationManager) window.rootViewController = UIHostingController(rootView: contentView)
// window.rootViewController = BrowserViewController(navigator: navigationManager)
self.window = window self.window = window
window.makeKeyAndVisible() window.makeKeyAndVisible()
} }
Preferences.shared.$theme
.sink { (newStyle) in
self.window!.overrideUserInterfaceStyle = newStyle
}
.store(in: &cancellables)
} }
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
@ -82,11 +92,6 @@ 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.
} }
private func shareCurrentURL() {
let vc = UIActivityViewController(activityItems: [navigationManager.currentURL], applicationActivities: nil)
window?.rootViewController?.present(vc, animated: true)
}
} }

View File

@ -10,7 +10,7 @@ import BrowserCore
struct ToolBar: View { struct ToolBar: View {
@ObservedObject var navigator: NavigationManager @ObservedObject var navigator: NavigationManager
let shareCurrentURL: () -> Void @Binding var showShareSheet: Bool
@State private var showPreferencesSheet = false @State private var showPreferencesSheet = false
@Environment(\.colorScheme) var colorScheme: ColorScheme @Environment(\.colorScheme) var colorScheme: ColorScheme
@ -30,6 +30,17 @@ struct ToolBar: View {
Image(systemName: "arrow.left") Image(systemName: "arrow.left")
.font(.system(size: 24)) .font(.system(size: 24))
} }
.accessibility(label: Text("Back"))
.hoverEffect(.highlight)
.contextMenu {
ForEach(Array(navigator.backStack.suffix(5).enumerated()), id: \.1) { (index, url) in
Button {
navigator.back(count: min(5, navigator.backStack.count) - index)
} label: {
Text(verbatim: urlForDisplay(url))
}
}
}
.disabled(navigator.backStack.isEmpty) .disabled(navigator.backStack.isEmpty)
Spacer() Spacer()
@ -38,6 +49,17 @@ struct ToolBar: View {
Image(systemName: "arrow.right") Image(systemName: "arrow.right")
.font(.system(size: 24)) .font(.system(size: 24))
} }
.accessibility(label: Text("Forward"))
.hoverEffect(.highlight)
.contextMenu {
ForEach(navigator.forwardStack.prefix(5).enumerated().reversed(), id: \.1) { (index, url) in
Button {
navigator.forward(count: index + 1)
} label: {
Text(verbatim: urlForDisplay(url))
}
}
}
.disabled(navigator.forwardStack.isEmpty) .disabled(navigator.forwardStack.isEmpty)
Spacer() Spacer()
@ -46,13 +68,19 @@ struct ToolBar: View {
Image(systemName: "arrow.clockwise") Image(systemName: "arrow.clockwise")
.font(.system(size: 24)) .font(.system(size: 24))
} }
.accessibility(label: Text("Reload"))
.hoverEffect(.highlight)
Spacer() Spacer()
Button(action: shareCurrentURL) { Button {
showShareSheet = true
} label: {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
.font(.system(size: 24)) .font(.system(size: 24))
} }
.accessibility(label: Text("Share"))
.hoverEffect(.highlight)
Spacer() Spacer()
@ -62,22 +90,36 @@ struct ToolBar: View {
Image(systemName: "gear") Image(systemName: "gear")
.font(.system(size: 24)) .font(.system(size: 24))
}) })
.accessibility(label: Text("Preferences"))
.hoverEffect(.highlight)
} }
Spacer() Spacer()
} }
Spacer(minLength: 4)
} }
.padding(.bottom, 4)
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.bottom)) .background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.bottom))
.sheet(isPresented: $showPreferencesSheet, content: { .sheet(isPresented: $showPreferencesSheet, content: {
PreferencesView(presented: $showPreferencesSheet) PreferencesView()
}) })
} }
private func urlForDisplay(_ url: URL) -> String {
var str = url.host!
if let port = url.port,
url.scheme != "gemini" || port != 1965 {
str += ":\(port)"
}
str += url.path
return str
}
} }
struct ToolBar_Previews: PreviewProvider { struct ToolBar_Previews: PreviewProvider {
@State private static var showShareSheet = false
static var previews: some View { static var previews: some View {
ToolBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), shareCurrentURL: {}) ToolBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), showShareSheet: $showShareSheet)
} }
} }

View File

@ -36,16 +36,17 @@
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; }; D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; };
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; }; D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; }; D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; };
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62BCEE1252553620031D894 /* ActivityView.swift */; };
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; }; D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; }; D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; }; D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
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 */; };
D691A6772522382E00348C4B /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A6762522382E00348C4B /* BrowserViewController.swift */; };
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; }; D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A69F252242FC00348C4B /* ToolBar.swift */; }; D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A69F252242FC00348C4B /* ToolBar.swift */; };
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; }; D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.swift */; }; D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.swift */; };
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DA5782252396030048B65A /* View+Extensions.swift */; };
D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; }; D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; };
D6E1529924BFAAA400FDF9D3 /* BrowserWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */; }; D6E1529924BFAAA400FDF9D3 /* BrowserWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */; };
D6E1529B24BFAEC700FDF9D3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */; }; D6E1529B24BFAEC700FDF9D3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */; };
@ -293,6 +294,7 @@
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; }; D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; };
D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; }; D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; };
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; }; D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; };
D62BCEE1252553620031D894 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; }; D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; }; D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; }; D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
@ -304,6 +306,7 @@
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = "<group>"; }; D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = "<group>"; };
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; }; D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; };
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = "<group>"; }; D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = "<group>"; };
D6DA5782252396030048B65A /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowController.swift; sourceTree = "<group>"; }; D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowController.swift; sourceTree = "<group>"; };
D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowserWindowController.xib; sourceTree = "<group>"; }; D6E1529724BFAAA400FDF9D3 /* BrowserWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrowserWindowController.xib; sourceTree = "<group>"; };
D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; }; D6E1529A24BFAEC700FDF9D3 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
@ -526,6 +529,7 @@
D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */, D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */,
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */, D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
D664673724BD086F00B0B741 /* RenderingBlockView.swift */, D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
D6DA5782252396030048B65A /* View+Extensions.swift */,
); );
path = GeminiRenderer; path = GeminiRenderer;
sourceTree = "<group>"; sourceTree = "<group>";
@ -557,6 +561,7 @@
D691A69F252242FC00348C4B /* ToolBar.swift */, D691A69F252242FC00348C4B /* ToolBar.swift */,
D691A64D25217C6F00348C4B /* Preferences.swift */, D691A64D25217C6F00348C4B /* Preferences.swift */,
D691A66625217FD800348C4B /* PreferencesView.swift */, D691A66625217FD800348C4B /* PreferencesView.swift */,
D62BCEE1252553620031D894 /* ActivityView.swift */,
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */, D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */, D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
D6E152B224BFFDF600FDF9D3 /* Info.plist */, D6E152B224BFFDF600FDF9D3 /* Info.plist */,
@ -1042,6 +1047,7 @@
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */, D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */,
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 */,
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */, D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -1059,10 +1065,10 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */, D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
D691A6772522382E00348C4B /* BrowserViewController.swift in Sources */,
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */, D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */, D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */, D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */,
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */, D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */, D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */, D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
@ -1325,7 +1331,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements; CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements;
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"Gemini/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Gemini/Preview Content\"";
@ -1351,7 +1357,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements; CODE_SIGN_ENTITLEMENTS = Gemini/Gemini.entitlements;
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"Gemini/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Gemini/Preview Content\"";
@ -1692,7 +1698,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"Gemini-iOS/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Gemini-iOS/Preview Content\"";
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1718,7 +1724,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"Gemini-iOS/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Gemini-iOS/Preview Content\"";
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;

View File

@ -59,7 +59,7 @@
<EnvironmentVariable <EnvironmentVariable
key = "DEFAULT_URL" key = "DEFAULT_URL"
value = "gemini://drewdevault.com" value = "gemini://drewdevault.com"
isEnabled = "YES"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>

View File

@ -27,12 +27,12 @@
<key>GeminiProtocol.xcscheme_^#shared#^_</key> <key>GeminiProtocol.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>4</integer>
</dict> </dict>
<key>GeminiRenderer.xcscheme_^#shared#^_</key> <key>GeminiRenderer.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>3</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -108,8 +108,8 @@ extension BrowserWindowController: NSToolbarDelegate {
item.label = "Go Back" item.label = "Go Back"
item.paletteLabel = "Go Back" item.paletteLabel = "Go Back"
item.toolTip = "Go to the previous page" item.toolTip = "Go to the previous page"
item.target = navigator item.target = self
item.action = #selector(NavigationManager.back) item.action = #selector(back)
item.isBordered = true item.isBordered = true
if #available(macOS 10.16, *) { if #available(macOS 10.16, *) {
item.isNavigational = true item.isNavigational = true
@ -127,8 +127,8 @@ extension BrowserWindowController: NSToolbarDelegate {
item.label = "Go Forward" item.label = "Go Forward"
item.paletteLabel = "Go Forward" item.paletteLabel = "Go Forward"
item.toolTip = "Go to the next page" item.toolTip = "Go to the next page"
item.target = navigator item.target = self
item.action = #selector(NavigationManager.forward) item.action = #selector(forward)
item.isBordered = true item.isBordered = true
if #available(macOS 10.16, *) { if #available(macOS 10.16, *) {
item.isNavigational = true item.isNavigational = true
@ -136,14 +136,22 @@ extension BrowserWindowController: NSToolbarDelegate {
return item return item
} }
@objc private func back() {
navigator.back()
}
@objc private func forward() {
navigator.forward()
}
} }
extension NavigationManager: NSToolbarItemValidation { extension BrowserWindowController: NSToolbarItemValidation {
public func validateToolbarItem(_ item: NSToolbarItem) -> Bool { public func validateToolbarItem(_ item: NSToolbarItem) -> Bool {
if item.itemIdentifier == .goBack { if item.itemIdentifier == .goBack {
return !backStack.isEmpty return !navigator.backStack.isEmpty
} else if item.itemIdentifier == .goForward { } else if item.itemIdentifier == .goForward {
return !forwardStack.isEmpty return !navigator.forwardStack.isEmpty
} else { } else {
return true return true
} }

View File

@ -35,7 +35,7 @@ public struct DocumentView: View {
private var scrollBody: some View { private var scrollBody: some View {
MaybeLazyVStack(alignment: .leading) { MaybeLazyVStack(alignment: .leading) {
ForEach(blocks.indices) { (index) in ForEach(blocks.indices) { (index) in
RenderingBlockView(block: blocks[index], changeURL: changeURL) RenderingBlockView(document: document, block: blocks[index], changeURL: changeURL)
} }
}.padding([.leading, .trailing, .bottom]) }.padding([.leading, .trailing, .bottom])
} }

View File

@ -9,11 +9,15 @@ import SwiftUI
import GeminiFormat import GeminiFormat
struct RenderingBlockView: View { struct RenderingBlockView: View {
let document: Document
let block: RenderingBlock let block: RenderingBlock
let changeURL: ((URL) -> Void)? let changeURL: ((URL) -> Void)?
@State var hovering = false @State var hovering = false
init(block: RenderingBlock, changeURL: ((URL) -> Void)? = nil) { @Environment(\.colorScheme) var colorScheme: ColorScheme
init(document: Document, block: RenderingBlock, changeURL: ((URL) -> Void)? = nil) {
self.document = document
self.block = block self.block = block
self.changeURL = changeURL self.changeURL = changeURL
} }
@ -37,7 +41,7 @@ struct RenderingBlockView: View {
} }
private func text(_ text: String) -> some View { private func text(_ text: String) -> some View {
Text(text) Text(verbatim: text)
.font(.documentBody) .font(.documentBody)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@ -51,24 +55,50 @@ struct RenderingBlockView: View {
let buttonStyle = PlainButtonStyle() let buttonStyle = PlainButtonStyle()
#endif #endif
let imageName: String
if url.scheme == "gemini" {
if url.host == document.url.host {
imageName = "arrow.right"
} else {
imageName = "link"
}
} else if url.scheme == "http" || url.scheme == "https" {
imageName = "safari"
} else if url.scheme == "mailto" {
imageName = "envelope"
} else {
imageName = "arrow.up.left.square"
}
let button: some View = Button { let button: some View = Button {
self.changeURL?(url) self.changeURL?(url)
} label: { } label: {
Text(verbatim: text) HStack(alignment: .firstTextBaseline, spacing: 4) {
.font(.documentBody) maybeLinkImage(name: imageName)
.foregroundColor(hovering ? .blue : Color.blue.opacity(0.8))
.underline() Text(verbatim: text)
.frame(maxWidth: .infinity, alignment: .leading) .font(.documentBody)
.foregroundColor(colorScheme == .dark ?
hovering ? Color.blue.opacity(0.8) : .blue :
hovering ? .blue : Color.blue.opacity(0.8))
.underline()
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.buttonStyle(buttonStyle) .buttonStyle(buttonStyle)
.onHover { hovering in .onHover { hovering in
self.hovering = hovering self.hovering = hovering
} }
if #available(macOS 10.16, iOS 14.0, *) { return button.maybeHelp(url.absoluteString)
return AnyView(button.help(url.absoluteString)) }
private func maybeLinkImage(name: String) -> AnyView {
// can't use availability check inside view body, since buildLimitedAvailability was introduced in iOS 14 :/
if #available(iOS 13.0, macOS 11.0, *) {
return AnyView(Image(systemName: name).frame(minWidth: 23, alignment: .leading))
} else { } else {
return AnyView(button) return AnyView(EmptyView())
} }
} }
@ -109,10 +139,12 @@ struct RenderingBlockView: View {
} }
struct RenderingBlockView_Previews: PreviewProvider { struct RenderingBlockView_Previews: PreviewProvider {
static let doc = Document(url: URL(string: "gemini://localhost/test.gmi")!, lines: [.text("Some Text"), .quote("A Quote")])
static var previews: some View { static var previews: some View {
Group { Group {
RenderingBlockView(block: .text("Some Text")) RenderingBlockView(document: doc, block: .text("Some Text"))
RenderingBlockView(block: .quote("A Quote")) RenderingBlockView(document: doc, block: .quote("A Quote"))
} }
} }
} }

View File

@ -0,0 +1,21 @@
//
// View+Extensions.swift
// GeminiRenderer
//
// Created by Shadowfacts on 9/29/20.
//
import SwiftUI
extension View {
@ViewBuilder
func maybeHelp(_ help: String) -> some View {
if #available(iOS 14.0, macOS 11.0, *) {
self.help(help)
} else {
self
}
}
}