aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2023-12-16 16:51:14 -0700
committerNavan Chauhan <navanchauhan@gmail.com>2023-12-16 16:51:14 -0700
commit373646f6611e92525c605d3fb3d1d462b0a4cfd5 (patch)
tree6716346eebd0bb2f23ea5de6e98a3a23248486e6
parentfd7fb8bb4c2e26ed9a5fbed5b725b8b44869e0b3 (diff)
init v0.9
-rw-r--r--iGopherBrowser.xcodeproj/project.pbxproj8
-rw-r--r--iGopherBrowser.xcodeproj/xcuserdata/navanchauhan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist6
-rw-r--r--iGopherBrowser/ContentView.swift169
-rw-r--r--iGopherBrowser/FileView.swift57
-rw-r--r--iGopherBrowser/SearchInputView.swift31
-rw-r--r--iGopherBrowser/iGopherBrowserApp.swift5
6 files changed, 256 insertions, 20 deletions
diff --git a/iGopherBrowser.xcodeproj/project.pbxproj b/iGopherBrowser.xcodeproj/project.pbxproj
index 346e488..4aad221 100644
--- a/iGopherBrowser.xcodeproj/project.pbxproj
+++ b/iGopherBrowser.xcodeproj/project.pbxproj
@@ -14,6 +14,8 @@
3E1BCC602B297E9C00A4CB69 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E1BCC5F2B297E9C00A4CB69 /* Preview Assets.xcassets */; };
3E1BCC842B298A9B00A4CB69 /* SwiftGopherClient in Frameworks */ = {isa = PBXBuildFile; productRef = 3E1BCC832B298A9B00A4CB69 /* SwiftGopherClient */; };
3E1BCC862B299E9F00A4CB69 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1BCC852B299E9F00A4CB69 /* SidebarView.swift */; };
+ 3EFB9C0D2B2E4F06005EAD7C /* SearchInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFB9C0C2B2E4F06005EAD7C /* SearchInputView.swift */; };
+ 3EFB9C0F2B2E6325005EAD7C /* FileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFB9C0E2B2E6325005EAD7C /* FileView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -26,6 +28,8 @@
3E1BCC5F2B297E9C00A4CB69 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
3E1BCC612B297E9C00A4CB69 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3E1BCC852B299E9F00A4CB69 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
+ 3EFB9C0C2B2E4F06005EAD7C /* SearchInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputView.swift; sourceTree = "<group>"; };
+ 3EFB9C0E2B2E6325005EAD7C /* FileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -68,6 +72,8 @@
3E1BCC612B297E9C00A4CB69 /* Info.plist */,
3E1BCC5E2B297E9C00A4CB69 /* Preview Content */,
3E1BCC852B299E9F00A4CB69 /* SidebarView.swift */,
+ 3EFB9C0C2B2E4F06005EAD7C /* SearchInputView.swift */,
+ 3EFB9C0E2B2E6325005EAD7C /* FileView.swift */,
);
path = iGopherBrowser;
sourceTree = "<group>";
@@ -163,10 +169,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3EFB9C0F2B2E6325005EAD7C /* FileView.swift in Sources */,
3E1BCC582B297E9B00A4CB69 /* ContentView.swift in Sources */,
3E1BCC862B299E9F00A4CB69 /* SidebarView.swift in Sources */,
3E1BCC5A2B297E9B00A4CB69 /* Item.swift in Sources */,
3E1BCC562B297E9B00A4CB69 /* iGopherBrowserApp.swift in Sources */,
+ 3EFB9C0D2B2E4F06005EAD7C /* SearchInputView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/iGopherBrowser.xcodeproj/xcuserdata/navanchauhan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/iGopherBrowser.xcodeproj/xcuserdata/navanchauhan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 0000000..03464b0
--- /dev/null
+++ b/iGopherBrowser.xcodeproj/xcuserdata/navanchauhan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Bucket
+ uuid = "C912F8DF-4702-48BD-8A6A-49ECE3BCCC24"
+ type = "1"
+ version = "2.0">
+</Bucket>
diff --git a/iGopherBrowser/ContentView.swift b/iGopherBrowser/ContentView.swift
index df4cbc6..cbf1247 100644
--- a/iGopherBrowser/ContentView.swift
+++ b/iGopherBrowser/ContentView.swift
@@ -28,16 +28,26 @@ struct ContentView: View {
@State private var gopherItems: [gopherItem] = []
@State public var hosts: [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?
+
let client = GopherClient()
var body: some View {
- NavigationView {
-
+ NavigationSplitView {
+#if os(iOS)
+#else
SidebarView(hosts: hosts, onSelect: { node in
performGopherRequest(host: node.host, port: node.port, selector: node.selector)
})
- .listStyle(SidebarListStyle())
+ .listStyle(.sidebar)
+#endif
+ } detail: {
ZStack(alignment: .bottom) {
@@ -52,7 +62,33 @@ struct ContentView: View {
.frame(height: 20)
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
- } else {
+ } else if item.parsedItemType == .directory {
+ HStack {
+ Text(Image(systemName: "folder"))
+ Text(item.message)
+ Spacer()
+ }.onTapGesture {
+ performGopherRequest(host: item.host, port: item.port, selector: item.selector)
+ }
+ } else if item.parsedItemType == .search {
+ HStack {
+ Text(Image(systemName: "magnifyingglass"))
+ Text(item.message)
+ Spacer()
+ }.onTapGesture {
+ self.selectedSearchItem = idx
+ self.showSearchInput = true
+ }
+ } else if item.parsedItemType == .text {
+ NavigationLink(destination: FileView(item: item)) {
+ HStack {
+ Text(Image(systemName: "doc.text"))
+ Text(item.message)
+ Spacer()
+ }
+ }
+ }
+ else {
Text(item.message)
.onTapGesture {
performGopherRequest(host: item.host, port: item.port, selector: item.selector)
@@ -63,8 +99,59 @@ struct ContentView: View {
}
.background(Color.white)
.cornerRadius(10)
+ .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 {
+
+ Text("Search is Broken.")
+ }
+ }
HStack(spacing: 10) {
HStack {
+ Spacer()
+ Button {
+ performGopherRequest(host:"gopher.navan.dev",port: 70,selector: "/")
+ } label: {
+ Label("Home", systemImage: "house")
+ .labelStyle(.iconOnly)
+ }
+
+ Button {
+ if let curNode = backwardStack.popLast() {
+ forwardStack.append(curNode)
+ if let prevNode = backwardStack.popLast() {
+ 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() {
+ //backwardStack.append(nextNode)
+ 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(iOS)
.keyboardType(.URL)
@@ -77,58 +164,98 @@ struct ContentView: View {
.cornerRadius(30)
Button("Go", action: {
- performGopherRequest()
+ performGopherRequest(clearForward: false)
})
.keyboardShortcut(.defaultAction)
.onSubmit {
performGopherRequest()
}
- .padding(10)
+ Spacer()
}
}
}
+ }.toolbar {
+ ToolbarItem(placement: .navigation) {
+ Button(action: toggleSidebar, label: {
+ Image(systemName: "sidebar.leading")
+ })
+ }
}
}
- public func getHostAndPort(from urlString: String, defaultPort: Int = 70, defaultHost: String = "gopher.navan.dev") -> (host: String, port: Int) {
+ private func toggleSidebar() {
+ #if os(iOS)
+ #else
+ NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
+ #endif
+ }
+
+ public func getHostAndPort(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
- return (host, port)
+ 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
- let port = (components.count > 1 ? Int(components[1]) : nil) ?? defaultPort
- return (host, port)
+
+ 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]
+
+ }
+ }
+
+
+ print("Else Else",components, host, port, selector)
+ return (host, port, selector)
}
}
- private func performGopherRequest(host: String = "", port: Int = -1, selector: String = "") {
-
+ 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)\(selector)"
- client.sendRequest(to: res.host, port: res.port, message: "\(selector)\r\n") { result in
+
+ self.url = "\(res.host):\(res.port)\(res.selector)"
+
+ client.sendRequest(to: res.host, port: res.port, message: "\(res.selector)\r\n") { result in
DispatchQueue.main.async {
switch result {
case .success(let resp):
- print(resp)
+ //print(resp)
var newNode = GopherNode(host: res.host, port: res.port, selector: selector, item: nil, children: convertToHostNodes(resp))
+ backwardStack.append(newNode)
+ if clearForward {
+ forwardStack.removeAll()
+ }
print(newNode.selector)
if let index = self.hosts.firstIndex(where: { $0.host == res.host && $0.port == res.port }) {
- if newNode.selector == "" || newNode.selector == "/" {
- print("do something")
- } else {
+ // TODO: Handle case where first link visited is a subdirectory, should the sidebar auto fetch the rest?
print("parent already exists")
//hosts[index] = newNode
hosts[index].children = hosts[index].children?.map { child in
@@ -139,13 +266,15 @@ struct ContentView: View {
return child
}
}
- }
+
} else {
+ newNode.selector = "/"
hosts.append(newNode)
print("created new")
}
self.gopherItems = resp
+
case .failure(let error):
print("Error \(error)")
var item = gopherItem(rawLine: "Error \(error)")
@@ -164,7 +293,7 @@ private func convertToHostNodes(_ responseItems: [gopherItem]) -> [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)")
+ //print("found: \(item.message)")
}
}
return returnItems
diff --git a/iGopherBrowser/FileView.swift b/iGopherBrowser/FileView.swift
new file mode 100644
index 0000000..52e2081
--- /dev/null
+++ b/iGopherBrowser/FileView.swift
@@ -0,0 +1,57 @@
+//
+// FileView.swift
+// iGopherBrowser
+//
+// Created by Navan Chauhan on 12/16/23.
+//
+import Foundation
+import SwiftUI
+
+import swiftGopherClient
+
+struct FileView: View {
+ var item: gopherItem
+ let client = GopherClient()
+ @State private var fileContent: String = "Loading..."
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ if item.parsedItemType == .text {
+ ScrollView {
+ Text(fileContent)
+ .onAppear {
+ readFile(item)
+ }
+ } .toolbar {
+ ToolbarItem() {
+ Button(action: {
+ dismiss()
+ }) {
+ Label("Back", systemImage: "arrow.left.circle")
+ }
+ }
+ }
+ }
+ }
+
+ private func readFile(_ item: gopherItem) {
+ // Execute the network request on a background thread
+ DispatchQueue.global(qos: .userInitiated).async {
+ 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
+ DispatchQueue.main.async {
+ switch result {
+ case .success(let resp):
+ if let firstLine = resp.first?.rawLine {
+ self.fileContent = firstLine
+ } else {
+ self.fileContent = "File is empty or couldn't be read."
+ }
+ case .failure(_):
+ self.fileContent = "Unable to fetch file due to network error."
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/iGopherBrowser/SearchInputView.swift b/iGopherBrowser/SearchInputView.swift
new file mode 100644
index 0000000..cae88f3
--- /dev/null
+++ b/iGopherBrowser/SearchInputView.swift
@@ -0,0 +1,31 @@
+//
+// SearchInputView.swift
+// iGopherBrowser
+//
+// Created by Navan Chauhan on 12/16/23.
+//
+
+import SwiftUI
+
+struct SearchInputView: View {
+
+ var host: String
+ var port: Int
+ var selector: String
+ @Binding var searchText: String
+ var onSearch: (String) -> Void
+
+ var body: some View {
+ VStack {
+ Text("Enter your query")
+ TextField("Search", text: $searchText)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding()
+ Button("Search") {
+ onSearch(searchText)
+ }
+ .padding()
+ }
+ .padding()
+ }
+}
diff --git a/iGopherBrowser/iGopherBrowserApp.swift b/iGopherBrowser/iGopherBrowserApp.swift
index 8ae83df..856ff5e 100644
--- a/iGopherBrowser/iGopherBrowserApp.swift
+++ b/iGopherBrowser/iGopherBrowserApp.swift
@@ -28,5 +28,10 @@ struct iGopherBrowserApp: App {
ContentView()
}
.modelContainer(sharedModelContainer)
+ .commands {
+ #if os(macOS)
+ SidebarCommands()
+ #endif
+ }
}
}