aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-07 18:03:44 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-07 18:03:44 -0800
commit767eb58da13d83aa321fff12c97fe433cd8fa734 (patch)
treea796f07731bd39818e0267a3beae3e6b633729c8
parent154763b8e2b14ff2a92d5a1f1f3e14a18167e6fc (diff)
A semi working file browser.
-rw-r--r--Hotline/Hotline/HotlineClient.swift73
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift10
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift3
-rw-r--r--Hotline/Views/ChatView.swift4
-rw-r--r--Hotline/Views/FilesView.swift254
-rw-r--r--Hotline/Views/MessageBoardView.swift4
-rw-r--r--Hotline/Views/NewsView.swift4
-rw-r--r--Hotline/Views/UserListView.swift5
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)
}
}