diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-07 18:03:44 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-07 18:03:44 -0800 |
| commit | 767eb58da13d83aa321fff12c97fe433cd8fa734 (patch) | |
| tree | a796f07731bd39818e0267a3beae3e6b633729c8 | |
| parent | 154763b8e2b14ff2a92d5a1f1f3e14a18167e6fc (diff) | |
A semi working file browser.
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 73 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 10 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 3 | ||||
| -rw-r--r-- | Hotline/Views/ChatView.swift | 4 | ||||
| -rw-r--r-- | Hotline/Views/FilesView.swift | 254 | ||||
| -rw-r--r-- | Hotline/Views/MessageBoardView.swift | 4 | ||||
| -rw-r--r-- | Hotline/Views/NewsView.swift | 4 | ||||
| -rw-r--r-- | Hotline/Views/UserListView.swift | 5 |
8 files changed, 253 insertions, 104 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index 59a1f9d..36872cb 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -34,7 +34,7 @@ class HotlineClient { var server: HotlineServer? @ObservationIgnored private var connection: NWConnection? - @ObservationIgnored private var transactionLog: [UInt32:(HotlineTransactionType, (() -> Void)?)] = [:] + @ObservationIgnored private var transactionLog: [UInt32:(HotlineTransactionType, ((HotlineTransaction) -> Void)?)] = [:] init() { // let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) @@ -106,7 +106,7 @@ class HotlineClient { // MARK: - - private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil, reply: (() -> Void)? = nil) { + private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil, reply: ((HotlineTransaction) -> Void)? = nil) { guard let c = connection else { return } @@ -353,16 +353,61 @@ class HotlineClient { self.sendTransaction(t, callback: callback) } - func sendGetFileList(path: [String] = [], callback: (() -> Void)? = nil, reply: (() -> Void)?) { + func sendGetFileList(path: [String] = [], callback: (() -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { var t = HotlineTransaction(type: .getFileNameList) + var parentFile: HotlineFile? = nil if !path.isEmpty { t.setFieldPath(type: .filePath, val: path) + parentFile = self.findFile(in: self.fileList, at: path) } + + + // if let p = path { // t.setFieldString(type: .filePath) // } - self.sendTransaction(t, callback: callback, reply: reply) + self.sendTransaction(t, callback: callback, reply: { r in + var files: [HotlineFile] = [] + for fi in r.getFieldList(type: .fileNameWithInfo) { + var file = fi.getFile() + file.path = path + [file.name] + files.append(file) + } + + DispatchQueue.main.async { + if var pf = parentFile { + print("\(pf.name) <= \(files.count) files") + pf.files = files + } + else { + self.fileList = files + } + reply?(files) + } + }) + } + + func findFile(in filesToSearch: [HotlineFile], at path: [String]) -> HotlineFile? { + guard !path.isEmpty, !filesToSearch.isEmpty else { return nil } + +// var stack: [([HotlineFile], [String])] = [(self.files!, path)] + + let currentName = path[0] + + for file in filesToSearch { + if file.name == currentName { + if path.count == 1 { + return file + } + else if let subfiles = file.files { + let remainingPath = Array(path[1...]) + return self.findFile(in: subfiles, at: remainingPath) + } + } + } + + return nil } // func sendGetNews(callback: (() -> Void)? = nil) { @@ -390,7 +435,7 @@ class HotlineClient { defer { let replyCallback = repliedTransactionType.1 DispatchQueue.main.async { - replyCallback?() + replyCallback?(transaction) } } @@ -446,15 +491,15 @@ class HotlineClient { self.messageBoardMessages = messages } } - case .getFileNameList: - var files: [HotlineFile] = [] - for fi in transaction.getFieldList(type: .fileNameWithInfo) { - let file = fi.getFile() - files.append(file) - } - DispatchQueue.main.async { - self.fileList = files - } +// case .getFileNameList: +// var files: [HotlineFile] = [] +// for fi in transaction.getFieldList(type: .fileNameWithInfo) { +// let file = fi.getFile() +// files.append(file) +// } +// DispatchQueue.main.async { +// self.fileList = files +// } case .getNewsCategoryNameList: var categories: [HotlineNewsCategory] = [] for fi in transaction.getFieldList(type: .newsCategoryListData15) { diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index 92d7449..e3fcd12 100644 --- a/Hotline/Hotline/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -94,13 +94,15 @@ struct HotlineNewsCategory: Identifiable, Hashable { } -struct HotlineFile: Identifiable, Hashable { +@Observable +class HotlineFile: Identifiable, Hashable { let id = UUID() let type: String let creator: String let fileSize: UInt32 let name: String + var path: [String] = [] var isExpanded: Bool = false var files: [HotlineFile]? = nil @@ -121,6 +123,9 @@ struct HotlineFile: Identifiable, Hashable { self.name = fileName self.isFolder = (self.type == "fldr") + if self.isFolder { + self.files = [] + } } init(from data: Data) { @@ -132,6 +137,9 @@ struct HotlineFile: Identifiable, Hashable { self.fileSize = data.readUInt32(at: 8)! self.isFolder = (self.type == "fldr") + if self.isFolder { + self.files = [] + } // data.readUInt32(at: 12)! // reserved // let nameScript = data.readUInt16(at: 16)! // name script diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index 4a24520..c437ee6 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -203,7 +203,6 @@ class HotlineTrackerClient { self.bytes.append(contentsOf: data) if bytes.count >= maxDataLength { - print("HotlineTracker: done with data, should close. \(self.bytes.count) \(self.maxDataLength)") self.disconnect() self.parseListing() return @@ -260,8 +259,6 @@ class HotlineTrackerClient { cursor += 10 + nameByteCount + descByteCount } - - print(cursor) } DispatchQueue.main.async { diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift index 2283cdd..28e548d 100644 --- a/Hotline/Views/ChatView.swift +++ b/Hotline/Views/ChatView.swift @@ -106,11 +106,11 @@ struct ChatView: View { Button { hotline.disconnect() } label: { - Image(systemName: "xmark.circle.fill") + Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) + .font(.title2) .foregroundColor(.secondary) } - } } diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift index 52963e0..091d718 100644 --- a/Hotline/Views/FilesView.swift +++ b/Hotline/Views/FilesView.swift @@ -1,21 +1,23 @@ import SwiftUI import UniformTypeIdentifiers -struct FileListView: View { +struct FileView: View { @Environment(HotlineClient.self) private var hotline - @State private var fetched = false + @State var file: HotlineFile + @State var loaded = false @State var fileList: [HotlineFile] = [] - var path: [String] = [] + let depth: Int + let path: [String] static let byteFormatter = ByteCountFormatter() private func formattedFileSize(_ fileSize: UInt32) -> String { // let bcf = ByteCountFormatter() - FileListView.byteFormatter.allowedUnits = [.useAll] - FileListView.byteFormatter.countStyle = .file - return FileListView.byteFormatter.string(fromByteCount: Int64(fileSize)) + FileView.byteFormatter.allowedUnits = [.useAll] + FileView.byteFormatter.countStyle = .file + return FileView.byteFormatter.string(fromByteCount: Int64(fileSize)) } private func fileIcon(name: String) -> UIImage { @@ -45,103 +47,199 @@ struct FileListView: View { } var body: some View { - List { - ForEach(fileList, id: \.self) { file in - if file.isFolder { - DisclosureGroup { - if !fetched { - ProgressView() - } - else { - FileListView(path: []) - } - } label: { - HStack { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Image(systemName: "folder.fill") + if file.isFolder { + DisclosureGroup { + if !loaded { + Text("Loading...") + .task { + if !loaded { + hotline.sendGetFileList(path: path) { + print("FETCHED!") + } reply: { newFiles in + print("GOT FILES REPLY?", newFiles) + + fileList = newFiles + loaded = true + } } - .frame(minWidth: 25) - Text(file.name).fontWeight(.medium) - Spacer() - Text("\(file.fileSize)").foregroundStyle(.secondary) } - } } else { + FileListView(fileList: fileList, depth: depth + 1, path: path + [file.name]) + } + } label: { + VStack { HStack { HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Image(uiImage: fileIcon(name: file.name)) - .renderingMode(.template) - .foregroundColor(.accentColor) + Image(systemName: "folder.fill") } .frame(minWidth: 25) - Text(file.name) + Text(file.name).fontWeight(.medium) Spacer() - Text(formattedFileSize(file.fileSize)).foregroundStyle(.gray) + Text("\(file.fileSize)").foregroundStyle(.secondary) } } + .frame(height: 44) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + // Divider() + // .padding(EdgeInsets(top: 0, leading: 16 + 25 + 8, bottom: 0, trailing: 0)) } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: depth == 0 ? 16 : 0)) + .background(Color(uiColor: UIColor.systemBackground)) } - .listStyle(.plain) - .task { - if !fetched { - hotline.sendGetFileList() { - print("FETCHED!") - fetched = true - } reply: { - print("GOT FILES REPLY?") - fileList = hotline.fileList + else { + VStack { + HStack { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Image(uiImage: fileIcon(name: file.name)) + .renderingMode(.template) + .foregroundColor(.accentColor) + } + .frame(minWidth: 25) + Text(file.name) + Spacer() + Text(formattedFileSize(file.fileSize)).foregroundStyle(.gray) } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)) } + .frame(height: 44) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: depth == 0 ? 16 : 0)) + .background(Color(uiColor: UIColor.systemBackground)) + + Divider() + .padding(EdgeInsets(top: 0, leading: 16 + 25 + 8, bottom: 0, trailing: 0)) } } } -struct FilesView: View { +struct FileListView: View { @Environment(HotlineClient.self) private var hotline @State private var fetched = false + @State private var loaded = false + @State var fileList: [HotlineFile] = [] + @State var expanded = false + + var depth = 0 + + var path: [String] = [] + + var body: some View { + LazyVStack(alignment: .leading, spacing: 0) { + Section { + ForEach(fileList, id: \.self) { file in + FileView(file: file, depth: depth, path: path + [file.name]) + } + } + Spacer() + } + .listStyle(.plain) + .animation(.default, value: fileList) + } +} + +struct BestFileView: View { + @Environment(HotlineClient.self) private var hotline + + @State var expanded = false + + var file: HotlineFile + + var body: some View { + if file.isFolder { + DisclosureGroup(isExpanded: $expanded) { + ForEach(file.files!) { childFile in + BestFileView(file: childFile) + .frame(height: 44) + } + } label: { + HStack { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Image(systemName: "folder.fill") + } + .frame(minWidth: 25) + Text(file.name).fontWeight(.medium) + Spacer() + Text("\(file.fileSize)").foregroundStyle(.secondary) + } + } + .onChange(of: expanded) { + print("EXPANDED CHANGED") + + hotline.sendGetFileList(path: file.path) + } + } + else { + HStack { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Image(uiImage: fileIcon(name: file.name)) + } + .frame(minWidth: 25) + Text(file.name).fontWeight(.medium) + Spacer() + Text(formattedFileSize(file.fileSize)).foregroundStyle(.secondary) + } + } + } + + static let byteFormatter = ByteCountFormatter() -// let fileList: [HotlineFile] + private func formattedFileSize(_ fileSize: UInt32) -> String { + // let bcf = ByteCountFormatter() + BestFileView.byteFormatter.allowedUnits = [.useAll] + BestFileView.byteFormatter.countStyle = .file + return BestFileView.byteFormatter.string(fromByteCount: Int64(fileSize)) + } -// static let byteFormatter = ByteCountFormatter() -// -// private func formattedFileSize(_ fileSize: UInt32) -> String { -// // let bcf = ByteCountFormatter() -// FilesView.byteFormatter.allowedUnits = [.useAll] -// FilesView.byteFormatter.countStyle = .file -// return FilesView.byteFormatter.string(fromByteCount: Int64(fileSize)) -// } -// -// private func fileIcon(name: String) -> UIImage { -// // func utTypeForFilename(_ filename: String) -> UTType? { -// let fileExtension = (name as NSString).pathExtension -// if let fileType = UTType(filenameExtension: fileExtension) { -// print("\(name) \(fileExtension) = \(fileType)") -// -// if fileType.isSubtype(of: .movie) { -// return UIImage(systemName: "play.rectangle")! -// } -// else if fileType.isSubtype(of: .image) { -// return UIImage(systemName: "photo")! -// } -// else if fileType.isSubtype(of: .archive) { -// return UIImage(systemName: "doc.zipper")! -// } -// else if fileType.isSubtype(of: .text) { -// return UIImage(systemName: "doc.text")! -// } -// else { -// return UIImage(systemName: "doc")! -// } -// } -// -// return UIImage(systemName: "doc")! -// } + private func fileIcon(name: String) -> UIImage { + // func utTypeForFilename(_ filename: String) -> UTType? { + let fileExtension = (name as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + print("\(name) \(fileExtension) = \(fileType)") + + if fileType.isSubtype(of: .movie) { + return UIImage(systemName: "play.rectangle")! + } + else if fileType.isSubtype(of: .image) { + return UIImage(systemName: "photo")! + } + else if fileType.isSubtype(of: .archive) { + return UIImage(systemName: "doc.zipper")! + } + else if fileType.isSubtype(of: .text) { + return UIImage(systemName: "doc.text")! + } + else { + return UIImage(systemName: "doc")! + } + } + + return UIImage(systemName: "doc")! + } +} + +struct FilesView: View { + @Environment(HotlineClient.self) private var hotline + + @State var initialLoad = false var body: some View { NavigationStack { - FileListView(path: []) + List(hotline.fileList) { file in +// OutlineGroup(hotline.fileList, children: \.files, expanded: $expandedFolders) { file in + BestFileView(file: file) + .frame(height: 44) +// } + } + .task { + if !initialLoad { + hotline.sendGetFileList(path: []) { + initialLoad = true + } + } + } + .listStyle(.plain) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { @@ -152,11 +250,11 @@ struct FilesView: View { Button { hotline.disconnect() } label: { - Image(systemName: "xmark.circle.fill") + Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) + .font(.title2) .foregroundColor(.secondary) } - } } } diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift index 35b4282..04f51df 100644 --- a/Hotline/Views/MessageBoardView.swift +++ b/Hotline/Views/MessageBoardView.swift @@ -41,11 +41,11 @@ struct MessageBoardView: View { Button { hotline.disconnect() } label: { - Image(systemName: "xmark.circle.fill") + Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) + .font(.title2) .foregroundColor(.secondary) } - } ToolbarItem(placement: .navigationBarTrailing) { Button { diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift index 78282d6..539ead8 100644 --- a/Hotline/Views/NewsView.swift +++ b/Hotline/Views/NewsView.swift @@ -105,11 +105,11 @@ struct NewsView: View { Button { hotline.disconnect() } label: { - Image(systemName: "xmark.circle.fill") + Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) + .font(.title2) .foregroundColor(.secondary) } - } ToolbarItem(placement: .navigationBarTrailing) { Button { diff --git a/Hotline/Views/UserListView.swift b/Hotline/Views/UserListView.swift index e0fa7fb..8ea186e 100644 --- a/Hotline/Views/UserListView.swift +++ b/Hotline/Views/UserListView.swift @@ -6,7 +6,7 @@ struct UserListView: View { var body: some View { NavigationStack { List(hotline.userList) { u in - Text(u.name) + Text("🤖 \(u.name)") .fontWeight(.medium) .lineLimit(1) .truncationMode(.tail) @@ -24,8 +24,9 @@ struct UserListView: View { Button { hotline.disconnect() } label: { - Image(systemName: "xmark.circle.fill") + Text(Image(systemName: "xmark.circle.fill")) .symbolRenderingMode(.hierarchical) + .font(.title2) .foregroundColor(.secondary) } } |