aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-10 21:00:43 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-10 21:00:43 -0800
commitddb9c69b24a67ac140af9ff20f5c36bdef6fb51b (patch)
tree0bb994f1ac7a608c90b33c402629b0c8b21caff4 /Hotline
parenta4263aea6e2875fa77783685985e5c8f7991337f (diff)
Remove "New" suffix from our transfer clients. Further cleanup. Allow previewing certain files with HFS types (but no extensions). Cleanup preview files when window closes.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineClientNew.swift108
-rw-r--r--Hotline/Hotline/Transfers/HotlineFileDownloadClient.swift (renamed from Hotline/Hotline/Transfers/HotlineFileDownloadClientNew.swift)0
-rw-r--r--Hotline/Hotline/Transfers/HotlineFilePreviewClient.swift (renamed from Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift)30
-rw-r--r--Hotline/Hotline/Transfers/HotlineFileUploadClient.swift (renamed from Hotline/Hotline/Transfers/HotlineFileUploadClientNew.swift)0
-rw-r--r--Hotline/Hotline/Transfers/HotlineFolderDownloadClient.swift (renamed from Hotline/Hotline/Transfers/HotlineFolderDownloadClientNew.swift)43
-rw-r--r--Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift (renamed from Hotline/Hotline/Transfers/HotlineFolderUploadClientNew.swift)10
-rw-r--r--Hotline/Library/Extensions.swift90
-rw-r--r--Hotline/Models/FileInfo.swift10
-rw-r--r--Hotline/Models/PreviewFileInfo.swift3
-rw-r--r--Hotline/State/AppUpdate.swift9
-rw-r--r--Hotline/State/FilePreviewState.swift153
-rw-r--r--Hotline/State/HotlineState.swift22
-rw-r--r--Hotline/State/ServerState.swift2
-rw-r--r--Hotline/macOS/Files/FilePreviewQuickLookView.swift53
-rw-r--r--Hotline/macOS/Files/FilesView.swift17
-rw-r--r--Hotline/macOS/Files/FolderItemView.swift2
-rw-r--r--Hotline/macOS/ServerView.swift72
-rw-r--r--Hotline/macOS/TransfersView.swift4
18 files changed, 337 insertions, 291 deletions
diff --git a/Hotline/Hotline/HotlineClientNew.swift b/Hotline/Hotline/HotlineClientNew.swift
index 854c7ff..cd34d9e 100644
--- a/Hotline/Hotline/HotlineClientNew.swift
+++ b/Hotline/Hotline/HotlineClientNew.swift
@@ -93,11 +93,11 @@ public struct HotlineServerInfo: Sendable {
// MARK: - Hotline Client
-/// Modern async/await-based Hotline protocol client
+/// A client for connecting to and interacting with Hotline servers.
///
/// Example usage:
/// ```swift
-/// let client = try await HotlineClientNew.connect(
+/// let client = try await HotlineClient.connect(
/// host: "server.example.com",
/// port: 5500,
/// login: HotlineLogin(login: "guest", password: "", username: "John", iconID: 414)
@@ -123,7 +123,7 @@ public struct HotlineServerInfo: Sendable {
/// // Get user list
/// let users = try await client.getUserList()
/// ```
-public actor HotlineClientNew {
+public actor HotlineClient {
// MARK: - Properties
private let socket: NetSocket
@@ -189,23 +189,23 @@ public actor HotlineClientNew {
host: String,
port: UInt16 = 5500,
login: HotlineLogin
- ) async throws -> HotlineClientNew {
- print("HotlineClientNew.connect(): Starting connection to \(host):\(port) as '\(login.username)'")
+ ) async throws -> HotlineClient {
+ print("HotlineClient.connect(): Starting connection to \(host):\(port) as '\(login.username)'")
// Connect socket
- print("HotlineClientNew.connect(): Connecting socket...")
+ print("HotlineClient.connect(): Connecting socket...")
let socket = try await NetSocket.connect(host: host, port: port)
- print("HotlineClientNew.connect(): Socket connected")
+ print("HotlineClient.connect(): Socket connected")
// Perform handshake
- print("HotlineClientNew.connect(): Sending handshake...")
+ print("HotlineClient.connect(): Sending handshake...")
try await socket.write(handshakeData)
let handshakeResponse = try await socket.read(8)
- print("HotlineClientNew.connect(): Handshake response received")
+ print("HotlineClient.connect(): Handshake response received")
// Verify handshake
guard handshakeResponse.prefix(4) == Data([0x54, 0x52, 0x54, 0x50]) else {
- print("HotlineClientNew.connect(): Invalid handshake response")
+ print("HotlineClient.connect(): Invalid handshake response")
throw HotlineClientError.connectionFailed(
NSError(domain: "HotlineClient", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Invalid handshake response"
@@ -215,7 +215,7 @@ public actor HotlineClientNew {
let errorCode = handshakeResponse.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt32.self) }
guard errorCode.bigEndian == 0 else {
- print("HotlineClientNew.connect(): Handshake failed with error code \(errorCode)")
+ print("HotlineClient.connect(): Handshake failed with error code \(errorCode)")
throw HotlineClientError.connectionFailed(
NSError(domain: "HotlineClient", code: Int(errorCode), userInfo: [
NSLocalizedDescriptionKey: "Handshake failed with error code \(errorCode)"
@@ -224,24 +224,24 @@ public actor HotlineClientNew {
}
// Create client
- print("HotlineClientNew.connect(): Creating client instance")
- let client = HotlineClientNew(socket: socket)
+ print("HotlineClient.connect(): Creating client instance")
+ let client = HotlineClient(socket: socket)
// Start receive loop
- print("HotlineClientNew.connect(): Starting receive loop")
+ print("HotlineClient.connect(): Starting receive loop")
await client.startReceiveLoop()
// Perform login
- print("HotlineClientNew.connect(): Performing login")
+ print("HotlineClient.connect(): Performing login")
let serverInfo = try await client.performLogin(login)
await client.setServerInfo(serverInfo)
- print("HotlineClientNew.connect(): Login successful")
+ print("HotlineClient.connect(): Login successful")
// Start keep-alive
- print("HotlineClientNew.connect(): Starting keep-alive")
+ print("HotlineClient.connect(): Starting keep-alive")
await client.startKeepAlive()
- print("HotlineClientNew.connect(): Connected to \(serverInfo.name) (v\(serverInfo.version))")
+ print("HotlineClient.connect(): Connected to \(serverInfo.name) (v\(serverInfo.version))")
return client
}
@@ -296,19 +296,19 @@ public actor HotlineClientNew {
isConnected = false
- print("HotlineClientNew.disconnect(): Starting disconnect")
+ print("HotlineClient.disconnect(): Starting disconnect")
self.receiveTask?.cancel()
self.keepAliveTask?.cancel()
await self.socket.close()
self.failAllPendingTransactions(HotlineClientError.notConnected)
self.eventContinuation.finish()
- print("HotlineClientNew.disconnect(): Disconnect complete")
+ print("HotlineClient.disconnect(): Disconnect complete")
}
// MARK: - Receive Loop
private func startReceiveLoop() {
- print("HotlineClientNew.startReceiveLoop(): Creating receive task")
+ print("HotlineClient.startReceiveLoop(): Creating receive task")
self.receiveTask = Task { [weak self] in
guard let self else {
return
@@ -320,21 +320,21 @@ public actor HotlineClientNew {
let transaction = try await self.socket.receive(HotlineTransaction.self, endian: .big)
await self.handleTransaction(transaction)
}
- print("HotlineClientNew.startReceiveLoop(): Task cancelled, exiting loop")
+ print("HotlineClient.startReceiveLoop(): Task cancelled, exiting loop")
} catch {
if Task.isCancelled || error is CancellationError {
- print("HotlineClientNew.startReceiveLoop(): Receive loop cancelled")
+ print("HotlineClient.startReceiveLoop(): Receive loop cancelled")
} else {
- print("HotlineClientNew.startReceiveLoop(): Receive loop error: \(error)")
+ print("HotlineClient.startReceiveLoop(): Receive loop error: \(error)")
await self.disconnect()
}
}
- print("HotlineClientNew.startReceiveLoop(): Receive loop ended")
+ print("HotlineClient.startReceiveLoop(): Receive loop ended")
}
}
private func handleTransaction(_ transaction: HotlineTransaction) {
- print("HotlineClientNew: <= \(transaction.type) [\(transaction.id)]")
+ print("HotlineClient: <= \(transaction.type) [\(transaction.id)]")
// Check if this is a reply to a pending transaction
if transaction.isReply == 1 || transaction.type == .reply {
@@ -348,7 +348,7 @@ public actor HotlineClientNew {
private func handleReply(_ transaction: HotlineTransaction) {
guard let continuation = pendingTransactions.removeValue(forKey: transaction.id) else {
- print("HotlineClientNew: Received reply for unknown transaction \(transaction.id)")
+ print("HotlineClient: Received reply for unknown transaction \(transaction.id)")
return
}
@@ -359,7 +359,6 @@ public actor HotlineClientNew {
message: errorText
))
} else {
- print("HELLO")
continuation.resume(returning: transaction)
}
}
@@ -417,14 +416,15 @@ public actor HotlineClientNew {
}
default:
- print("HotlineClientNew: Unhandled event type \(transaction.type)")
+ print("HotlineClient: Unhandled event type \(transaction.type)")
}
}
// MARK: - Transaction Sending
+ @discardableResult
private func sendTransaction(_ transaction: HotlineTransaction, timeout: TimeInterval = 30.0) async throws -> HotlineTransaction {
- print("HotlineClientNew: => \(transaction.type) [\(transaction.id)]")
+ print("HotlineClient: => \(transaction.type) [\(transaction.id)]")
let transactionID = transaction.id
@@ -518,7 +518,7 @@ public actor HotlineClientNew {
let _ = try? await self.getUserList()
}
} catch {
- print("HotlineClientNew: Keep-alive failed: \(error)")
+ print("HotlineClient: Keep-alive failed: \(error)")
}
}
@@ -605,7 +605,7 @@ public actor HotlineClientNew {
try await socket.send(transaction, endian: .big)
}
- // MARK: - Public API - Files
+ // MARK: - Files
/// Get the file list for a directory
///
@@ -617,7 +617,7 @@ public actor HotlineClientNew {
transaction.setFieldPath(type: .filePath, val: path)
}
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
var files: [HotlineFile] = []
for field in reply.getFieldList(type: .fileNameWithInfo) {
@@ -649,7 +649,7 @@ public actor HotlineClientNew {
transaction.setFieldUInt32(type: .fileTransferOptions, val: 2)
}
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferSize = reply.getField(type: .transferSize)?.getInteger(),
@@ -664,7 +664,7 @@ public actor HotlineClientNew {
return (referenceNumber, transferSize, fileSize, waitingCount)
}
- // MARK: - Public API - News
+ // MARK: - News
/// Get news categories at a path
///
@@ -676,7 +676,7 @@ public actor HotlineClientNew {
transaction.setFieldPath(type: .newsPath, val: path)
}
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
var categories: [HotlineNewsCategory] = []
for field in reply.getFieldList(type: .newsCategoryListData15) {
@@ -698,7 +698,7 @@ public actor HotlineClientNew {
transaction.setFieldPath(type: .newsPath, val: path)
}
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard let articleData = reply.getField(type: .newsArticleListData) else {
return []
@@ -725,7 +725,7 @@ public actor HotlineClientNew {
transaction.setFieldUInt32(type: .newsArticleID, val: id)
transaction.setFieldString(type: .newsArticleDataFlavor, val: flavor, encoding: .ascii)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
return reply.getField(type: .newsArticleData)?.getString()
}
@@ -754,17 +754,17 @@ public actor HotlineClientNew {
transaction.setFieldUInt32(type: .newsArticleFlags, val: 0)
transaction.setFieldString(type: .newsArticleData, val: text)
- _ = try await sendTransaction(transaction)
+ try await self.sendTransaction(transaction)
}
- // MARK: - Public API - Message Board
+ // MARK: - Message Board
/// Get message board posts
///
/// - Returns: Array of message strings
public func getMessageBoard() async throws -> [String] {
let transaction = HotlineTransaction(id: self.generateTransactionID(), type: .getMessageBoard)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard let text = reply.getField(type: .data)?.getString() else {
return []
@@ -784,10 +784,10 @@ public actor HotlineClientNew {
var transaction = HotlineTransaction(id: self.generateTransactionID(), type: .oldPostNews)
transaction.setFieldString(type: .data, val: text, encoding: .macOSRoman)
- try await socket.send(transaction, endian: .big)
+ try await self.socket.send(transaction, endian: .big)
}
- // MARK: - Public API - File Operations
+ // MARK: - File Operations
/// Get detailed information about a file
///
@@ -840,7 +840,7 @@ public actor HotlineClientNew {
transaction.setFieldPath(type: .filePath, val: path)
do {
- _ = try await sendTransaction(transaction)
+ try await self.sendTransaction(transaction)
return true
} catch {
return false
@@ -854,7 +854,7 @@ public actor HotlineClientNew {
/// - Returns: Array of user accounts sorted by login
public func getAccounts() async throws -> [HotlineAccount] {
let transaction = HotlineTransaction(id: self.generateTransactionID(), type: .getAccounts)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
let accountFields = reply.getFieldList(type: .data)
var accounts: [HotlineAccount] = []
@@ -886,7 +886,7 @@ public actor HotlineClientNew {
transaction.setFieldEncodedString(type: .userPassword, val: password)
}
- _ = try await sendTransaction(transaction)
+ try await self.sendTransaction(transaction)
}
/// Update an existing user account (requires admin access)
@@ -919,7 +919,7 @@ public actor HotlineClientNew {
transaction.setFieldEncodedString(type: .userPassword, val: password!)
}
- _ = try await sendTransaction(transaction)
+ try await self.sendTransaction(transaction)
}
/// Delete a user account (requires admin access)
@@ -929,7 +929,7 @@ public actor HotlineClientNew {
var transaction = HotlineTransaction(id: self.generateTransactionID(), type: .deleteUser)
transaction.setFieldEncodedString(type: .userLogin, val: login)
- _ = try await sendTransaction(transaction)
+ try await self.sendTransaction(transaction)
}
// MARK: - Banners
@@ -940,7 +940,7 @@ public actor HotlineClientNew {
/// - Throws: HotlineClientError if not connected or server doesn't support banners
public func downloadBanner() async throws -> (referenceNumber: UInt32, transferSize: Int)? {
let transaction = HotlineTransaction(id: self.generateTransactionID(), type: .downloadBanner)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferSizeField = reply.getField(type: .transferSize),
@@ -972,7 +972,7 @@ public actor HotlineClientNew {
transaction.setFieldUInt32(type: .fileTransferOptions, val: 2)
}
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferSizeField = reply.getField(type: .transferSize),
@@ -1000,7 +1000,7 @@ public actor HotlineClientNew {
transaction.setFieldString(type: .fileName, val: name)
transaction.setFieldPath(type: .filePath, val: path)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferSizeField = reply.getField(type: .transferSize),
@@ -1027,7 +1027,7 @@ public actor HotlineClientNew {
transaction.setFieldString(type: .fileName, val: name)
transaction.setFieldPath(type: .filePath, val: path)
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferReferenceField = reply.getField(type: .referenceNumber),
@@ -1046,7 +1046,7 @@ public actor HotlineClientNew {
/// - path: Directory path where the folder should be uploaded
/// - Returns: Reference number for the upload transfer
public func uploadFolder(name: String, path: [String], fileCount: UInt32, totalSize: UInt32) async throws -> UInt32? {
- print("HotlineClientNew: uploadFolder request - name='\(name)', path=\(path), fileCount=\(fileCount), totalSize=\(totalSize)")
+ print("HotlineClient: uploadFolder request - name='\(name)', path=\(path), fileCount=\(fileCount), totalSize=\(totalSize)")
var transaction = HotlineTransaction(id: self.generateTransactionID(), type: .uploadFolder)
transaction.setFieldString(type: .fileName, val: name)
@@ -1054,7 +1054,7 @@ public actor HotlineClientNew {
transaction.setFieldUInt32(type: .transferSize, val: totalSize)
transaction.setFieldUInt16(type: .folderItemCount, val: UInt16(truncatingIfNeeded: fileCount))
- let reply = try await sendTransaction(transaction)
+ let reply = try await self.sendTransaction(transaction)
guard
let transferReferenceField = reply.getField(type: .referenceNumber),
diff --git a/Hotline/Hotline/Transfers/HotlineFileDownloadClientNew.swift b/Hotline/Hotline/Transfers/HotlineFileDownloadClient.swift
index 82f61d4..82f61d4 100644
--- a/Hotline/Hotline/Transfers/HotlineFileDownloadClientNew.swift
+++ b/Hotline/Hotline/Transfers/HotlineFileDownloadClient.swift
diff --git a/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift b/Hotline/Hotline/Transfers/HotlineFilePreviewClient.swift
index 5cf5628..8f3088d 100644
--- a/Hotline/Hotline/Transfers/HotlineFilePreviewClientNew.swift
+++ b/Hotline/Hotline/Transfers/HotlineFilePreviewClient.swift
@@ -8,23 +8,29 @@ public class HotlineFilePreviewClient {
private let referenceNumber: UInt32
private let fileName: String
private let transferSize: UInt32
+ private let fileType: String?
+ private let fileCreator: String?
private var downloadClient: HotlineFileDownloadClient?
private var previewTask: Task<URL, Error>?
private var temporaryFileURL: URL?
-
+
public init(
fileName: String,
address: String,
port: UInt16,
reference: UInt32,
- size: UInt32
+ size: UInt32,
+ fileType: String? = nil,
+ fileCreator: String? = nil
) {
self.fileName = fileName
self.serverAddress = address
self.serverPort = port
self.referenceNumber = reference
self.transferSize = size
+ self.fileType = fileType
+ self.fileCreator = fileCreator
}
// MARK: - API
@@ -105,13 +111,23 @@ public class HotlineFilePreviewClient {
progressHandler?(.connected)
// Stream raw data directly to temp file with progress tracking
- print("HotlineFilePreviewClient[\(self.referenceNumber)]: Streaming \(transferSize) bytes to temp file")
-
- let totalSize = Int(transferSize)
+ print("HotlineFilePreviewClient[\(self.referenceNumber)]: Streaming \(self.transferSize) bytes to temp file")
- // Create empty file
- FileManager.default.createFile(atPath: tempFileURL.path(percentEncoded: false), contents: nil)
+ let totalSize = Int(self.transferSize)
+ // Create empty file (with HFS attributes if available)
+ var attributes: [FileAttributeKey: Any] = [:]
+ if let creator = self.fileCreator, !creator.isBlank {
+ attributes[.hfsCreatorCode] = creator.fourCharCode() as NSNumber
+ }
+ if let type = self.fileType, !type.isBlank {
+ attributes[.hfsTypeCode] = type.fourCharCode() as NSNumber
+ }
+
+ guard FileManager.default.createFile(atPath: tempFileURL.path, contents: nil, attributes: attributes) else {
+ throw HotlineTransferClientError.failedToTransfer
+ }
+
let fileHandle = try FileHandle(forWritingTo: tempFileURL)
defer { try? fileHandle.close() }
diff --git a/Hotline/Hotline/Transfers/HotlineFileUploadClientNew.swift b/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift
index 384e9bd..384e9bd 100644
--- a/Hotline/Hotline/Transfers/HotlineFileUploadClientNew.swift
+++ b/Hotline/Hotline/Transfers/HotlineFileUploadClient.swift
diff --git a/Hotline/Hotline/Transfers/HotlineFolderDownloadClientNew.swift b/Hotline/Hotline/Transfers/HotlineFolderDownloadClient.swift
index 600b9f2..36193bd 100644
--- a/Hotline/Hotline/Transfers/HotlineFolderDownloadClientNew.swift
+++ b/Hotline/Hotline/Transfers/HotlineFolderDownloadClient.swift
@@ -9,7 +9,7 @@ public struct HotlineFolderItemProgress: Sendable {
}
@MainActor
-public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
+public class HotlineFolderDownloadClient: @MainActor HotlineTransferClient {
private let serverAddress: String
private let serverPort: UInt16
private let referenceNumber: UInt32
@@ -22,8 +22,6 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
private var downloadTask: Task<URL, Error>?
private var folderProgress: Progress?
- // MARK: - Initialization
-
public init(
address: String,
port: UInt16,
@@ -36,11 +34,9 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
self.referenceNumber = reference
self.transferTotal = Int(size)
self.folderItemCount = itemCount
-
- print("HotlineFolderDownloadClientNew[\(reference)]: Server reported transferSize=\(size) bytes, folderItemCount=\(itemCount) items")
}
- // MARK: - Public API
+ // MARK: - API
public func download(
to location: HotlineDownloadLocation,
@@ -63,7 +59,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
self.downloadTask = nil
return url
} catch {
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Failed to download folder: \(error)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Failed to download folder: \(error)")
self.downloadTask = nil
progressHandler?(.error(error))
throw error
@@ -82,7 +78,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
}
}
- // MARK: - Private Implementation
+ // MARK: - Implementation
private func performDownload(
to destination: HotlineDownloadLocation,
@@ -113,7 +109,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
destinationFilename = destinationURL.lastPathComponent
}
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Downloading folder to \(destinationURL.path)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Downloading folder to \(destinationURL.path)")
// Create destination folder
try? fm.removeItem(at: destinationURL)
@@ -127,7 +123,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
self.folderProgress = progress
// Send initial magic header
- print("HotlineFolderDownloadClientNew[\(self.referenceNumber)]: Sending HTXF magic")
+ print("HotlineFolderDownloadClient[\(self.referenceNumber)]: Sending HTXF magic")
try await socket.write(Data(endian: .big) {
"HTXF".fourCharCode()
self.referenceNumber
@@ -157,14 +153,14 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
}
let joinedPath = pathComponents.joined(separator: "/")
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Item type=\(itemType) path=\(joinedPath)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Item type=\(itemType) path=\(joinedPath)")
if itemType == 1 {
// Folder entry - no progress shown for folder creation
if !pathComponents.isEmpty {
let folderURL = destinationURL.appendingPathComponents(pathComponents)
try fm.createDirectory(at: folderURL, withIntermediateDirectories: true)
- print("HotlineFolderDownloadClientNew[\(self.referenceNumber)]: Created folder at \(folderURL.path)")
+ print("HotlineFolderDownloadClient[\(self.referenceNumber)]: Created folder at \(folderURL.path)")
}
completedItemCount += 1
@@ -187,7 +183,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
let fileSize = fileSizeData.readUInt32(at: 0)!
totalBytesTransferred += 4
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: File '\(fileName)' size: \(fileSize) bytes")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: File '\(fileName)' size: \(fileSize) bytes")
// Notify item progress before download starts
completedItemCount += 1
@@ -213,7 +209,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
totalBytesTransferred += fileBytesRead
self.transferSize = totalBytesTransferred
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Downloaded file to \(fileURL.path)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Downloaded file to \(fileURL.path)")
// Request next item if not done
if completedItemCount < folderItemCount {
@@ -222,7 +218,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
} else {
// Unknown item type
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Unknown item type \(itemType), skipping")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Unknown item type \(itemType), skipping")
completedItemCount += 1
if completedItemCount < folderItemCount {
@@ -231,7 +227,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
}
}
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Download complete!")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Download complete!")
// Ensure folder progress shows 100% complete
self.folderProgress?.completedUnitCount = Int64(self.transferTotal)
@@ -244,14 +240,14 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
// MARK: - Helper Methods
private func connectToTransferServer() async throws -> NetSocket {
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Connecting to \(serverAddress):\(serverPort + 1)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Connecting to \(serverAddress):\(serverPort + 1)")
let socket = try await NetSocket.connect(
host: self.serverAddress,
port: self.serverPort + 1
)
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Connected!")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Connected!")
return socket
}
@@ -260,7 +256,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
action.rawValue
}
try await socket.write(actionData)
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Sent action: \(action)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Sent action: \(action)")
}
private func parseItemHeaderPath(_ headerData: Data) -> (type: UInt16, components: [String])? {
@@ -314,7 +310,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
let totalBytesNow = totalBytesTransferredSoFar + bytesRead
self.folderProgress?.completedUnitCount = Int64(totalBytesNow)
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: File has \(header.forkCount) forks")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: File has \(header.forkCount) forks")
var resourceForkData: Data?
var fileHandle: FileHandle?
@@ -340,7 +336,7 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
if forkHeader.isInfoFork {
// Read INFO fork
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Reading INFO fork (\(forkHeader.dataSize) bytes)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Reading INFO fork (\(forkHeader.dataSize) bytes)")
let infoData = try await socket.read(Int(forkHeader.dataSize))
bytesRead += infoData.count
@@ -366,9 +362,10 @@ public class HotlineFolderDownloadClientNew: @MainActor HotlineTransferClient {
try? fm.removeItem(at: finalURL)
fileHandle = try fm.createHotlineFile(at: finalURL, infoFork: info)
- } else if forkHeader.isDataFork {
+ }
+ else if forkHeader.isDataFork {
// Stream DATA fork to disk
- print("HotlineFolderDownloadClientNew[\(referenceNumber)]: Reading DATA fork (\(forkHeader.dataSize) bytes)")
+ print("HotlineFolderDownloadClient[\(referenceNumber)]: Reading DATA fork (\(forkHeader.dataSize) bytes)")
guard let fh = fileHandle else {
throw HotlineTransferClientError.failedToTransfer
diff --git a/Hotline/Hotline/Transfers/HotlineFolderUploadClientNew.swift b/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift
index 15636f3..1947de6 100644
--- a/Hotline/Hotline/Transfers/HotlineFolderUploadClientNew.swift
+++ b/Hotline/Hotline/Transfers/HotlineFolderUploadClient.swift
@@ -16,16 +16,12 @@ private struct FolderItem {
}
@MainActor
-public class HotlineFolderUploadClientNew: @MainActor HotlineTransferClient {
- // MARK: - Configuration
-
+public class HotlineFolderUploadClient: @MainActor HotlineTransferClient {
public struct Configuration: Sendable {
public var chunkSize: Int = 256 * 1024
public init() {}
}
- // MARK: - Properties
-
private let serverAddress: String
private let serverPort: UInt16
private let referenceNumber: UInt32
@@ -41,8 +37,6 @@ public class HotlineFolderUploadClientNew: @MainActor HotlineTransferClient {
private var socket: NetSocket?
private var uploadTask: Task<Void, Error>?
- // MARK: - Initialization
-
public init?(
folderURL: URL,
address: String,
@@ -108,7 +102,7 @@ public class HotlineFolderUploadClientNew: @MainActor HotlineTransferClient {
}
}
- // MARK: -
+ // MARK: - Implementation
private enum UploadStage {
case waitingForNextFile // Waiting for server to send .nextFile action
diff --git a/Hotline/Library/Extensions.swift b/Hotline/Library/Extensions.swift
index bb28370..cf9f8ff 100644
--- a/Hotline/Library/Extensions.swift
+++ b/Hotline/Library/Extensions.swift
@@ -2,23 +2,85 @@ import Foundation
import SwiftUI
import UniformTypeIdentifiers
+extension FileManager {
+ @discardableResult
+ func moveToDownloads(from sourceURL: URL, using filename: String, bounceDock: Bool = false) -> Bool {
+ let filePath = URL.downloadsDirectory.generateUniqueFilePath(filename: filename)
+ let destinationURL = URL(filePath: filePath).resolvingSymlinksInPath()
+
+ do {
+ try FileManager.default.moveItem(at: sourceURL.resolvingSymlinksInPath(), to: destinationURL)
+ }
+ catch {
+ return false
+ }
+
+ if bounceDock {
+ #if os(macOS)
+ DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: destinationURL.path)
+ #endif
+ }
+
+ return true
+ }
+
+ @discardableResult
+ func copyToDownloads(from sourceURL: URL, using filename: String, bounceDock: Bool = false) -> Bool {
+ let filePath = URL.downloadsDirectory.generateUniqueFilePath(filename: filename)
+ let destinationURL = URL(filePath: filePath).resolvingSymlinksInPath()
+
+ do {
+ try FileManager.default.copyItem(at: sourceURL.resolvingSymlinksInPath(), to: destinationURL)
+ }
+ catch {
+ return false
+ }
+
+ if bounceDock {
+ #if os(macOS)
+ DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: destinationURL.path)
+ #endif
+ }
+
+ return true
+ }
+}
+
+// MARK: -
+
+extension View {
+ @ViewBuilder
+ func applyNavigationDocumentIfPresent(_ url: URL?) -> some View {
+ if let url {
+ self.navigationDocument(url)
+ } else {
+ self
+ }
+ }
+}
+
+// MARK: -
+
extension Data {
func saveAsFileToDownloads(filename: String, bounceDock: Bool = true) -> Bool {
- let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
- let filePath = folderURL.generateUniqueFilePath(filename: filename)
- if FileManager.default.createFile(atPath: filePath, contents: nil) {
- if let h = FileHandle(forWritingAtPath: filePath) {
- try? h.write(contentsOf: self)
- try? h.close()
- if bounceDock {
- #if os(macOS)
- var downloadURL = URL(filePath: filePath)
- downloadURL.resolveSymlinksInPath()
- DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path)
- #endif
- }
- return true
+ let filePath = URL.downloadsDirectory.generateUniqueFilePath(filename: filename)
+
+ if FileManager.default.createFile(atPath: filePath, contents: self) {
+ if bounceDock {
+ #if os(macOS)
+ var downloadURL = URL(filePath: filePath)
+ downloadURL.resolveSymlinksInPath()
+ DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path)
+ #endif
}
+ return true
+
+// if FileManager.default.createFile(atPath: filePath, contents: nil) {
+// if let h = FileHandle(forWritingAtPath: filePath) {
+// try? h.write(contentsOf: self)
+// try? h.close()
+//
+// }
}
return false
}
diff --git a/Hotline/Models/FileInfo.swift b/Hotline/Models/FileInfo.swift
index e3081c4..a2f0ea5 100644
--- a/Hotline/Models/FileInfo.swift
+++ b/Hotline/Models/FileInfo.swift
@@ -49,13 +49,19 @@ import UniformTypeIdentifiers
var children: [FileInfo]? = nil
var isPreviewable: Bool {
- let fileExtension = (self.name as NSString).pathExtension.lowercased()
+ var fileExtension = (self.name as NSString).pathExtension.lowercased()
+ if fileExtension.isEmpty && !self.type.isEmpty {
+ let type = self.type.lowercased()
+ if let ext = FileManager.HFSTypeToExtension[type] {
+ fileExtension = ext
+ }
+ }
+
if let fileType = UTType(filenameExtension: fileExtension) {
if fileType.canBePreviewedByQuickLook {
return true
}
- print("FILE TYPE?", fileType, fileExtension, fileType.isSubtype(of: .pdf), fileType.isSupertype(of: .pdf))
if fileType.isSubtype(of: .image) {
return true
}
diff --git a/Hotline/Models/PreviewFileInfo.swift b/Hotline/Models/PreviewFileInfo.swift
index dcf5314..7e05a97 100644
--- a/Hotline/Models/PreviewFileInfo.swift
+++ b/Hotline/Models/PreviewFileInfo.swift
@@ -7,6 +7,9 @@ struct PreviewFileInfo: Identifiable, Codable {
var size: Int
var name: String
+ var type: String? = nil
+ var creator: String? = nil
+
var previewType: FilePreviewType {
let fileExtension = (self.name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
diff --git a/Hotline/State/AppUpdate.swift b/Hotline/State/AppUpdate.swift
index 601859e..558a296 100644
--- a/Hotline/State/AppUpdate.swift
+++ b/Hotline/State/AppUpdate.swift
@@ -35,8 +35,6 @@ final class AppUpdate {
case manual
}
- // MARK: - Public State
-
var isChecking = false
var isDownloading = false
var showWindow = false
@@ -55,7 +53,7 @@ final class AppUpdate {
private let remindDateKey = "update.remind.date"
private let lastPromptedVersionKey = "update.last.prompt.version"
- // MARK: - Public API
+ // MARK: - API
func checkForUpdatesOnLaunch() async {
await checkForUpdates(trigger: .automatic)
@@ -100,7 +98,7 @@ final class AppUpdate {
resetAndCloseWindow()
}
- // MARK: - Internal Logic
+ // MARK: - Implementation
private func checkForUpdates(trigger: CheckTrigger) async {
await MainActor.run {
@@ -259,8 +257,7 @@ final class AppUpdate {
private func downloadRelease(_ release: UpdateReleaseInfo) async {
do {
let (temporaryURL, _) = try await URLSession.shared.download(from: release.downloadURL)
- let downloadsDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!
- let destinationURL = downloadsDirectory.appendingPathComponent(release.assetName)
+ let destinationURL = URL.downloadsDirectory.appendingPathComponent(release.assetName)
if FileManager.default.fileExists(atPath: destinationURL.path) {
try? FileManager.default.removeItem(at: destinationURL)
diff --git a/Hotline/State/FilePreviewState.swift b/Hotline/State/FilePreviewState.swift
index 27f8199..ec79f66 100644
--- a/Hotline/State/FilePreviewState.swift
+++ b/Hotline/State/FilePreviewState.swift
@@ -1,8 +1,6 @@
import SwiftUI
import UniformTypeIdentifiers
-// MARK: - Preview Type
-
enum FilePreviewType: Equatable {
case unknown
case image
@@ -13,13 +11,15 @@ enum FilePreviewType: Equatable {
@MainActor
@Observable
final class FilePreviewState {
- // MARK: - Properties
-
+ enum LoadState: Equatable {
+ case unloaded
+ case loading
+ case loaded
+ case failed
+ }
+
let info: PreviewFileInfo
- private var previewClient: HotlineFilePreviewClient?
- private var previewTask: Task<Void, Never>?
-
var state: LoadState = .unloaded
var progress: Double = 0.0
@@ -33,39 +33,35 @@ final class FilePreviewState {
var text: String? = nil
var styledText: NSAttributedString? = nil
-
- // MARK: - Computed Properties
+
+ @ObservationIgnored private var previewClient: HotlineFilePreviewClient?
+ @ObservationIgnored private var previewTask: Task<Void, Never>?
var previewType: FilePreviewType {
- info.previewType
+ self.info.previewType
}
- // MARK: - Initialization
-
init(info: PreviewFileInfo) {
self.info = info
}
- nonisolated deinit {
- // Note: Can't access @MainActor properties from deinit
- // Cleanup will happen when previewClient is deallocated
- }
-
- // MARK: - Public API
+ // MARK: - API
func download() {
// Cancel any existing download
- previewTask?.cancel()
- previewClient?.cleanup()
+ self.previewTask?.cancel()
+ self.previewClient?.cleanup()
let task = Task { @MainActor in
do {
let client = HotlineFilePreviewClient(
- fileName: info.name,
- address: info.address,
- port: UInt16(info.port),
- reference: info.id,
- size: UInt32(info.size)
+ fileName: self.info.name,
+ address: self.info.address,
+ port: UInt16(self.info.port),
+ reference: self.info.id,
+ size: UInt32(self.info.size),
+ fileType: self.info.type,
+ fileCreator: self.info.creator
)
self.previewClient = client
@@ -132,69 +128,58 @@ final class FilePreviewState {
}
func cancel() {
- previewTask?.cancel()
- previewTask = nil
- previewClient?.cancel()
+ self.previewTask?.cancel()
+ self.previewTask = nil
+ self.previewClient?.cancel()
}
func cleanup() {
- previewClient?.cleanup()
- previewClient = nil
- fileURL = nil
- image = nil
- text = nil
- styledText = nil
+ self.previewClient?.cleanup()
+ self.previewClient = nil
+ self.fileURL = nil
+ self.image = nil
+ self.text = nil
+ self.styledText = nil
}
- // MARK: - Private Implementation
+ // MARK: - Utility
- private func loadPreview(from url: URL) {
- guard let data = try? Data(contentsOf: url) else {
- self.state = .failed
- print("FilePreviewState: Failed to read preview data from \(url.path)")
- return
- }
-
- switch self.previewType {
- case .image:
- #if os(iOS)
- self.image = UIImage(data: data)
- #elseif os(macOS)
- self.image = NSImage(data: data)
- #endif
-
- if self.image == nil {
- self.state = .failed
- print("FilePreviewState: Failed to create image from data")
- }
-
- case .text:
- let encoding: UInt = NSString.stringEncoding(for: data, convertedString: nil, usedLossyConversion: nil)
- if encoding != 0 {
- self.text = String(data: data, encoding: String.Encoding(rawValue: encoding))
- } else {
- self.text = String(data: data, encoding: .utf8)
- }
-
- if self.text == nil {
- self.state = .failed
- print("FilePreviewState: Failed to decode text data")
- }
-
- case .unknown:
- print("FilePreviewState: Unknown preview type for \(info.name)")
- break
- }
- }
-}
-
-// MARK: - Load State
-
-extension FilePreviewState {
- enum LoadState: Equatable {
- case unloaded
- case loading
- case loaded
- case failed
- }
+// private func loadPreview(from url: URL) {
+// guard let data = try? Data(contentsOf: url) else {
+// self.state = .failed
+// print("FilePreviewState: Failed to read preview data from \(url.path)")
+// return
+// }
+//
+// switch self.previewType {
+// case .image:
+// #if os(iOS)
+// self.image = UIImage(data: data)
+// #elseif os(macOS)
+// self.image = NSImage(data: data)
+// #endif
+//
+// if self.image == nil {
+// self.state = .failed
+// print("FilePreviewState: Failed to create image from data")
+// }
+//
+// case .text:
+// let encoding: UInt = NSString.stringEncoding(for: data, convertedString: nil, usedLossyConversion: nil)
+// if encoding != 0 {
+// self.text = String(data: data, encoding: String.Encoding(rawValue: encoding))
+// } else {
+// self.text = String(data: data, encoding: .utf8)
+// }
+//
+// if self.text == nil {
+// self.state = .failed
+// print("FilePreviewState: Failed to decode text data")
+// }
+//
+// case .unknown:
+// print("FilePreviewState: Unknown preview type for \(info.name)")
+// break
+// }
+// }
}
diff --git a/Hotline/State/HotlineState.swift b/Hotline/State/HotlineState.swift
index e80571a..f814818 100644
--- a/Hotline/State/HotlineState.swift
+++ b/Hotline/State/HotlineState.swift
@@ -252,7 +252,7 @@ class HotlineState: Equatable {
// MARK: - Private State
- @ObservationIgnored private var client: HotlineClientNew?
+ @ObservationIgnored private var client: HotlineClient?
@ObservationIgnored private var eventTask: Task<Void, Never>?
@ObservationIgnored private var chatSessionKey: ChatStore.SessionKey?
@ObservationIgnored private var restoredChatSessionKey: ChatStore.SessionKey?
@@ -308,13 +308,13 @@ class HotlineState: Equatable {
iconID: UInt16(iconID)
)
- print("HotlineState.login(): Calling HotlineClientNew.connect()...")
- let client = try await HotlineClientNew.connect(
+ print("HotlineState.login(): Calling HotlineClient.connect()...")
+ let client = try await HotlineClient.connect(
host: server.address,
port: UInt16(server.port),
login: loginInfo
)
- print("HotlineState.login(): HotlineClientNew.connect() returned")
+ print("HotlineState.login(): HotlineClient.connect() returned")
self.client = client
print("HotlineState.login(): Client stored")
@@ -865,7 +865,7 @@ class HotlineState: Equatable {
/// - progressCallback: Optional callback for progress updates (receives TransferInfo and progress 0.0-1.0)
/// - callback: Optional completion callback (receives TransferInfo and final file URL)
@MainActor
- func downloadFileNew(_ fileName: String, path: [String], to destination: URL? = nil, progress progressCallback: ((TransferInfo) -> Void)? = nil, complete callback: ((TransferInfo) -> Void)? = nil) {
+ func downloadFile(_ fileName: String, path: [String], to destination: URL? = nil, progress progressCallback: ((TransferInfo) -> Void)? = nil, complete callback: ((TransferInfo) -> Void)? = nil) {
guard let client = self.client else { return }
var fullPath: [String] = []
@@ -975,7 +975,7 @@ class HotlineState: Equatable {
/// - itemProgressCallback: Optional callback for per-item updates (receives TransferInfo with current file info)
/// - callback: Optional completion callback (receives TransferInfo and final folder URL)
@MainActor
- func downloadFolderNew(
+ func downloadFolder(
_ folderName: String,
path: [String],
to destination: URL? = nil,
@@ -1018,7 +1018,7 @@ class HotlineState: Equatable {
AppState.shared.addTransfer(transfer)
// Create download client
- let downloadClient = HotlineFolderDownloadClientNew(
+ let downloadClient = HotlineFolderDownloadClient(
address: address,
port: UInt16(port),
reference: referenceNumber,
@@ -1091,7 +1091,7 @@ class HotlineState: Equatable {
}
}
- /// Modern async/await folder upload using HotlineFolderUploadClientNew
+ /// Upload a folder to the server.
///
/// - Parameters:
/// - folderURL: URL to the folder on disk to upload
@@ -1155,7 +1155,7 @@ class HotlineState: Equatable {
print("HotlineState: Got folder upload reference: \(referenceNumber)")
// Create upload client
- guard let uploadClient = HotlineFolderUploadClientNew(
+ guard let uploadClient = HotlineFolderUploadClient(
folderURL: folderURL,
address: address,
port: UInt16(port),
@@ -1346,9 +1346,9 @@ class HotlineState: Equatable {
}
func setFileInfo(fileName: String, path filePath: [String], fileNewName: String?, comment: String?, encoding: String.Encoding = .utf8) {
- // TODO: Implement setFileInfo in HotlineClientNew
+ // TODO: Implement setFileInfo in HotlineClient
// This method updates file metadata (name and/or comment)
- print("setFileInfo not yet implemented in HotlineState/HotlineClientNew")
+ print("setFileInfo not yet implemented in HotlineState/HotlineClient")
}
@MainActor
diff --git a/Hotline/State/ServerState.swift b/Hotline/State/ServerState.swift
index 805672d..5913bf5 100644
--- a/Hotline/State/ServerState.swift
+++ b/Hotline/State/ServerState.swift
@@ -29,7 +29,7 @@ enum ServerNavigationType: Identifiable, Hashable, Equatable {
case .files:
return "Files"
case .accounts:
- return "Admin"
+ return "Accounts"
case .user(let userID):
return String(userID)
}
diff --git a/Hotline/macOS/Files/FilePreviewQuickLookView.swift b/Hotline/macOS/Files/FilePreviewQuickLookView.swift
index 26bd286..a504a7a 100644
--- a/Hotline/macOS/Files/FilePreviewQuickLookView.swift
+++ b/Hotline/macOS/Files/FilePreviewQuickLookView.swift
@@ -11,16 +11,16 @@ struct FilePreviewQuickLookView: View {
@Environment(\.dismiss) private var dismiss
@Binding var info: PreviewFileInfo?
- @State var preview: FilePreviewState? = nil
+ @State private var preview: FilePreviewState? = nil
@FocusState private var focusField: FilePreviewFocus?
var body: some View {
Group {
- if preview?.state != .loaded {
+ if self.preview?.state != .loaded {
VStack(alignment: .center, spacing: 0) {
Spacer()
- ProgressView(value: max(0.0, min(1.0, preview?.progress ?? 0.0)))
+ ProgressView(value: max(0.0, min(1.0, self.preview?.progress ?? 0.0)))
.focusable(false)
.progressViewStyle(.circular)
.controlSize(.extraLarge)
@@ -33,7 +33,7 @@ struct FilePreviewQuickLookView: View {
.padding()
}
else {
- if let fileURL = preview?.fileURL {
+ if let fileURL = self.preview?.fileURL {
QuickLookPreviewView(fileURL: fileURL)
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity)
}
@@ -68,25 +68,15 @@ struct FilePreviewQuickLookView: View {
.focusable()
.focusEffectDisabled()
.background(Color(nsColor: .textBackgroundColor))
- .focused($focusField, equals: .window)
- .navigationTitle(info?.name ?? "File Preview")
- .background {
- if let fileURL = self.preview?.fileURL {
- WindowConfigurator { window in
- window.representedURL = fileURL
- window.standardWindowButton(.documentIconButton)?.isHidden = false
- }
- }
- }
+ .focused(self.$focusField, equals: .window)
+ .navigationTitle(self.info?.name ?? "File Preview")
+ .applyNavigationDocumentIfPresent(self.preview?.fileURL)
.toolbar {
- if let _ = preview?.fileURL {
+ if let fileURL = self.preview?.fileURL {
if let info = info {
ToolbarItem(placement: .primaryAction) {
Button {
- if let fileURL = preview?.fileURL,
- let data = try? Data(contentsOf: fileURL) {
- let _ = data.saveAsFileToDownloads(filename: info.name)
- }
+ FileManager.default.copyToDownloads(from: fileURL, using: info.name, bounceDock: true)
} label: {
Label("Download File...", systemImage: "arrow.down")
}
@@ -96,28 +86,27 @@ struct FilePreviewQuickLookView: View {
}
}
.task {
- if let info = info {
- preview = FilePreviewState(info: info)
- preview?.download()
+ if let info = self.info {
+ self.preview = FilePreviewState(info: info)
+ self.preview?.download()
}
}
.onAppear {
- if info == nil {
- Task {
- dismiss()
- }
+ guard self.info != nil else {
+ self.dismiss()
return
}
- focusField = .window
+ self.focusField = .window
}
.onDisappear {
- preview?.cancel()
- dismiss()
+ self.preview?.cancel()
+ self.preview?.cleanup()
+ self.dismiss()
}
- .onChange(of: preview?.state) {
- if preview?.state == .failed {
- dismiss()
+ .onChange(of: self.preview?.state) {
+ if self.preview?.state == .failed {
+ self.dismiss()
}
}
.preferredColorScheme(.dark)
diff --git a/Hotline/macOS/Files/FilesView.swift b/Hotline/macOS/Files/FilesView.swift
index d4ba5c8..984b120 100644
--- a/Hotline/macOS/Files/FilesView.swift
+++ b/Hotline/macOS/Files/FilesView.swift
@@ -376,11 +376,11 @@ struct FilesView: View {
private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
switch previewInfo.previewType {
case .image:
- openWindow(id: "preview-quicklook", value: previewInfo)
+ self.openWindow(id: "preview-quicklook", value: previewInfo)
case .text:
- openWindow(id: "preview-quicklook", value: previewInfo)
+ self.openWindow(id: "preview-quicklook", value: previewInfo)
case .unknown:
- openWindow(id: "preview-quicklook", value: previewInfo)
+ self.openWindow(id: "preview-quicklook", value: previewInfo)
return
}
}
@@ -397,10 +397,10 @@ struct FilesView: View {
@MainActor private func downloadFile(_ file: FileInfo) {
if file.isFolder {
- model.downloadFolderNew(file.name, path: file.path)
+ model.downloadFolder(file.name, path: file.path)
}
else {
- model.downloadFileNew(file.name, path: file.path)
+ model.downloadFile(file.name, path: file.path)
}
}
@@ -442,9 +442,12 @@ struct FilesView: View {
return
}
- model.previewFile(file.name, path: file.path) { info in
+ self.model.previewFile(file.name, path: file.path) { info in
if let info = info {
- openPreviewWindow(info)
+ var extendedInfo = info
+ extendedInfo.creator = file.creator
+ extendedInfo.type = file.type
+ self.openPreviewWindow(extendedInfo)
}
}
}
diff --git a/Hotline/macOS/Files/FolderItemView.swift b/Hotline/macOS/Files/FolderItemView.swift
index 2b1b695..9b13cc0 100644
--- a/Hotline/macOS/Files/FolderItemView.swift
+++ b/Hotline/macOS/Files/FolderItemView.swift
@@ -81,7 +81,7 @@ struct FolderItemView: View {
.opacity(file.isUnavailable ? 0.5 : 1.0)
if loading {
- ProgressView().controlSize(.small).padding([.leading, .trailing], 5)
+ ProgressView().controlSize(.mini).padding([.leading, .trailing], 5)
}
Spacer()
if !file.isUnavailable {
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 757dbd8..d4b3407 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -93,7 +93,7 @@ struct ServerView: View {
ServerMenuItem(type: .board, name: "Board", image: "Section Board"),
ServerMenuItem(type: .news, name: "News", image: "Section News"),
ServerMenuItem(type: .files, name: "Files", image: "Section Files"),
- ServerMenuItem(type: .accounts, name: "Admin", image: "Section Users"),
+ ServerMenuItem(type: .accounts, name: "Accounts", image: "Section Users"),
]
static var classicMenuItems: [ServerMenuItem] = [
@@ -280,47 +280,43 @@ struct ServerView: View {
}
var transfersSection: some View {
-// Section("Transfers") {
- ForEach(model.transfers) { transfer in
- TransferItemView(transfer: transfer)
- }
-// }
+ ForEach(model.transfers) { transfer in
+ TransferItemView(transfer: transfer)
+ }
}
var usersSection: some View {
-// Section("\(model.users.count) Online") {
- ForEach(model.users) { user in
- HStack(spacing: 5) {
- if let iconImage = HotlineState.getClassicIcon(Int(user.iconID)) {
- Image(nsImage: iconImage)
- .frame(width: 16, height: 16)
- .padding(.leading, 2)
- .padding(.trailing, 2)
- }
- else {
- Image("User")
- .frame(width: 16, height: 16)
- .padding(.leading, 2)
- .padding(.trailing, 2)
- }
-
- Text(user.name)
- .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary)
-
- Spacer()
-
- if model.hasUnreadInstantMessages(userID: user.id) {
- Circle()
- .frame(width: 6, height: 6)
- .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5))
- .padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2))
- }
+ ForEach(model.users) { user in
+ HStack(spacing: 5) {
+ if let iconImage = HotlineState.getClassicIcon(Int(user.iconID)) {
+ Image(nsImage: iconImage)
+ .frame(width: 16, height: 16)
+ .padding(.leading, 2)
+ .padding(.trailing, 2)
+ }
+ else {
+ Image("User")
+ .frame(width: 16, height: 16)
+ .padding(.leading, 2)
+ .padding(.trailing, 2)
+ }
+
+ Text(user.name)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary)
+
+ Spacer()
+
+ if model.hasUnreadInstantMessages(userID: user.id) {
+ Circle()
+ .frame(width: 6, height: 6)
+ .foregroundStyle(user.isAdmin ? Color.hotlineRed : .primary.opacity(0.5))
+ .padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 2))
}
- .opacity(user.isIdle ? 0.5 : 1.0)
- .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
- .tag(ServerNavigationType.user(userID: user.id))
}
-// }
+ .opacity(user.isIdle ? 0.5 : 1.0)
+ .opacity(controlActiveState == .inactive ? 0.5 : 1.0)
+ .tag(ServerNavigationType.user(userID: user.id))
+ }
}
var serverView: some View {
@@ -353,7 +349,7 @@ struct ServerView: View {
case .accounts:
AccountManagerView()
.navigationTitle(model.serverTitle)
- .navigationSubtitle("Administration")
+ .navigationSubtitle("Accounts")
.navigationSplitViewColumnWidth(min: 250, ideal: 500)
case .user(let userID):
let user = model.users.first(where: { $0.id == userID })
diff --git a/Hotline/macOS/TransfersView.swift b/Hotline/macOS/TransfersView.swift
index 489a50b..bf8c8bd 100644
--- a/Hotline/macOS/TransfersView.swift
+++ b/Hotline/macOS/TransfersView.swift
@@ -19,9 +19,7 @@ struct TransfersView: View {
ToolbarItem(placement: .primaryAction) {
Button {
if self.selectedTransfers.isEmpty {
- if let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first {
- NSWorkspace.shared.open(downloadsURL)
- }
+ NSWorkspace.shared.open(URL.downloadsDirectory)
}
else {
let fileURLs = self.selectedTransfers.compactMap(\.fileURL)