aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj40
-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
19 files changed, 357 insertions, 311 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index aa10d51..57e7032 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -22,9 +22,9 @@
DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32CD4A2B29318E0053B98B /* FileInfo.swift */; };
DA32CD4D2B2931B50053B98B /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32CD4C2B2931B50053B98B /* ChatMessage.swift */; };
DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */; };
- DA3429AE2EB9C0280010784E /* HotlineFileUploadClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429AD2EB9C0220010784E /* HotlineFileUploadClientNew.swift */; };
- DA3429B02EBA70790010784E /* HotlineFolderUploadClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429AF2EBA70790010784E /* HotlineFolderUploadClientNew.swift */; };
- DA3429B32EBA7ADF0010784E /* HotlineFilePreviewClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClientNew.swift */; };
+ DA3429AE2EB9C0280010784E /* HotlineFileUploadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429AD2EB9C0220010784E /* HotlineFileUploadClient.swift */; };
+ DA3429B02EBA70790010784E /* HotlineFolderUploadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429AF2EBA70790010784E /* HotlineFolderUploadClient.swift */; };
+ DA3429B32EBA7ADF0010784E /* HotlineFilePreviewClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClient.swift */; };
DA3429B52EBA8A450010784E /* FilePreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429B42EBA8A450010784E /* FilePreviewState.swift */; };
DA3429B72EBAB1750010784E /* QuickLookPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429B62EBAB1750010784E /* QuickLookPreviewView.swift */; platformFilters = (macos, ); };
DA3429B92EBAB2130010784E /* FilePreviewQuickLookView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3429B82EBAB2130010784E /* FilePreviewQuickLookView.swift */; };
@@ -49,11 +49,11 @@
DA5268AD2EB12FE200DCB941 /* ServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268AC2EB12FE200DCB941 /* ServerState.swift */; };
DA5268AF2EB2682B00DCB941 /* HotlineClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268AE2EB2682B00DCB941 /* HotlineClientNew.swift */; };
DA5268B12EB2708E00DCB941 /* HotlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B02EB2708E00DCB941 /* HotlineState.swift */; };
- DA5268B32EB6806E00DCB941 /* HotlineFileDownloadClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClientNew.swift */; };
+ DA5268B32EB6806E00DCB941 /* HotlineFileDownloadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClient.swift */; };
DA5268B52EB6840A00DCB941 /* TransfersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B42EB6840A00DCB941 /* TransfersView.swift */; };
DA5268B82EB916AF00DCB941 /* TransferRateEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B72EB916AF00DCB941 /* TransferRateEstimator.swift */; };
DA5268BA2EB91B5E00DCB941 /* FileProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268B92EB91B5E00DCB941 /* FileProgress.swift */; };
- DA5268BC2EB95A7700DCB941 /* HotlineFolderDownloadClientNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClientNew.swift */; };
+ DA5268BC2EB95A7700DCB941 /* HotlineFolderDownloadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClient.swift */; };
DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */; platformFilters = (macos, ); };
DA55AC752BE4888300034857 /* InstantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC742BE4888300034857 /* InstantMessage.swift */; };
DA55AC772BE589F700034857 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA55AC762BE589F700034857 /* AboutView.swift */; platformFilters = (macos, ); };
@@ -136,9 +136,9 @@
DA32CD4A2B29318E0053B98B /* FileInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileInfo.swift; sourceTree = "<group>"; };
DA32CD4C2B2931B50053B98B /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsInfo.swift; sourceTree = "<group>"; };
- DA3429AD2EB9C0220010784E /* HotlineFileUploadClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileUploadClientNew.swift; sourceTree = "<group>"; };
- DA3429AF2EBA70790010784E /* HotlineFolderUploadClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFolderUploadClientNew.swift; sourceTree = "<group>"; };
- DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFilePreviewClientNew.swift; sourceTree = "<group>"; };
+ DA3429AD2EB9C0220010784E /* HotlineFileUploadClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileUploadClient.swift; sourceTree = "<group>"; };
+ DA3429AF2EBA70790010784E /* HotlineFolderUploadClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFolderUploadClient.swift; sourceTree = "<group>"; };
+ DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFilePreviewClient.swift; sourceTree = "<group>"; };
DA3429B42EBA8A450010784E /* FilePreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewState.swift; sourceTree = "<group>"; };
DA3429B62EBAB1750010784E /* QuickLookPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreviewView.swift; sourceTree = "<group>"; };
DA3429B82EBAB2130010784E /* FilePreviewQuickLookView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewQuickLookView.swift; sourceTree = "<group>"; };
@@ -162,11 +162,11 @@
DA5268AC2EB12FE200DCB941 /* ServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerState.swift; sourceTree = "<group>"; };
DA5268AE2EB2682B00DCB941 /* HotlineClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClientNew.swift; sourceTree = "<group>"; };
DA5268B02EB2708E00DCB941 /* HotlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineState.swift; sourceTree = "<group>"; };
- DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileDownloadClientNew.swift; sourceTree = "<group>"; };
+ DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileDownloadClient.swift; sourceTree = "<group>"; };
DA5268B42EB6840A00DCB941 /* TransfersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransfersView.swift; sourceTree = "<group>"; };
DA5268B72EB916AF00DCB941 /* TransferRateEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferRateEstimator.swift; sourceTree = "<group>"; };
DA5268B92EB91B5E00DCB941 /* FileProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProgress.swift; sourceTree = "<group>"; };
- DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClientNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFolderDownloadClientNew.swift; sourceTree = "<group>"; };
+ DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFolderDownloadClient.swift; sourceTree = "<group>"; };
DA55AC722BE42AF000034857 /* AsyncLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncLinkPreview.swift; sourceTree = "<group>"; };
DA55AC742BE4888300034857 /* InstantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantMessage.swift; sourceTree = "<group>"; };
DA55AC762BE589F700034857 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@@ -248,11 +248,11 @@
isa = PBXGroup;
children = (
DA5753672B33E88A00FAC277 /* HotlineTransferClient.swift */,
- DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClientNew.swift */,
- DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClientNew.swift */,
- DA3429AD2EB9C0220010784E /* HotlineFileUploadClientNew.swift */,
- DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClientNew.swift */,
- DA3429AF2EBA70790010784E /* HotlineFolderUploadClientNew.swift */,
+ DA3429B22EBA7ADE0010784E /* HotlineFilePreviewClient.swift */,
+ DA5268B22EB6806E00DCB941 /* HotlineFileDownloadClient.swift */,
+ DA3429AD2EB9C0220010784E /* HotlineFileUploadClient.swift */,
+ DA5268BB2EB95A7700DCB941 /* HotlineFolderDownloadClient.swift */,
+ DA3429AF2EBA70790010784E /* HotlineFolderUploadClient.swift */,
);
path = Transfers;
sourceTree = "<group>";
@@ -639,9 +639,9 @@
DA52689E2EB073A400DCB941 /* IconSettingsView.swift in Sources */,
DAC3D9832BC33FD000A727C9 /* AppState.swift in Sources */,
DA65499C2BEC3FBD00EDB697 /* ServerAgreementView.swift in Sources */,
- DA5268BC2EB95A7700DCB941 /* HotlineFolderDownloadClientNew.swift in Sources */,
+ DA5268BC2EB95A7700DCB941 /* HotlineFolderDownloadClient.swift in Sources */,
DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */,
- DA3429B02EBA70790010784E /* HotlineFolderUploadClientNew.swift in Sources */,
+ DA3429B02EBA70790010784E /* HotlineFolderUploadClient.swift in Sources */,
DA5268AB2EB11EA300DCB941 /* ColorArt.swift in Sources */,
DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */,
DA5268BA2EB91B5E00DCB941 /* FileProgress.swift in Sources */,
@@ -672,7 +672,7 @@
DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */,
DAF5BC6C2EC2727700551E4D /* ConnectView.swift in Sources */,
DA20BBE12BF5237600B94E7C /* Bookmark.swift in Sources */,
- DA3429B32EBA7ADF0010784E /* HotlineFilePreviewClientNew.swift in Sources */,
+ DA3429B32EBA7ADF0010784E /* HotlineFilePreviewClient.swift in Sources */,
DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */,
DA2863DA2B37BF6E00A7D050 /* Preferences.swift in Sources */,
DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */,
@@ -706,7 +706,7 @@
DA501BF02EBED848001714F8 /* BonjourState.swift in Sources */,
DAAEE66F2B47625600A5BA07 /* FilePreviewImageView.swift in Sources */,
DA872B152BDDEE1A008B1012 /* VisualEffectView.swift in Sources */,
- DA3429AE2EB9C0280010784E /* HotlineFileUploadClientNew.swift in Sources */,
+ DA3429AE2EB9C0280010784E /* HotlineFileUploadClient.swift in Sources */,
DAAEE66D2B475F1400A5BA07 /* PreviewFileInfo.swift in Sources */,
DAB4D8842B4CABEF0048A05C /* Extensions.swift in Sources */,
DA0D698D2B1E7CF700C71DF5 /* UsersView.swift in Sources */,
@@ -723,7 +723,7 @@
DAB4D87E2B4C8BCA0048A05C /* FilePreviewTextView.swift in Sources */,
DA3429B72EBAB1750010784E /* QuickLookPreviewView.swift in Sources */,
DA4F2C012B1A558E00D8ADDC /* ChatView.swift in Sources */,
- DA5268B32EB6806E00DCB941 /* HotlineFileDownloadClientNew.swift in Sources */,
+ DA5268B32EB6806E00DCB941 /* HotlineFileDownloadClient.swift in Sources */,
DACCE5E32EABE86A008CDD92 /* AppUpdateView.swift in Sources */,
DAC6B2E02EAC6236004E2CBA /* ChatStore.swift in Sources */,
DA32CD492B2931640053B98B /* User.swift in Sources */,
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)