From 373646f6611e92525c605d3fb3d1d462b0a4cfd5 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sat, 16 Dec 2023 16:51:14 -0700 Subject: init v0.9 --- iGopherBrowser.xcodeproj/project.pbxproj | 8 + .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + iGopherBrowser/ContentView.swift | 169 ++++++++++++++++++--- iGopherBrowser/FileView.swift | 57 +++++++ iGopherBrowser/SearchInputView.swift | 31 ++++ iGopherBrowser/iGopherBrowserApp.swift | 5 + 6 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 iGopherBrowser.xcodeproj/xcuserdata/navanchauhan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 iGopherBrowser/FileView.swift create mode 100644 iGopherBrowser/SearchInputView.swift 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 = ""; }; 3E1BCC612B297E9C00A4CB69 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3E1BCC852B299E9F00A4CB69 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + 3EFB9C0C2B2E4F06005EAD7C /* SearchInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputView.swift; sourceTree = ""; }; + 3EFB9C0E2B2E6325005EAD7C /* FileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileView.swift; sourceTree = ""; }; /* 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 = ""; @@ -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 @@ + + + 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 + } } } -- cgit v1.2.3