gemenon

[ACTIVE] The Safari of the Gemini ecosystem
git clone git://git.figbert.com/gemenon.git
Log | Files | Refs

commit 832b401c9eb9911069d501cc61c2f75aed4137e5
parent e4c33ddc98a8865f21ed858a7bf7281d28e58755
Author: FIGBERT <figbert@figbert.com>
Date:   Wed,  7 Sep 2022 10:55:37 -0700

Add secondary browser functionality

Diffstat:
MGemenon.xcodeproj/project.pbxproj | 28++++++++++++++++++++++++++--
AShared/BrowserFunctions.swift | 31+++++++++++++++++++++++++++++++
AShared/CapsuleView.swift | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MShared/ContentView.swift | 106++++++++++++++++++++++++++++++++++---------------------------------------------
MShared/GemenonApp.swift | 3+++
AShared/HistoryView.swift | 30++++++++++++++++++++++++++++++
AShared/StartPage.swift | 27+++++++++++++++++++++++++++
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() + } +}