aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.swift-format5
-rw-r--r--iGopherBrowser/BookmarksView.swift32
-rw-r--r--iGopherBrowser/BrowserView.swift1001
-rw-r--r--iGopherBrowser/ContentView.swift64
-rw-r--r--iGopherBrowser/FileView.swift254
-rw-r--r--iGopherBrowser/Helpers.swift54
-rw-r--r--iGopherBrowser/SearchInputView.swift44
-rw-r--r--iGopherBrowser/SettingsView.swift251
-rw-r--r--iGopherBrowser/SidebarView.swift24
-rw-r--r--iGopherBrowser/iGopherBrowserApp.swift40
10 files changed, 911 insertions, 858 deletions
diff --git a/.swift-format b/.swift-format
new file mode 100644
index 0000000..95d360e
--- /dev/null
+++ b/.swift-format
@@ -0,0 +1,5 @@
+{
+ "indentation": {
+ "spaces": 4
+ }
+}
diff --git a/iGopherBrowser/BookmarksView.swift b/iGopherBrowser/BookmarksView.swift
index b23196b..fa95459 100644
--- a/iGopherBrowser/BookmarksView.swift
+++ b/iGopherBrowser/BookmarksView.swift
@@ -11,27 +11,27 @@ import SwiftUI
struct BookmarksView: View {
- enum sectionType: String, CaseIterable, Identifiable {
- case bookmarks, history
- var id: Self { self }
- }
+ enum sectionType: String, CaseIterable, Identifiable {
+ case bookmarks, history
+ var id: Self { self }
+ }
- @State private var selectedSection: sectionType = .bookmarks
+ @State private var selectedSection: sectionType = .bookmarks
- var body: some View {
- VStack {
- Picker("Section", selection: $selectedSection) {
- ForEach(sectionType.allCases) { section in
- Text(section.rawValue.capitalized)
+ var body: some View {
+ VStack {
+ Picker("Section", selection: $selectedSection) {
+ ForEach(sectionType.allCases) { section in
+ Text(section.rawValue.capitalized)
+ }
+ }.pickerStyle(.segmented).padding(.top, 20).padding(.leading, 10).padding(.trailing, 10)
+ .padding(.bottom, 10)
+ Text("You picked \(selectedSection.rawValue.capitalized)")
+ Spacer()
}
- }.pickerStyle(.segmented).padding(.top, 20).padding(.leading, 10).padding(.trailing, 10)
- .padding(.bottom, 10)
- Text("You picked \(selectedSection.rawValue.capitalized)")
- Spacer()
}
- }
}
#Preview {
- BookmarksView()
+ BookmarksView()
}
diff --git a/iGopherBrowser/BrowserView.swift b/iGopherBrowser/BrowserView.swift
index 880bccd..cd983a5 100644
--- a/iGopherBrowser/BrowserView.swift
+++ b/iGopherBrowser/BrowserView.swift
@@ -6,515 +6,558 @@
//
import GopherHelpers
+import SwiftGopherClient
import SwiftUI
import TelemetryClient
-import SwiftGopherClient
func openURL(url: URL) {
- #if os(OSX)
- NSWorkspace.shared.open(url)
- #else
- UIApplication.shared.open(url)
- #endif
+ #if os(OSX)
+ NSWorkspace.shared.open(url)
+ #else
+ UIApplication.shared.open(url)
+ #endif
}
#if canImport(UIKit)
- extension View {
- func hideKeyboard() {
- UIApplication.shared.sendAction(
- #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+ extension View {
+ func hideKeyboard() {
+ UIApplication.shared.sendAction(
+ #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+ }
}
- }
#endif
struct BrowserView: View {
- @AppStorage("homeURL") var homeURL: URL = URL(string: "gopher://gopher.navan.dev:70/")!
- @AppStorage("accentColour", store: .standard) var accentColour: Color = Color(.blue)
- @AppStorage("linkColour", store: .standard) var linkColour: Color = Color(.white)
- @AppStorage("shareThroughProxy", store: .standard) var shareThroughProxy: Bool = true
-
- @State var homeURLString = "gopher://gopher.navan.dev:70/"
-
- @State var url: String = ""
- @State private var gopherItems: [gopherItem] = []
-
- @Binding public var hosts: [GopherNode]
- @Binding var selectedNode: GopherNode?
-
- @State private var backwardStack: [GopherNode] = []
- @State private var forwardStack: [GopherNode] = []
-
- @State private var searchText: String = ""
- @State private var showSearchInput = false
- @State var selectedSearchItem: Int?
-
- @State private var showPreferences = false
- @State private var showBookmarks = false
-
- @Namespace var topID
- @State private var scrollToTop: Bool = false
-
- @State var currentTask: Task<Void, Never>?
-
- let client = GopherClient()
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 0) {
- if gopherItems.count >= 1 {
- ScrollViewReader { proxy in
-
- List {
- ForEach(Array(gopherItems.enumerated()), id: \.offset) { idx, item in
- if item.parsedItemType == .info {
- Text(item.message)
- .font(.system(size: 12, design: .monospaced))
- .frame(height: 20)
- .listRowSeparator(.hidden)
- .padding(.vertical, -8)
- .id(idx)
- } else if item.parsedItemType == .directory {
- Button(action: {
- performGopherRequest(host: item.host, port: item.port, selector: item.selector)
- #if canImport(UIKit)
- hideKeyboard()
- #endif
- }) {
- HStack {
- Text(Image(systemName: "folder"))
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- }.buttonStyle(PlainButtonStyle())
- .id(idx)
- } else if item.parsedItemType == .search {
- Button(action: {
- #if canImport(UIKit)
- hideKeyboard()
- #endif
- self.selectedSearchItem = idx
- self.showSearchInput = true
- }) {
- HStack {
- Text(Image(systemName: "magnifyingglass"))
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- }.buttonStyle(PlainButtonStyle())
- .id(idx)
- } else if item.parsedItemType == .text {
- NavigationLink(destination: FileView(item: item)) {
- HStack {
- Text(Image(systemName: "doc.plaintext"))
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- }
- .id(idx)
- } else if item.selector.hasPrefix("URL:") {
- if let url = URL(string: item.selector.replacingOccurrences(of: "URL:", with: ""))
- {
- //UIApplication.shared.canOpenURL(url) {
- Button(action: {
- openURL(url: url)
- }) {
- HStack {
- Image(systemName: "link")
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- }.buttonStyle(PlainButtonStyle())
- .id(idx)
- }
- } else if [.doc, .image, .gif, .movie, .sound, .bitmap].contains(
- item.parsedItemType)
- {
- NavigationLink(destination: FileView(item: item)) {
- HStack {
- Text(Image(systemName: itemToImageType(item)))
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- .id(idx)
- }
+ @AppStorage("homeURL") var homeURL: URL = URL(string: "gopher://gopher.navan.dev:70/")!
+ @AppStorage("accentColour", store: .standard) var accentColour: Color = Color(.blue)
+ @AppStorage("linkColour", store: .standard) var linkColour: Color = Color(.white)
+ @AppStorage("shareThroughProxy", store: .standard) var shareThroughProxy: Bool = true
+
+ @State var homeURLString = "gopher://gopher.navan.dev:70/"
+
+ @State var url: String = ""
+ @State private var gopherItems: [gopherItem] = []
+
+ @Binding public var hosts: [GopherNode]
+ @Binding var selectedNode: GopherNode?
+
+ @State private var backwardStack: [GopherNode] = []
+ @State private var forwardStack: [GopherNode] = []
+
+ @State private var searchText: String = ""
+ @State private var showSearchInput = false
+ @State var selectedSearchItem: Int?
+
+ @State private var showPreferences = false
+ @State private var showBookmarks = false
+
+ @Namespace var topID
+ @State private var scrollToTop: Bool = false
+
+ @State var currentTask: Task<Void, Never>?
+
+ let client = GopherClient()
+
+ var body: some View {
+ NavigationStack {
+ VStack(spacing: 0) {
+ if gopherItems.count >= 1 {
+ ScrollViewReader { proxy in
+
+ List {
+ ForEach(Array(gopherItems.enumerated()), id: \.offset) { idx, item in
+ if item.parsedItemType == .info {
+ Text(item.message)
+ .font(.system(size: 12, design: .monospaced))
+ .frame(height: 20)
+ .listRowSeparator(.hidden)
+ .padding(.vertical, -8)
+ .id(idx)
+ } else if item.parsedItemType == .directory {
+ Button(action: {
+ performGopherRequest(
+ host: item.host, port: item.port,
+ selector: item.selector)
+ #if canImport(UIKit)
+ hideKeyboard()
+ #endif
+ }) {
+ HStack {
+ Text(Image(systemName: "folder"))
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ }.buttonStyle(PlainButtonStyle())
+ .id(idx)
+ } else if item.parsedItemType == .search {
+ Button(action: {
+ #if canImport(UIKit)
+ hideKeyboard()
+ #endif
+ self.selectedSearchItem = idx
+ self.showSearchInput = true
+ }) {
+ HStack {
+ Text(Image(systemName: "magnifyingglass"))
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ }.buttonStyle(PlainButtonStyle())
+ .id(idx)
+ } else if item.parsedItemType == .text {
+ NavigationLink(destination: FileView(item: item)) {
+ HStack {
+ Text(Image(systemName: "doc.plaintext"))
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ }
+ .id(idx)
+ } else if item.selector.hasPrefix("URL:") {
+ if let url = URL(
+ string: item.selector.replacingOccurrences(
+ of: "URL:", with: ""))
+ {
+ //UIApplication.shared.canOpenURL(url) {
+ Button(action: {
+ openURL(url: url)
+ }) {
+ HStack {
+ Image(systemName: "link")
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ }.buttonStyle(PlainButtonStyle())
+ .id(idx)
+ }
+ } else if [.doc, .image, .gif, .movie, .sound, .bitmap].contains(
+ item.parsedItemType)
+ {
+ NavigationLink(destination: FileView(item: item)) {
+ HStack {
+ Text(Image(systemName: itemToImageType(item)))
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ .id(idx)
+ }
+ } else {
+ Button(action: {
+ TelemetryManager.send(
+ "applicationBrowsedUnknown",
+ with: [
+ "gopherURL":
+ "\(item.host):\(item.port)\(item.selector)"
+ ])
+ performGopherRequest(
+ host: item.host, port: item.port,
+ selector: item.selector)
+ }) {
+ HStack {
+ Text(Image(systemName: "questionmark.app.dashed"))
+ Text(item.message)
+ Spacer()
+ }.foregroundStyle(linkColour)
+ }.buttonStyle(PlainButtonStyle())
+ .id(idx)
+ }
+
+ }
+ }
+ //.background(Color.white)
+ .cornerRadius(10)
+ .onChange(of: scrollToTop) {
+ // TODO: Cleanup
+ withAnimation {
+ proxy.scrollTo(0, anchor: .top)
+
+ }
+ }
+ }
+ .sheet(isPresented: $showSearchInput) {
+ if let index = selectedSearchItem, gopherItems.indices.contains(index) {
+ let searchItem = gopherItems[index]
+ SearchInputView(
+ host: searchItem.host,
+ port: searchItem.port,
+ selector: searchItem.selector,
+ searchText: $searchText,
+ onSearch: { query in
+ performGopherRequest(
+ host: searchItem.host, port: searchItem.port,
+ selector: "\(searchItem.selector)\t\(query)")
+ showSearchInput = false
+ }
+ )
+ } else {
+
+ VStack {
+ Text("Weird bug. Please Dismiss -> Press Go -> Try Again")
+ Button("Dismiss") {
+ self.showSearchInput = false
+ }.onAppear {
+ TelemetryManager.send(
+ "applicationSearchError", with: ["gopherURL": "\(self.url)"]
+ )
+ }
+ }
+
+ }
+ }
} else {
- Button(action: {
- TelemetryManager.send(
- "applicationBrowsedUnknown",
- with: ["gopherURL": "\(item.host):\(item.port)\(item.selector)"])
- performGopherRequest(host: item.host, port: item.port, selector: item.selector)
- }) {
- HStack {
- Text(Image(systemName: "questionmark.app.dashed"))
- Text(item.message)
- Spacer()
- }.foregroundStyle(linkColour)
- }.buttonStyle(PlainButtonStyle())
- .id(idx)
+ Spacer()
+ Text("Welcome to iGopher Browser")
+ Spacer()
}
+ #if os(iOS)
+ VStack {
+ HStack(spacing: 10) {
+ HStack {
+ Spacer()
+
+ TextField("Enter a URL", text: $url)
+ .keyboardType(.URL)
+ .autocapitalization(.none)
+ .padding(10)
+ .background(Color.gray.opacity(0.2))
+ .cornerRadius(10)
+ Spacer()
+ }
+ //.background(Color.white)
+ .cornerRadius(30)
+
+ Button(
+ "Go",
+ action: {
+ TelemetryManager.send(
+ "applicationClickedGo", with: ["gopherURL": "\(self.url)"])
+ performGopherRequest(clearForward: false)
+
+ }
+ )
+ .keyboardShortcut(.defaultAction)
+ .onSubmit {
+ performGopherRequest()
+ }
+ Spacer()
+ }
+ .padding(.bottom, 10)
+ .padding(.top, 5)
+ HStack {
+ Spacer()
+ Button {
+ print(homeURL, "home")
+ TelemetryManager.send(
+ "applicationClickedHome", with: ["gopherURL": "\(self.url)"])
+ performGopherRequest(
+ host: homeURL.host ?? "gopher.navan.dev",
+ port: homeURL.port ?? 70,
+ selector: homeURL.path)
+ } label: {
+ Label("Home", systemImage: "house")
+ .labelStyle(.iconOnly)
+ }
+ Spacer()
+ Button {
+ if let curNode = backwardStack.popLast() {
+ forwardStack.append(curNode)
+ if let prevNode = backwardStack.popLast() {
+ TelemetryManager.send(
+ "applicationClickedBack",
+ with: [
+ "gopherURL":
+ "\(prevNode.host):\(prevNode.port)\(prevNode.selector)"
+ ])
+ performGopherRequest(
+ host: prevNode.host, port: prevNode.port,
+ selector: prevNode.selector,
+ clearForward: false)
+ }
+ }
+ } label: {
+ Label("Back", systemImage: "chevron.left")
+ .labelStyle(.iconOnly)
+ }
+ .disabled(backwardStack.count < 2)
+ Spacer()
+ Button {
+ if let nextNode = forwardStack.popLast() {
+ TelemetryManager.send(
+ "applicationClickedForward",
+ with: [
+ "gopherURL":
+ "\(nextNode.host):\(nextNode.port)\(nextNode.selector)"
+ ])
+ performGopherRequest(
+ host: nextNode.host, port: nextNode.port,
+ selector: nextNode.selector,
+ clearForward: false)
+ }
+ } label: {
+ Label("Forward", systemImage: "chevron.right")
+ .labelStyle(.iconOnly)
+ }
+ .disabled(forwardStack.isEmpty)
+ Spacer()
+ if shareThroughProxy {
+ ShareLink(item: URL(string: "https://gopher.navan.dev/\(url)")!) {
+ Label("Share", systemImage: "square.and.arrow.up").labelStyle(
+ .iconOnly)
+ }
+ } else {
+ ShareLink(item: URL(string: "gopher://\(url)")!) {
+ Label("Share", systemImage: "square.and.arrow.up").labelStyle(
+ .iconOnly)
+ }
+ }
+ Spacer()
+ // Button {
+ // showBookmarks = true
+ // } label: {
+ // Label("Bookmarks", systemImage: "book")
+ // .labelStyle(.iconOnly)
+ // }.sheet(isPresented: $showBookmarks) {
+ // BookmarksView()
+ // .presentationDetents([.height(400), .medium, .large])
+ // .presentationDragIndicator(.automatic)
+ // }
+ // Spacer()
+ Button {
+ self.showPreferences = true
+ } label: {
+ Label("Settings", systemImage: "gear")
+ .labelStyle(.iconOnly)
+ }
+ Spacer()
+ }
+ }
+ #else
+ HStack(spacing: 10) {
+ HStack {
+ Spacer()
+ Button {
+ TelemetryManager.send(
+ "applicationClickedHome", with: ["gopherURL": "\(self.url)"])
+ performGopherRequest(
+ host: homeURL.host ?? "gopher.navan.dev",
+ port: homeURL.port ?? 70,
+ selector: homeURL.path)
+ } label: {
+ Label("Home", systemImage: "house")
+ .labelStyle(.iconOnly)
+ }
- }
+ #if os(visionOS)
+ Button {
+ self.showPreferences = true
+ } label: {
+ Label("Settings", systemImage: "gear")
+ .labelStyle(.iconOnly)
+ }
+ #endif
+
+ Button {
+ if let curNode = backwardStack.popLast() {
+ forwardStack.append(curNode)
+ if let prevNode = backwardStack.popLast() {
+ TelemetryManager.send(
+ "applicationClickedBack",
+ with: [
+ "gopherURL":
+ "\(prevNode.host):\(prevNode.port)\(prevNode.selector)"
+ ])
+ performGopherRequest(
+ host: prevNode.host, port: prevNode.port,
+ selector: prevNode.selector,
+ clearForward: false)
+ }
+ }
+ } label: {
+ Label("Back", systemImage: "chevron.left")
+ .labelStyle(.iconOnly)
+ }
+ .disabled(backwardStack.count < 2)
+
+ Button {
+ if let nextNode = forwardStack.popLast() {
+ TelemetryManager.send(
+ "applicationClickedForward",
+ with: [
+ "gopherURL":
+ "\(nextNode.host):\(nextNode.port)\(nextNode.selector)"
+ ])
+ performGopherRequest(
+ host: nextNode.host, port: nextNode.port,
+ selector: nextNode.selector,
+ clearForward: false)
+ }
+ } label: {
+ Label("Forward", systemImage: "chevron.right")
+ .labelStyle(.iconOnly)
+ }
+ .disabled(forwardStack.isEmpty)
+
+ TextField("Enter a URL", text: $url)
+ #if !os(OSX)
+ .keyboardType(.URL)
+ .autocapitalization(.none)
+ #endif
+ .padding(10)
+ }
+ //.background(Color.white)
+ .cornerRadius(30)
+ if shareThroughProxy {
+ ShareLink(item: URL(string: "https://gopher.navan.dev/\(url)")!) {
+ Label("Share", systemImage: "square.and.arrow.up").labelStyle(
+ .iconOnly)
+ }
+ } else {
+ ShareLink(item: URL(string: "gopher://\(url)")!) {
+ Label("Share", systemImage: "square.and.arrow.up").labelStyle(
+ .iconOnly)
+ }
+ }
+ Button(
+ "Go",
+ action: {
+ TelemetryManager.send(
+ "applicationClickedGo", with: ["gopherURL": "\(self.url)"])
+ performGopherRequest(clearForward: false)
+ }
+ )
+ .keyboardShortcut(.defaultAction)
+ .onSubmit {
+ performGopherRequest()
+ }
+ Spacer()
+ }
+ #endif
}
- //.background(Color.white)
- .cornerRadius(10)
- .onChange(of: scrollToTop) {
- // TODO: Cleanup
- withAnimation {
- proxy.scrollTo(0, anchor: .top)
-
- }
+ }
+ .onChange(of: selectedNode) {
+ if let node = selectedNode {
+ performGopherRequest(host: node.host, port: node.port, selector: node.selector)
}
- }
- .sheet(isPresented: $showSearchInput) {
- if let index = selectedSearchItem, gopherItems.indices.contains(index) {
- let searchItem = gopherItems[index]
- SearchInputView(
- host: searchItem.host,
- port: searchItem.port,
- selector: searchItem.selector,
- searchText: $searchText,
- onSearch: { query in
- performGopherRequest(
- host: searchItem.host, port: searchItem.port,
- selector: "\(searchItem.selector)\t\(query)")
- showSearchInput = false
+ }
+ .onOpenURL { gopherURL in
+ self.url = gopherURL.absoluteString
+ performGopherRequest()
+ }
+ .sheet(
+ isPresented: $showPreferences,
+ onDismiss: {
+ print("badm", homeURL, homeURLString)
+ if let url = URL(string: homeURLString) {
+ self.homeURL = url
}
- )
- } else {
+ }
+ ) {
+ #if os(macOS)
+ SettingsView()
+ #else
+ SettingsView(homeURL: $homeURL, homeURLString: $homeURLString)
+ #endif
+ }
+ .accentColor(accentColour)
+ }
- VStack {
- Text("Weird bug. Please Dismiss -> Press Go -> Try Again")
- Button("Dismiss") {
- self.showSearchInput = false
- }.onAppear {
- TelemetryManager.send(
- "applicationSearchError", with: ["gopherURL": "\(self.url)"])
- }
- }
+ private func performGopherRequest(
+ host: String = "", port: Int = -1, selector: String = "", clearForward: Bool = true
+ ) {
+ // TODO: Remove getHostandPort call here, and call it before calling performGopherRequest
+ print("recieved ", host, port, selector)
+ var res = getHostAndPort(from: self.url)
+ if host != "" {
+ res.host = host
+ if selector != "" {
+ res.selector = selector
+ } else {
+ res.selector = ""
}
- }
- } else {
- Spacer()
- Text("Welcome to iGopher Browser")
- Spacer()
}
- #if os(iOS)
- VStack {
- HStack(spacing: 10) {
- HStack {
- Spacer()
-
- TextField("Enter a URL", text: $url)
- .keyboardType(.URL)
- .autocapitalization(.none)
- .padding(10)
- .background(Color.gray.opacity(0.2))
- .cornerRadius(10)
- Spacer()
- }
- //.background(Color.white)
- .cornerRadius(30)
-
- Button(
- "Go",
- action: {
- TelemetryManager.send("applicationClickedGo", with: ["gopherURL": "\(self.url)"])
- performGopherRequest(clearForward: false)
+ if port != -1 {
+ res.port = port
+ }
+
+ self.url = "\(res.host):\(res.port)\(res.selector)"
+
+ currentTask?.cancel()
+
+ let myHost = res.host
+ let myPort = res.port
+ let mySelector = res.selector
+
+ currentTask = Task {
+ do {
+ try Task.checkCancellation()
+ let resp = try await client.sendRequest(
+ to: myHost, port: myPort, message: "\(mySelector)\r\n")
+
+ var newNode = GopherNode(
+ host: myHost, port: myPort, selector: mySelector, item: nil,
+ children: convertToHostNodes(resp))
+
+ backwardStack.append(newNode)
+ if clearForward {
+ forwardStack.removeAll()
}
- )
- .keyboardShortcut(.defaultAction)
- .onSubmit {
- performGopherRequest()
- }
- Spacer()
- }
- .padding(.bottom, 10)
- .padding(.top, 5)
- HStack {
- Spacer()
- Button {
- print(homeURL, "home")
- TelemetryManager.send("applicationClickedHome", with: ["gopherURL": "\(self.url)"])
- performGopherRequest(
- host: homeURL.host ?? "gopher.navan.dev", port: homeURL.port ?? 70,
- selector: homeURL.path)
- } label: {
- Label("Home", systemImage: "house")
- .labelStyle(.iconOnly)
- }
- Spacer()
- Button {
- if let curNode = backwardStack.popLast() {
- forwardStack.append(curNode)
- if let prevNode = backwardStack.popLast() {
- TelemetryManager.send(
- "applicationClickedBack",
- with: ["gopherURL": "\(prevNode.host):\(prevNode.port)\(prevNode.selector)"])
- performGopherRequest(
- host: prevNode.host, port: prevNode.port, selector: prevNode.selector,
- clearForward: false)
- }
- }
- } label: {
- Label("Back", systemImage: "chevron.left")
- .labelStyle(.iconOnly)
- }
- .disabled(backwardStack.count < 2)
- Spacer()
- Button {
- if let nextNode = forwardStack.popLast() {
- TelemetryManager.send(
- "applicationClickedForward",
- with: ["gopherURL": "\(nextNode.host):\(nextNode.port)\(nextNode.selector)"])
- performGopherRequest(
- host: nextNode.host, port: nextNode.port, selector: nextNode.selector,
- clearForward: false)
- }
- } label: {
- Label("Forward", systemImage: "chevron.right")
- .labelStyle(.iconOnly)
- }
- .disabled(forwardStack.isEmpty)
- Spacer()
- if shareThroughProxy {
- ShareLink(item: URL(string: "https://gopher.navan.dev/\(url)")!) {
- Label("Share", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
- }
- } else {
- ShareLink(item: URL(string: "gopher://\(url)")!) {
- Label("Share", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
- }
- }
- Spacer()
- // Button {
- // showBookmarks = true
- // } label: {
- // Label("Bookmarks", systemImage: "book")
- // .labelStyle(.iconOnly)
- // }.sheet(isPresented: $showBookmarks) {
- // BookmarksView()
- // .presentationDetents([.height(400), .medium, .large])
- // .presentationDragIndicator(.automatic)
- // }
- // Spacer()
- Button {
- self.showPreferences = true
- } label: {
- Label("Settings", systemImage: "gear")
- .labelStyle(.iconOnly)
- }
- Spacer()
- }
- }
- #else
- HStack(spacing: 10) {
- HStack {
- Spacer()
- Button {
- TelemetryManager.send("applicationClickedHome", with: ["gopherURL": "\(self.url)"])
- performGopherRequest(
- host: homeURL.host ?? "gopher.navan.dev", port: homeURL.port ?? 70,
- selector: homeURL.path)
- } label: {
- Label("Home", systemImage: "house")
- .labelStyle(.iconOnly)
- }
-
- #if os(visionOS)
- Button {
- self.showPreferences = true
- } label: {
- Label("Settings", systemImage: "gear")
- .labelStyle(.iconOnly)
+
+ if let index = self.hosts.firstIndex(where: {
+ $0.host == myHost && $0.port == myPort
+ }) {
+ // TODO: Handle case where first link visited is a subdirectory, should the sidebar auto fetch the rest?
+ hosts[index].children = hosts[index].children?.map { child in
+ if child.selector == newNode.selector {
+ newNode.message = child.message
+ return newNode
+ } else {
+ return child
+ }
+ }
+ } else {
+ newNode.selector = "/"
+ hosts.append(newNode)
}
- #endif
-
- Button {
- if let curNode = backwardStack.popLast() {
- forwardStack.append(curNode)
- if let prevNode = backwardStack.popLast() {
- TelemetryManager.send(
- "applicationClickedBack",
- with: ["gopherURL": "\(prevNode.host):\(prevNode.port)\(prevNode.selector)"])
- performGopherRequest(
- host: prevNode.host, port: prevNode.port, selector: prevNode.selector,
- clearForward: false)
- }
+ //TODO: Fix this stupid bodge
+ if self.url != "\(myHost):\(myPort)\(mySelector)" {
+ print("Different URL being processed right now... Cancelling")
+ } else {
+ self.gopherItems = resp
}
- } label: {
- Label("Back", systemImage: "chevron.left")
- .labelStyle(.iconOnly)
- }
- .disabled(backwardStack.count < 2)
-
- Button {
- if let nextNode = forwardStack.popLast() {
- TelemetryManager.send(
- "applicationClickedForward",
- with: ["gopherURL": "\(nextNode.host):\(nextNode.port)\(nextNode.selector)"])
- performGopherRequest(
- host: nextNode.host, port: nextNode.port, selector: nextNode.selector,
- clearForward: false)
+
+ } catch is CancellationError {
+ print("Request was cancelled")
+ } catch {
+ TelemetryManager.send(
+ "applicationRequestError",
+ with: ["gopherURL": "\(self.url)", "errorMessage": "\(error)"])
+ print("Error \(error)")
+ var item = gopherItem(rawLine: "Error \(error)")
+ item.message = "Error \(error)"
+ if self.url != "\(myHost):\(myPort)\(mySelector)" {
+ print("Different URL being processed right now... Cancelling")
+ } else {
+ self.gopherItems = [item]
}
- } label: {
- Label("Forward", systemImage: "chevron.right")
- .labelStyle(.iconOnly)
- }
- .disabled(forwardStack.isEmpty)
-
- TextField("Enter a URL", text: $url)
- #if !os(OSX)
- .keyboardType(.URL)
- .autocapitalization(.none)
- #endif
- .padding(10)
}
- //.background(Color.white)
- .cornerRadius(30)
- if shareThroughProxy {
- ShareLink(item: URL(string: "https://gopher.navan.dev/\(url)")!) {
- Label("Share", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
- }
- } else {
- ShareLink(item: URL(string: "gopher://\(url)")!) {
- Label("Share", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
- }
- }
- Button(
- "Go",
- action: {
- TelemetryManager.send("applicationClickedGo", with: ["gopherURL": "\(self.url)"])
- performGopherRequest(clearForward: false)
- }
- )
- .keyboardShortcut(.defaultAction)
- .onSubmit {
- performGopherRequest()
- }
- Spacer()
- }
- #endif
- }
- }
- .onChange(of: selectedNode) {
- if let node = selectedNode {
- performGopherRequest(host: node.host, port: node.port, selector: node.selector)
- }
- }
- .onOpenURL { gopherURL in
- self.url = gopherURL.absoluteString
- performGopherRequest()
- }
- .sheet(
- isPresented: $showPreferences,
- onDismiss: {
- print("badm", homeURL, homeURLString)
- if let url = URL(string: homeURLString) {
- self.homeURL = url
}
- }
- ) {
- #if os(macOS)
- SettingsView()
- #else
- SettingsView(homeURL: $homeURL, homeURLString: $homeURLString)
- #endif
- }
- .accentColor(accentColour)
- }
-
- private func performGopherRequest(
- host: String = "", port: Int = -1, selector: String = "", clearForward: Bool = true
- ) {
- // TODO: Remove getHostandPort call here, and call it before calling performGopherRequest
- print("recieved ", host, port, selector)
- var res = getHostAndPort(from: self.url)
-
- if host != "" {
- res.host = host
- if selector != "" {
- res.selector = selector
- } else {
- res.selector = ""
- }
}
- if port != -1 {
- res.port = port
- }
-
- self.url = "\(res.host):\(res.port)\(res.selector)"
-
- currentTask?.cancel()
-
- let myHost = res.host
- let myPort = res.port
- let mySelector = res.selector
-
- currentTask = Task {
- do {
- try Task.checkCancellation()
- let resp = try await client.sendRequest(to: myHost, port: myPort, message: "\(mySelector)\r\n")
-
- var newNode = GopherNode(host: myHost, port: myPort, selector: mySelector, item: nil, children: convertToHostNodes(resp))
-
- backwardStack.append(newNode)
- if clearForward {
- forwardStack.removeAll()
- }
-
- if let index = self.hosts.firstIndex(where: { $0.host == myHost && $0.port == myPort }) {
- // TODO: Handle case where first link visited is a subdirectory, should the sidebar auto fetch the rest?
- hosts[index].children = hosts[index].children?.map { child in
- if child.selector == newNode.selector {
- newNode.message = child.message
- return newNode
- } else {
- return child
- }
- }
- } else {
- newNode.selector = "/"
- hosts.append(newNode)
- }
- //TODO: Fix this stupid bodge
- if self.url != "\(myHost):\(myPort)\(mySelector)" {
- print("Different URL being processed right now... Cancelling")
- } else {
- self.gopherItems = resp
- }
-
- } catch is CancellationError {
- print("Request was cancelled")
- } catch {
- TelemetryManager.send(
- "applicationRequestError", with: ["gopherURL": "\(self.url)", "errorMessage": "\(error)"])
- print("Error \(error)")
- var item = gopherItem(rawLine: "Error \(error)")
- item.message = "Error \(error)"
- if self.url != "\(myHost):\(myPort)\(mySelector)" {
- print("Different URL being processed right now... Cancelling")
- } else {
- self.gopherItems = [item]
- }
- }
- }
- }
-
- private func convertToHostNodes(_ responseItems: [gopherItem]) -> [GopherNode] {
- var returnItems: [GopherNode] = []
- responseItems.forEach { item in
- if item.parsedItemType != .info {
- returnItems.append(
- GopherNode(
- host: item.host, port: item.port, selector: item.selector, message: item.message,
- item: item, children: nil))
- //print("found: \(item.message)")
- }
+ private func convertToHostNodes(_ responseItems: [gopherItem]) -> [GopherNode] {
+ var returnItems: [GopherNode] = []
+ responseItems.forEach { item in
+ if item.parsedItemType != .info {
+ returnItems.append(
+ GopherNode(
+ host: item.host, port: item.port, selector: item.selector,
+ message: item.message,
+ item: item, children: nil))
+ //print("found: \(item.message)")
+ }
+ }
+ return returnItems
}
- return returnItems
- }
}
diff --git a/iGopherBrowser/ContentView.swift b/iGopherBrowser/ContentView.swift
index 06ad2a6..f65c786 100644
--- a/iGopherBrowser/ContentView.swift
+++ b/iGopherBrowser/ContentView.swift
@@ -6,50 +6,50 @@
//
import GopherHelpers
-import SwiftUI
import SwiftGopherClient
+import SwiftUI
struct GopherNode: Identifiable, Equatable {
- static func == (lhs: GopherNode, rhs: GopherNode) -> Bool {
- return lhs.host == rhs.host && lhs.port == rhs.port && lhs.selector == rhs.selector
- }
-
- let id = UUID()
- var host: String
- let port: Int
- var selector: String
- var message: String?
- let item: gopherItem?
- var children: [GopherNode]?
+ static func == (lhs: GopherNode, rhs: GopherNode) -> Bool {
+ return lhs.host == rhs.host && lhs.port == rhs.port && lhs.selector == rhs.selector
+ }
+
+ let id = UUID()
+ var host: String
+ let port: Int
+ var selector: String
+ var message: String?
+ let item: gopherItem?
+ var children: [GopherNode]?
}
struct ContentView: View {
- @State public var hosts: [GopherNode] = []
- @State private var selectedNode: GopherNode?
+ @State public var hosts: [GopherNode] = []
+ @State private var selectedNode: GopherNode?
- @State private var columnVisibility = NavigationSplitViewVisibility.automatic
+ @State private var columnVisibility = NavigationSplitViewVisibility.automatic
- var body: some View {
+ var body: some View {
- #if os(iOS)
- BrowserView(hosts: $hosts, selectedNode: $selectedNode)
- #else
+ #if os(iOS)
+ BrowserView(hosts: $hosts, selectedNode: $selectedNode)
+ #else
- NavigationSplitView(columnVisibility: $columnVisibility) {
- SidebarView(
- hosts: hosts,
- onSelect: { node in
- selectedNode = node
+ NavigationSplitView(columnVisibility: $columnVisibility) {
+ SidebarView(
+ hosts: hosts,
+ onSelect: { node in
+ selectedNode = node
- }
- )
- .listStyle(.sidebar)
- } detail: {
- BrowserView(hosts: $hosts, selectedNode: $selectedNode)
- }
- #endif
- }
+ }
+ )
+ .listStyle(.sidebar)
+ } detail: {
+ BrowserView(hosts: $hosts, selectedNode: $selectedNode)
+ }
+ #endif
+ }
}
diff --git a/iGopherBrowser/FileView.swift b/iGopherBrowser/FileView.swift
index 0e05d3d..b1fc0e1 100644
--- a/iGopherBrowser/FileView.swift
+++ b/iGopherBrowser/FileView.swift
@@ -7,152 +7,154 @@
import Foundation
import GopherHelpers
import QuickLook
+import SwiftGopherClient
import SwiftUI
import TelemetryClient
-import SwiftGopherClient
func determineFileType(data: Data) -> String? {
- let signatures: [Data: String] = [
- Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]): "png",
- Data([0xFF, 0xD8, 0xFF]): "jpeg",
- Data("GIF87a".utf8): "gif",
- Data("GIF89a".utf8): "gif",
- Data("BM".utf8): "bmp",
- Data("%PDF-".utf8): "pdf",
- Data([0x50, 0x4B, 0x03, 0x04]): "docx",
- Data([0x50, 0x4B, 0x05, 0x06]): "docx",
- Data([0x50, 0x4B, 0x07, 0x08]): "docx",
- Data([0x49, 0x44, 0x33]): "mp3",
- Data([0x52, 0x49, 0x46, 0x46]): "wav",
- Data([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]): "mp4",
- Data([0x6D, 0x6F, 0x6F, 0x76]): "mov",
- // Add other file signatures as needed
- ]
+ let signatures: [Data: String] = [
+ Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]): "png",
+ Data([0xFF, 0xD8, 0xFF]): "jpeg",
+ Data("GIF87a".utf8): "gif",
+ Data("GIF89a".utf8): "gif",
+ Data("BM".utf8): "bmp",
+ Data("%PDF-".utf8): "pdf",
+ Data([0x50, 0x4B, 0x03, 0x04]): "docx",
+ Data([0x50, 0x4B, 0x05, 0x06]): "docx",
+ Data([0x50, 0x4B, 0x07, 0x08]): "docx",
+ Data([0x49, 0x44, 0x33]): "mp3",
+ Data([0x52, 0x49, 0x46, 0x46]): "wav",
+ Data([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]): "mp4",
+ Data([0x6D, 0x6F, 0x6F, 0x76]): "mov",
+ // Add other file signatures as needed
+ ]
- // Debug file signatures
- // for byte in data.prefix(10) {
- // print(String(format: "%02x", byte), terminator: " ")
- // }
+ // Debug file signatures
+ // for byte in data.prefix(10) {
+ // print(String(format: "%02x", byte), terminator: " ")
+ // }
- // Check for each signature
- for (signature, fileType) in signatures {
- if data.starts(with: signature) {
- return fileType
+ // Check for each signature
+ for (signature, fileType) in signatures {
+ if data.starts(with: signature) {
+ return fileType
+ }
}
- }
- return nil
+ return nil
}
struct FileView: View {
- var item: gopherItem
- let client = GopherClient()
- @State private var fileContent: [String] = []
- @State private var fileURL: URL?
- @State private var QLURL: URL?
+ var item: gopherItem
+ let client = GopherClient()
+ @State private var fileContent: [String] = []
+ @State private var fileURL: URL?
+ @State private var QLURL: URL?
- var body: some View {
- if item.parsedItemType == .text {
- GeometryReader { geometry in
- ScrollView {
- LazyVStack(alignment: .leading, spacing: 0) {
- ForEach(fileContent.indices, id: \.self) { index in
- Text(fileContent[index])
- .font(.system(.body, design: .monospaced))
- .textSelection(.enabled)
- .padding(.vertical, 2)
- }
- }
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding()
- .task {
- readFile(item)
- }
- .listStyle(PlainListStyle())
- }
- } else if [.doc, .image, .gif, .movie, .sound, .bitmap].contains(item.parsedItemType) { // Preview Document: .pdf, .docx, e.t.c
- // Quicklook
- if let url = fileURL {
- Button("Preview Document") {
- print(url)
- QLURL = url
- }.quickLookPreview($QLURL)
- } else {
- Text("Loading Document...")
- .onAppear {
- readFile(item)
- }
- }
+ var body: some View {
+ if item.parsedItemType == .text {
+ GeometryReader { geometry in
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 0) {
+ ForEach(fileContent.indices, id: \.self) { index in
+ Text(fileContent[index])
+ .font(.system(.body, design: .monospaced))
+ .textSelection(.enabled)
+ .padding(.vertical, 2)
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding()
+ .task {
+ readFile(item)
+ }
+ .listStyle(PlainListStyle())
+ }
+ } else if [.doc, .image, .gif, .movie, .sound, .bitmap].contains(item.parsedItemType) { // Preview Document: .pdf, .docx, e.t.c
+ // Quicklook
+ if let url = fileURL {
+ Button("Preview Document") {
+ print(url)
+ QLURL = url
+ }.quickLookPreview($QLURL)
+ } else {
+ Text("Loading Document...")
+ .onAppear {
+ readFile(item)
+ }
+ }
+ }
}
- }
- private func readFile(_ item: gopherItem) {
- self.client.sendRequest(to: item.host, port: item.port, message: "\(item.selector)\r\n") {
- result in
- // Dispatch the result handling back to the main thread
- switch result {
- case .success(let resp):
- if var data = resp.first?.rawData {
- let tempDirURL = FileManager.default.temporaryDirectory
+ private func readFile(_ item: gopherItem) {
+ self.client.sendRequest(to: item.host, port: item.port, message: "\(item.selector)\r\n") {
+ result in
+ // Dispatch the result handling back to the main thread
+ switch result {
+ case .success(let resp):
+ if var data = resp.first?.rawData {
+ let tempDirURL = FileManager.default.temporaryDirectory
- do {
- var fileData = Data()
- print("Readable byees", data.readableBytes)
- while data.readableBytes > 0 {
- if let bytes = data.readBytes(length: data.readableBytes) {
- fileData.append(contentsOf: bytes)
- }
- }
- print("Read entire file")
- if item.parsedItemType == .text {
- print("parsing string file")
- if let string = String(data: fileData, encoding: .utf8) {
- print("updating state")
- let lines = string.components(separatedBy: .newlines)
- let chunkSize = 100
- self.fileContent = stride(from: 0, to: lines.count, by: chunkSize).map {
- lines[$0..<min($0 + chunkSize, lines.count)].joined(separator: "\n")
- }
- } else {
- print("Could not get file")
- }
- return
- }
- let fileURL = tempDirURL.appendingPathComponent(
- UUID().uuidString + ".\(determineFileType(data: fileData) ?? "unkown")")
- print(fileURL)
+ do {
+ var fileData = Data()
+ print("Readable byees", data.readableBytes)
+ while data.readableBytes > 0 {
+ if let bytes = data.readBytes(length: data.readableBytes) {
+ fileData.append(contentsOf: bytes)
+ }
+ }
+ print("Read entire file")
+ if item.parsedItemType == .text {
+ print("parsing string file")
+ if let string = String(data: fileData, encoding: .utf8) {
+ print("updating state")
+ let lines = string.components(separatedBy: .newlines)
+ let chunkSize = 100
+ self.fileContent = stride(from: 0, to: lines.count, by: chunkSize)
+ .map {
+ lines[$0..<min($0 + chunkSize, lines.count)].joined(
+ separator: "\n")
+ }
+ } else {
+ print("Could not get file")
+ }
+ return
+ }
+ let fileURL = tempDirURL.appendingPathComponent(
+ UUID().uuidString + ".\(determineFileType(data: fileData) ?? "unkown")")
+ print(fileURL)
- if determineFileType(data: fileData) == nil {
- TelemetryManager.send(
- "applicationUnableToDetectFiletype",
- with: ["gopherURL": "\(item.host):\(item.port)\(item.selector)"])
+ if determineFileType(data: fileData) == nil {
+ TelemetryManager.send(
+ "applicationUnableToDetectFiletype",
+ with: ["gopherURL": "\(item.host):\(item.port)\(item.selector)"])
+ }
+
+ try fileData.write(to: fileURL)
+ self.fileURL = fileURL
+ } catch {
+ print("Error writing file to temp directory: \(error)")
+ }
+ }
+ case .failure(_):
+ self.fileContent = ["Unable to fetch file due to network error."]
}
+ }
+
+ }
+ private func getTempFileURL(_ data: [UInt8]) -> URL? {
+ let tempDirURL = FileManager.default.temporaryDirectory
+ let fileURL = tempDirURL.appendingPathComponent(UUID().uuidString + ".pdf")
+ print(fileURL)
+ do {
+ let fileData = Data(data)
try fileData.write(to: fileURL)
- self.fileURL = fileURL
- } catch {
+ return fileURL
+ } catch {
print("Error writing file to temp directory: \(error)")
- }
+ return nil
}
- case .failure(_):
- self.fileContent = ["Unable to fetch file due to network error."]
- }
- }
-
- }
-
- private func getTempFileURL(_ data: [UInt8]) -> URL? {
- let tempDirURL = FileManager.default.temporaryDirectory
- let fileURL = tempDirURL.appendingPathComponent(UUID().uuidString + ".pdf")
- print(fileURL)
- do {
- let fileData = Data(data)
- try fileData.write(to: fileURL)
- return fileURL
- } catch {
- print("Error writing file to temp directory: \(error)")
- return nil
}
- }
}
diff --git a/iGopherBrowser/Helpers.swift b/iGopherBrowser/Helpers.swift
index decc97e..521ffab 100644
--- a/iGopherBrowser/Helpers.swift
+++ b/iGopherBrowser/Helpers.swift
@@ -9,36 +9,36 @@ import Foundation
import SwiftGopherClient
public func getHostAndPort(
- from urlString: String, defaultPort: Int = 70, defaultHost: String = "gopher.navan.dev"
+ from urlString: String, defaultPort: Int = 70, defaultHost: String = "gopher.navan.dev"
) -> (host: String, port: Int, selector: String) {
- if let urlComponents = URLComponents(string: urlString),
- let host = urlComponents.host
- {
- let port = urlComponents.port ?? defaultPort
- let selector = urlComponents.path
- print("Mainmain, ", urlComponents, host, port, selector)
- return (host, port, selector)
- } else {
- // Fallback for simpler formats like "localhost:8080"
- let components = urlString.split(separator: ":")
- let host = components.first.map(String.init) ?? defaultHost
+ if let urlComponents = URLComponents(string: urlString),
+ let host = urlComponents.host
+ {
+ let port = urlComponents.port ?? defaultPort
+ let selector = urlComponents.path
+ print("Mainmain, ", urlComponents, host, port, selector)
+ return (host, port, selector)
+ } else {
+ // Fallback for simpler formats like "localhost:8080"
+ let components = urlString.split(separator: ":")
+ let host = components.first.map(String.init) ?? defaultHost
- var port = (components.count > 1 ? Int(components[1]) : nil) ?? defaultPort
- var selector = "/"
+ var port = (components.count > 1 ? Int(components[1]) : nil) ?? defaultPort
+ var selector = "/"
- if components.count > 1 {
- let portCompString = components[1]
- let portCompComponents = portCompString.split(separator: "/", maxSplits: 1)
- if portCompComponents.count > 1 {
- port = Int(portCompComponents[0]) ?? defaultPort
- selector = "/" + portCompComponents[1]
+ if components.count > 1 {
+ let portCompString = components[1]
+ let portCompComponents = portCompString.split(separator: "/", maxSplits: 1)
+ if portCompComponents.count > 1 {
+ port = Int(portCompComponents[0]) ?? defaultPort
+ selector = "/" + portCompComponents[1]
- } else if portCompComponents.count == 1 {
- port = Int(portCompComponents[0]) ?? defaultPort
- }
- }
+ } else if portCompComponents.count == 1 {
+ port = Int(portCompComponents[0]) ?? defaultPort
+ }
+ }
- print("Else Else", components, host, port, selector)
- return (host, port, selector)
- }
+ print("Else Else", components, host, port, selector)
+ return (host, port, selector)
+ }
}
diff --git a/iGopherBrowser/SearchInputView.swift b/iGopherBrowser/SearchInputView.swift
index 54e36c5..f85b94a 100644
--- a/iGopherBrowser/SearchInputView.swift
+++ b/iGopherBrowser/SearchInputView.swift
@@ -9,33 +9,33 @@ import SwiftUI
struct SearchInputView: View {
- var host: String
- var port: Int
- var selector: String
- @Binding var searchText: String
- var onSearch: (String) -> Void
+ var host: String
+ var port: Int
+ var selector: String
+ @Binding var searchText: String
+ var onSearch: (String) -> Void
- @Environment(\.presentationMode) var presentationMode
+ @Environment(\.presentationMode) var presentationMode
- var body: some View {
- VStack {
- Text("Enter your query")
- TextField("Search", text: $searchText)
- .textFieldStyle(RoundedBorderTextFieldStyle())
- .padding()
- HStack {
+ var body: some View {
+ VStack {
+ Text("Enter your query")
+ TextField("Search", text: $searchText)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding()
+ HStack {
- Button("Cancel") {
- presentationMode.wrappedValue.dismiss()
- }
- .padding()
+ Button("Cancel") {
+ presentationMode.wrappedValue.dismiss()
+ }
+ .padding()
- Button("Search") {
- onSearch(searchText)
+ Button("Search") {
+ onSearch(searchText)
+ }
+ .padding()
+ }
}
.padding()
- }
}
- .padding()
- }
}
diff --git a/iGopherBrowser/SettingsView.swift b/iGopherBrowser/SettingsView.swift
index e3e1daf..1f48bad 100644
--- a/iGopherBrowser/SettingsView.swift
+++ b/iGopherBrowser/SettingsView.swift
@@ -9,149 +9,152 @@ import SwiftUI
extension Color: RawRepresentable {
- public init?(rawValue: String) {
+ public init?(rawValue: String) {
- guard let data = Data(base64Encoded: rawValue) else {
- self = .black
- return
- }
+ guard let data = Data(base64Encoded: rawValue) else {
+ self = .black
+ return
+ }
+
+ do {
+ #if os(macOS)
+ let color =
+ try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSColor
+ ?? .black
+ #else
+ let color =
+ try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data)
+ ?? .black
+ #endif
+ self = Color(color)
+ } catch {
+ self = .black
+ }
- do {
- #if os(macOS)
- let color =
- try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSColor ?? .black
- #else
- let color =
- try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) ?? .black
- #endif
- self = Color(color)
- } catch {
- self = .black
}
- }
+ public var rawValue: String {
- public var rawValue: String {
+ do {
+ #if os(macOS)
+ let data =
+ try NSKeyedArchiver.archivedData(
+ withRootObject: NSColor(self), requiringSecureCoding: false) as Data
+ #else
+ let data =
+ try NSKeyedArchiver.archivedData(
+ withRootObject: UIColor(self), requiringSecureCoding: false) as Data
+ #endif
+ return data.base64EncodedString()
- do {
- #if os(macOS)
- let data =
- try NSKeyedArchiver.archivedData(
- withRootObject: NSColor(self), requiringSecureCoding: false) as Data
- #else
- let data =
- try NSKeyedArchiver.archivedData(
- withRootObject: UIColor(self), requiringSecureCoding: false) as Data
- #endif
- return data.base64EncodedString()
+ } catch {
- } catch {
+ return ""
- return ""
+ }
}
- }
-
}
struct SettingsView: View {
- @AppStorage("accentColour", store: .standard) var accentColour: Color = Color(.blue)
- @AppStorage("linkColour", store: .standard) var linkColour: Color = Color(.white)
- @AppStorage("shareThroughProxy", store: .standard) var shareThroughProxy: Bool = true
-
- #if os(macOS)
- @AppStorage("homeURL") var homeURL: URL = URL(string: "gopher://gopher.navan.dev:70/")!
- @State var homeURLString: String = ""
- #else
- @Binding var homeURL: URL
- @Binding var homeURLString: String
- #endif
- @State private var showAlert = false
- @State private var alertMessage: String = ""
- @Environment(\.dismiss) var dismiss
-
- var body: some View {
- Form {
- Section(header: Text("Preferences")) {
- VStack {
- TextField("Home URL", text: $homeURLString)
- .textFieldStyle(RoundedBorderTextFieldStyle())
- .disableAutocorrection(true)
- // .onAppear {
- // // Convert URL to String when the view appears
- // self.homeURLString = homeURL.absoluteString
- // }
- .onSubmit {
- // Convert String back to URL when the user submits the text
- if let url = URL(string: homeURLString) {
- self.homeURL = url
- }
+ @AppStorage("accentColour", store: .standard) var accentColour: Color = Color(.blue)
+ @AppStorage("linkColour", store: .standard) var linkColour: Color = Color(.white)
+ @AppStorage("shareThroughProxy", store: .standard) var shareThroughProxy: Bool = true
+
+ #if os(macOS)
+ @AppStorage("homeURL") var homeURL: URL = URL(string: "gopher://gopher.navan.dev:70/")!
+ @State var homeURLString: String = ""
+ #else
+ @Binding var homeURL: URL
+ @Binding var homeURLString: String
+ #endif
+ @State private var showAlert = false
+ @State private var alertMessage: String = ""
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ Form {
+ Section(header: Text("Preferences")) {
+ VStack {
+ TextField("Home URL", text: $homeURLString)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .disableAutocorrection(true)
+ // .onAppear {
+ // // Convert URL to String when the view appears
+ // self.homeURLString = homeURL.absoluteString
+ // }
+ .onSubmit {
+ // Convert String back to URL when the user submits the text
+ if let url = URL(string: homeURLString) {
+ self.homeURL = url
+ }
+ }
+ HStack {
+ Button(
+ "Save",
+ action: {
+ if let url = URL(string: homeURLString) {
+ homeURL = url
+ print("Saved \(self.homeURL)")
+ #if os(iOS)
+ dismiss()
+ #endif
+ } else {
+ self.alertMessage =
+ "Unable to convert \(homeURLString) to a URL"
+ self.showAlert = true
+ }
+ })
+ Button(
+ "Reset Preferences",
+ action: {
+ self.homeURL = URL(string: "gopher://gopher.navan.dev:70/")!
+ #if os(iOS)
+ dismiss()
+ #endif
+ })
+ }
+ }
+ }
+ Section(header: Text("UI Settings")) {
+ ColorPicker("Link Colour", selection: $linkColour)
+ ColorPicker("Accent Colour", selection: $accentColour)
+ Button("Reset Colours") {
+ self.linkColour = Color(.white)
+ self.accentColour = Color(.blue)
+ }
+ }
+
+ Section {
+ Toggle("Share links through HTTP(s) proxy", isOn: $shareThroughProxy)
+ .toggleStyle(.switch)
+ } header: {
+ Text("Share Settings")
+ } footer: {
+ Text(
+ "Enabling this option shares Gopher URLs through an HTTP proxy, allowing people to view the page without needing a Gopher client"
+ )
+ .font(.caption)
+ .foregroundColor(.gray)
}
- HStack {
- Button(
- "Save",
- action: {
- if let url = URL(string: homeURLString) {
- homeURL = url
- print("Saved \(self.homeURL)")
- #if os(iOS)
+ #if os(visionOS)
+ Button("Done") {
dismiss()
- #endif
- } else {
- self.alertMessage = "Unable to convert \(homeURLString) to a URL"
- self.showAlert = true
}
- })
- Button(
- "Reset Preferences",
- action: {
- self.homeURL = URL(string: "gopher://gopher.navan.dev:70/")!
- #if os(iOS)
- dismiss()
- #endif
- })
- }
- }
- }
- Section(header: Text("UI Settings")) {
- ColorPicker("Link Colour", selection: $linkColour)
- ColorPicker("Accent Colour", selection: $accentColour)
- Button("Reset Colours") {
- self.linkColour = Color(.white)
- self.accentColour = Color(.blue)
+ #endif
}
- }
-
- Section {
- Toggle("Share links through HTTP(s) proxy", isOn: $shareThroughProxy)
- .toggleStyle(.switch)
- } header: {
- Text("Share Settings")
- } footer: {
- Text(
- "Enabling this option shares Gopher URLs through an HTTP proxy, allowing people to view the page without needing a Gopher client"
- )
- .font(.caption)
- .foregroundColor(.gray)
- }
- #if os(visionOS)
- Button("Done") {
- dismiss()
+ #if os(OSX)
+ .padding(20)
+ .frame(width: 350, height: 350)
+ #endif
+ .alert(isPresented: $showAlert) {
+ Alert(
+ title: Text("Error Saving"),
+ message: Text(alertMessage),
+ dismissButton: .default(Text("Got it!"))
+ )
}
- #endif
- }
- #if os(OSX)
- .padding(20)
- .frame(width: 350, height: 350)
- #endif
- .alert(isPresented: $showAlert) {
- Alert(
- title: Text("Error Saving"),
- message: Text(alertMessage),
- dismissButton: .default(Text("Got it!"))
- )
}
- }
}
diff --git a/iGopherBrowser/SidebarView.swift b/iGopherBrowser/SidebarView.swift
index f19670f..7d3f9c3 100644
--- a/iGopherBrowser/SidebarView.swift
+++ b/iGopherBrowser/SidebarView.swift
@@ -9,18 +9,18 @@ import Foundation
import SwiftUI
struct SidebarView: View {
- let hosts: [GopherNode]
- var onSelect: (GopherNode) -> Void
+ let hosts: [GopherNode]
+ var onSelect: (GopherNode) -> Void
- var body: some View {
- VStack {
- List(hosts, children: \.children) { node in
- Text(node.message ?? node.host)
- .onTapGesture {
- onSelect(node)
- }
- }
+ var body: some View {
+ VStack {
+ List(hosts, children: \.children) { node in
+ Text(node.message ?? node.host)
+ .onTapGesture {
+ onSelect(node)
+ }
+ }
+ }
+ .navigationTitle("Your Gophertree")
}
- .navigationTitle("Your Gophertree")
- }
}
diff --git a/iGopherBrowser/iGopherBrowserApp.swift b/iGopherBrowser/iGopherBrowserApp.swift
index e18d6f5..f9a1d7b 100644
--- a/iGopherBrowser/iGopherBrowserApp.swift
+++ b/iGopherBrowser/iGopherBrowserApp.swift
@@ -10,27 +10,27 @@ import TelemetryClient
@main
struct iGopherBrowserApp: App {
- var body: some Scene {
- WindowGroup {
- ContentView()
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ .commands {
+ #if os(macOS)
+ SidebarCommands()
+ #endif
+ }
+ #if os(macOS)
+ Settings {
+ SettingsView()
+ }
+ #endif
}
- .commands {
- #if os(macOS)
- SidebarCommands()
- #endif
- }
- #if os(macOS)
- Settings {
- SettingsView()
- }
- #endif
- }
- init() {
- let configuration = TelemetryManagerConfiguration(
- appID: "400187ED-ADA9-4AB4-91F8-8825AD8FC67C")
- TelemetryManager.initialize(with: configuration)
+ init() {
+ let configuration = TelemetryManagerConfiguration(
+ appID: "400187ED-ADA9-4AB4-91F8-8825AD8FC67C")
+ TelemetryManager.initialize(with: configuration)
- TelemetryManager.send("applicationDidFinishLaunching")
- }
+ TelemetryManager.send("applicationDidFinishLaunching")
+ }
}