commit 832b401c9eb9911069d501cc61c2f75aed4137e5
parent e4c33ddc98a8865f21ed858a7bf7281d28e58755
Author: FIGBERT <figbert@figbert.com>
Date: Wed, 7 Sep 2022 10:55:37 -0700
Add secondary browser functionality
Diffstat:
7 files changed, 233 insertions(+), 63 deletions(-)
diff --git a/Gemenon.xcodeproj/project.pbxproj b/Gemenon.xcodeproj/project.pbxproj
@@ -7,6 +7,14 @@
objects = {
/* Begin PBXBuildFile section */
+ 3A00264A28C8340500625B2C /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264928C8340500625B2C /* HistoryView.swift */; };
+ 3A00264B28C8340500625B2C /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264928C8340500625B2C /* HistoryView.swift */; };
+ 3A00264D28C8350200625B2C /* BrowserFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264C28C8350200625B2C /* BrowserFunctions.swift */; };
+ 3A00264E28C8350200625B2C /* BrowserFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264C28C8350200625B2C /* BrowserFunctions.swift */; };
+ 3A00265028C8398A00625B2C /* CapsuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264F28C8398A00625B2C /* CapsuleView.swift */; };
+ 3A00265128C8398A00625B2C /* CapsuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00264F28C8398A00625B2C /* CapsuleView.swift */; };
+ 3A00265328C854A100625B2C /* StartPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00265228C854A100625B2C /* StartPage.swift */; };
+ 3A00265428C854A100625B2C /* StartPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A00265228C854A100625B2C /* StartPage.swift */; };
3AB5B67B28BF23FF00F0C1A5 /* GemenonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB5B66B28BF23FF00F0C1A5 /* GemenonApp.swift */; };
3AB5B67C28BF23FF00F0C1A5 /* GemenonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB5B66B28BF23FF00F0C1A5 /* GemenonApp.swift */; };
3AB5B67D28BF23FF00F0C1A5 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB5B66C28BF23FF00F0C1A5 /* ContentView.swift */; };
@@ -18,6 +26,10 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
+ 3A00264928C8340500625B2C /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
+ 3A00264C28C8350200625B2C /* BrowserFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFunctions.swift; sourceTree = "<group>"; };
+ 3A00264F28C8398A00625B2C /* CapsuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleView.swift; sourceTree = "<group>"; };
+ 3A00265228C854A100625B2C /* StartPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartPage.swift; sourceTree = "<group>"; };
3A1FABC828C6F91D00950AF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3AB5B66B28BF23FF00F0C1A5 /* GemenonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GemenonApp.swift; sourceTree = "<group>"; };
3AB5B66C28BF23FF00F0C1A5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -61,6 +73,10 @@
children = (
3AB5B66B28BF23FF00F0C1A5 /* GemenonApp.swift */,
3AB5B66C28BF23FF00F0C1A5 /* ContentView.swift */,
+ 3A00265228C854A100625B2C /* StartPage.swift */,
+ 3A00264F28C8398A00625B2C /* CapsuleView.swift */,
+ 3A00264928C8340500625B2C /* HistoryView.swift */,
+ 3A00264C28C8350200625B2C /* BrowserFunctions.swift */,
3AB5B66D28BF23FF00F0C1A5 /* Assets.xcassets */,
);
path = Shared;
@@ -191,6 +207,10 @@
buildActionMask = 2147483647;
files = (
3AB5B67D28BF23FF00F0C1A5 /* ContentView.swift in Sources */,
+ 3A00265328C854A100625B2C /* StartPage.swift in Sources */,
+ 3A00265028C8398A00625B2C /* CapsuleView.swift in Sources */,
+ 3A00264A28C8340500625B2C /* HistoryView.swift in Sources */,
+ 3A00264D28C8350200625B2C /* BrowserFunctions.swift in Sources */,
3AB5B67B28BF23FF00F0C1A5 /* GemenonApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -200,6 +220,10 @@
buildActionMask = 2147483647;
files = (
3AB5B67E28BF23FF00F0C1A5 /* ContentView.swift in Sources */,
+ 3A00265428C854A100625B2C /* StartPage.swift in Sources */,
+ 3A00265128C8398A00625B2C /* CapsuleView.swift in Sources */,
+ 3A00264B28C8340500625B2C /* HistoryView.swift in Sources */,
+ 3A00264E28C8350200625B2C /* BrowserFunctions.swift in Sources */,
3AB5B67C28BF23FF00F0C1A5 /* GemenonApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -390,7 +414,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "macOS/Info.plist";
+ INFOPLIST_FILE = macOS/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -419,7 +443,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "macOS/Info.plist";
+ INFOPLIST_FILE = macOS/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
diff --git a/Shared/BrowserFunctions.swift b/Shared/BrowserFunctions.swift
@@ -0,0 +1,31 @@
+//
+// BrowserFunctions.swift
+// Gemenon
+//
+// Created by figbert on 9/6/22.
+//
+
+import Foundation
+import SwiftGemini
+import SwiftGemtext
+
+class BrowserData: ObservableObject {
+ @Published var url: String = ""
+
+ @Published var history: [Date: URL] = [:]
+
+ @Published var tabs: [UUID: SwiftGemini.GeminiResponse] = [:]
+ @Published var currentTab: UUID? = nil
+
+ @Published var currentView: GemenonView = .StartPage
+ @Published var views: [GemenonView] = [.StartPage, .Capsule, .History]
+
+ @Published var parser = SwiftGemtext.Gemtext()
+ @Published var engine = SwiftGemini.GeminiRequestor()
+}
+
+enum GemenonView {
+ case StartPage
+ case Capsule
+ case History
+}
diff --git a/Shared/CapsuleView.swift b/Shared/CapsuleView.swift
@@ -0,0 +1,71 @@
+//
+// CapsuleView.swift
+// Gemenon
+//
+// Created by figbert on 9/6/22.
+//
+
+import SwiftUI
+
+struct CapsuleView: View {
+ @EnvironmentObject var data: BrowserData
+
+ var body: some View {
+ ScrollView {
+ HStack {
+ Spacer()
+ if data.currentTab != nil {
+ if data.tabs[data.currentTab!]?.body?.gemtext != nil {
+ VStack(alignment: .leading, spacing: 10) {
+ renderGemtext((data.tabs[data.currentTab!]?.body?.gemtext)!, url: (data.tabs[data.currentTab!]?.url)!)
+ }
+ }
+ }
+ Spacer()
+ }
+ .padding(.vertical)
+ }
+ }
+
+ func renderGemtext(_ gemtext: String, url: URL) -> some View {
+ ForEach(data.parser.parse(gemtext, url: url), id: \.self) { line in
+ switch line {
+ case .Text(let str):
+ Text(str)
+ case .Link(let url, let str):
+ if url.scheme == "gemini" {
+ Link(destination: url) {
+ Text(str != nil ? str! : url.absoluteString)
+ }
+ } else {
+ Link(destination: url) {
+ Text(str != nil ? str! : url.absoluteString)
+ }
+ .foregroundColor(.purple)
+ }
+ case .PreformattedText(let code, _):
+ Text(code).monospaced(true)
+ case .Heading(let level, let str):
+ if level == 1 {
+ Text(str).font(.title)
+ } else if level == 2 {
+ Text(str).font(.title2)
+ } else if level == 3 {
+ Text(str).font(.title3)
+ }
+ case .UnorderedList(let str):
+ Text("· \(str)")
+ case .Quote(let str):
+ Text("\"\(str)\"")
+ case .EmptyLine:
+ EmptyView()
+ }
+ }
+ }
+}
+
+struct CapsuleView_Previews: PreviewProvider {
+ static var previews: some View {
+ CapsuleView()
+ }
+}
diff --git a/Shared/ContentView.swift b/Shared/ContentView.swift
@@ -6,45 +6,54 @@
//
import SwiftUI
-import SwiftGemini
-import SwiftGemtext
struct ContentView: View {
- @State private var url = ""
- @State private var columnVisibility = NavigationSplitViewVisibility.detailOnly
+ @EnvironmentObject var data: BrowserData
- @State private var engine = SwiftGemini.GeminiRequestor()
- @State private var parser = SwiftGemtext.Gemtext()
- @State private var response: SwiftGemini.GeminiResponse?
+ @State private var columnVisibility = NavigationSplitViewVisibility.detailOnly
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
- Text("bookmarks")
- } detail: {
- ScrollView {
- HStack {
- Spacer()
- if response?.body?.gemtext != nil {
- VStack(alignment: .leading, spacing: 10) {
- renderGemtext()
- }
- } else {
- Text("foo")
+ List(data.views, id: \.self, selection: $data.currentView) { s in
+ NavigationLink {
+ switch s {
+ case .Capsule:
+ CapsuleView()
+ case .History:
+ HistoryView()
+ case .StartPage:
+ StartPage()
+ }
+ } label: {
+ switch s {
+ case .Capsule:
+ Label("\(data.tabs.count) Tab\(data.tabs.count > 1 || data.tabs.isEmpty ? "s" : "")", systemImage: "laptopcomputer")
+ case .History:
+ Label("History", systemImage: "clock")
+ case .StartPage:
+ Label("Start Page", systemImage: "house")
}
- Spacer()
}
- .padding(.vertical)
+ }
+ } detail: {
+ switch data.currentView {
+ case .StartPage:
+ StartPage()
+ case .Capsule:
+ CapsuleView()
+ case .History:
+ HistoryView()
}
}
.toolbar {
ToolbarItem(placement: .principal) {
- TextField("Search or enter website name", text: $url)
+ TextField("Search or enter website name", text: $data.url)
.frame(minWidth: 500)
.textFieldStyle(.roundedBorder)
.autocorrectionDisabled(true)
.onSubmit {
Task {
- if let url = URL(string: url) {
+ if let url = URL(string: data.url) {
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
if components.scheme?.isEmpty ?? true {
if components.host?.isEmpty ?? true {
@@ -55,14 +64,14 @@ struct ContentView: View {
components.scheme = "gemini"
}
if let url = components.url {
- response = try! await engine.request(url)
+ await openURL(url)
}
}
} else {
var components = URLComponents(string: "gemini://geminispace.info/search")
- components?.query = url
+ components?.query = data.url
if let url = components?.url {
- response = try! await engine.request(url)
+ await openURL(url)
}
}
}
@@ -71,46 +80,21 @@ struct ContentView: View {
}
.handlesExternalEvents(preferring: ["gemini://*"], allowing: ["*"])
.onOpenURL(perform: { url in
- Task {
- response = try! await engine.request(url)
- }
+ Task { await openURL(url) }
})
}
- func renderGemtext() -> some View {
- ForEach(parser.parse(response!.body!.gemtext!, url: response!.url), id: \.self) { line in
- switch line {
- case .Text(let str):
- Text(str)
- case .Link(let url, let str):
- if url.scheme == "gemini" {
- Link(destination: url) {
- Text(str != nil ? str! : url.absoluteString)
- }
- } else {
- Link(destination: url) {
- Text(str != nil ? str! : url.absoluteString)
- }
- .foregroundColor(.purple)
- }
- case .PreformattedText(let code, _):
- Text(code).monospaced(true)
- case .Heading(let level, let str):
- if level == 1 {
- Text(str).font(.title)
- } else if level == 2 {
- Text(str).font(.title2)
- } else if level == 3 {
- Text(str).font(.title3)
- }
- case .UnorderedList(let str):
- Text("· \(str)")
- case .Quote(let str):
- Text("\"\(str)\"")
- case .EmptyLine:
- EmptyView()
- }
+ func openURL(_ url: URL) async {
+ var uuid = UUID()
+ let response = try! await data.engine.request(url)
+ if data.tabs.count == 1 {
+ uuid = data.tabs.keys.first!
}
+ data.tabs[uuid] = response
+ data.currentTab = uuid
+ data.history[Date.now] = url
+ data.currentView = .Capsule
+ data.url = url.absoluteString
}
}
diff --git a/Shared/GemenonApp.swift b/Shared/GemenonApp.swift
@@ -9,9 +9,12 @@ import SwiftUI
@main
struct GemenonApp: App {
+ @StateObject private var data = BrowserData()
+
var body: some Scene {
WindowGroup {
ContentView()
+ .environmentObject(data)
}
.windowToolbarStyle(.unified(showsTitle: false))
}
diff --git a/Shared/HistoryView.swift b/Shared/HistoryView.swift
@@ -0,0 +1,30 @@
+//
+// HistoryView.swift
+// Gemenon
+//
+// Created by figbert on 9/6/22.
+//
+
+import SwiftUI
+
+struct HistoryView: View {
+ @EnvironmentObject var data: BrowserData
+
+ var body: some View {
+ List {
+ ForEach(data.history.sorted(by: { first, second in first.key > second.key }), id: \.key) { date, url in
+ HStack {
+ Text(url.absoluteString)
+ Spacer()
+ Text(date.formatted(date: .long, time: .shortened))
+ }
+ }
+ }
+ }
+}
+
+struct HistoryView_Previews: PreviewProvider {
+ static var previews: some View {
+ HistoryView()
+ }
+}
diff --git a/Shared/StartPage.swift b/Shared/StartPage.swift
@@ -0,0 +1,27 @@
+//
+// StartPage.swift
+// Gemenon
+//
+// Created by figbert on 9/6/22.
+//
+
+import SwiftUI
+
+struct StartPage: View {
+ var body: some View {
+ VStack(alignment: .center) {
+ Spacer()
+ Text("Gemenon")
+ .font(.largeTitle)
+ Text("The Safari of the Gemini ecosystem")
+ .font(.subheadline)
+ Spacer()
+ }
+ }
+}
+
+struct StartPage_Previews: PreviewProvider {
+ static var previews: some View {
+ StartPage()
+ }
+}