aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Models
diff options
context:
space:
mode:
Diffstat (limited to 'Hotline/Models')
-rw-r--r--Hotline/Models/ApplicationState.swift6
-rw-r--r--Hotline/Models/Bookmark.swift284
-rw-r--r--Hotline/Models/ChatMessage.swift17
-rw-r--r--Hotline/Models/FileDetails.swift2
-rw-r--r--Hotline/Models/FileInfo.swift30
-rw-r--r--Hotline/Models/FilePreview.swift51
-rw-r--r--Hotline/Models/Hotline.swift699
-rw-r--r--Hotline/Models/NewsInfo.swift47
-rw-r--r--Hotline/Models/Preferences.swift134
-rw-r--r--Hotline/Models/PreviewFileInfo.swift5
-rw-r--r--Hotline/Models/Server.swift25
-rw-r--r--Hotline/Models/Tracker.swift4
-rw-r--r--Hotline/Models/TransferInfo.swift12
-rw-r--r--Hotline/Models/User.swift8
14 files changed, 719 insertions, 605 deletions
diff --git a/Hotline/Models/ApplicationState.swift b/Hotline/Models/ApplicationState.swift
index 6e077c5..3e90ed9 100644
--- a/Hotline/Models/ApplicationState.swift
+++ b/Hotline/Models/ApplicationState.swift
@@ -3,14 +3,14 @@ import SwiftUI
@Observable
final class ApplicationState {
static let shared = ApplicationState()
-
+
var activeHotline: Hotline? = nil
var activeServerState: ServerState? = nil
-
+
// Frontmost server window information
var activeServerID: UUID? = nil
var activeServerBanner: NSImage? = nil
var activeServerName: String? = nil
-
+
var cloudKitReady: Bool = false
}
diff --git a/Hotline/Models/Bookmark.swift b/Hotline/Models/Bookmark.swift
index b410f75..5afc2c0 100644
--- a/Hotline/Models/Bookmark.swift
+++ b/Hotline/Models/Bookmark.swift
@@ -11,131 +11,140 @@ enum BookmarkType: String, Codable {
final class Bookmark {
var type: BookmarkType = BookmarkType.server
var order: Int = 0
-
+
var name: String = ""
var address: String = ""
var port: Int = HotlinePorts.DefaultServerPort
-
+
@Attribute(.allowsCloudEncryption)
var login: String?
-
+
@Attribute(.allowsCloudEncryption)
var password: String?
-
+
@Attribute
var autoconnect: Bool = false
-
+
@Attribute(.ephemeral)
var expanded: Bool = false
-
+
@Attribute(.ephemeral)
var loading: Bool = false
-
+
@Attribute(.ephemeral)
var serverDescription: String? = nil
-
+
@Attribute(.ephemeral)
var serverUserCount: Int? = nil
-
+
@Transient
var servers: [Bookmark] = []
-
+
func hash(into hasher: inout Hasher) {
-
+
}
-
+
@Transient
var displayAddress: String {
switch self.type {
case .tracker:
if self.port == HotlinePorts.DefaultTrackerPort {
return self.address
- }
- else {
+ } else {
return "\(self.address):\(String(self.port))"
}
-
+
case .server, .temporary:
if self.port == HotlinePorts.DefaultServerPort {
return self.address
- }
- else {
+ } else {
return "\(self.address):\(String(self.port))"
}
}
}
-
+
@Transient
var server: Server? {
switch self.type {
case .tracker:
return nil
-
+
case .server, .temporary:
- return Server(name: self.name, description: self.serverDescription, address: self.address, port: self.port, login: self.login, password: self.password, autoconnect: self.autoconnect)
+ return Server(
+ name: self.name, description: self.serverDescription, address: self.address,
+ port: self.port, login: self.login, password: self.password, autoconnect: self.autoconnect)
}
}
-
+
static let DefaultBookmarks: [Bookmark] = [
- Bookmark(type: .server, name: "The Mobius Strip", address: "67.174.208.111", port: HotlinePorts.DefaultServerPort),
- Bookmark(type: .server, name: "System 7 Today", address: "hotline.system7today.com", port: HotlinePorts.DefaultServerPort),
- Bookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com", port: HotlinePorts.DefaultTrackerPort)
+ Bookmark(
+ type: .server, name: "The Mobius Strip", address: "67.174.208.111",
+ port: HotlinePorts.DefaultServerPort),
+ Bookmark(
+ type: .server, name: "System 7 Today", address: "hotline.system7today.com",
+ port: HotlinePorts.DefaultServerPort),
+ Bookmark(
+ type: .tracker, name: "Featured Servers", address: "hltracker.com",
+ port: HotlinePorts.DefaultTrackerPort),
]
-
- init(type: BookmarkType, name: String, address: String, port: Int, login: String? = nil, password: String? = nil, autoconnect: Bool = false) {
+
+ init(
+ type: BookmarkType, name: String, address: String, port: Int, login: String? = nil,
+ password: String? = nil, autoconnect: Bool = false
+ ) {
self.type = type
self.name = name
self.address = address
self.port = port
-
+
self.login = login
self.password = password
-
+
self.autoconnect = autoconnect
}
-
+
init(temporaryServer server: Server) {
self.type = .temporary
-
+
self.name = server.name ?? server.address
self.address = server.address
self.port = server.port
-
+
self.serverDescription = server.description
self.serverUserCount = server.users
}
-
+
init?(fileData: Data, name: String? = nil) {
guard fileData.count <= 2000 else {
return nil
}
-
+
var fileDataArray: [UInt8] = [UInt8](fileData)
-
+
guard let headerValue = fileDataArray.consumeUInt32(),
- headerValue.fourCharCode() == "HTsc",
- let versionNumber = fileDataArray.consumeUInt16(),
- versionNumber == 1,
- fileDataArray.consume(128), // Skip 128 reserved bytes.
- let loginLength = fileDataArray.consumeUInt16(),
- let loginData: Data = fileDataArray.consumeBytes(32),
- let passwordLength = fileDataArray.consumeUInt16(),
- let passwordData: Data = fileDataArray.consumeBytes(32),
- let addressLength = fileDataArray.consumeUInt16(),
- let addressData: Data = fileDataArray.consumeBytes(256),
- let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii),
- let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii),
- let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else {
+ headerValue.fourCharCode() == "HTsc",
+ let versionNumber = fileDataArray.consumeUInt16(),
+ versionNumber == 1,
+ fileDataArray.consume(128), // Skip 128 reserved bytes.
+ let loginLength = fileDataArray.consumeUInt16(),
+ let loginData: Data = fileDataArray.consumeBytes(32),
+ let passwordLength = fileDataArray.consumeUInt16(),
+ let passwordData: Data = fileDataArray.consumeBytes(32),
+ let addressLength = fileDataArray.consumeUInt16(),
+ let addressData: Data = fileDataArray.consumeBytes(256),
+ let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii),
+ let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii),
+ let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii)
+ else {
return nil
}
-
+
let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString)
-
+
self.type = .server
if let name = name, !name.isEmpty {
self.name = name
- }
- else {
+ } else {
self.name = addressHost
}
self.address = addressHost
@@ -143,156 +152,158 @@ final class Bookmark {
self.login = loginString.isEmpty ? nil : loginString
self.password = passwordString.isEmpty ? nil : passwordString
}
-
+
convenience init?(fileURL bookmarkFileURL: URL) {
guard bookmarkFileURL.isFileURL,
- let fileAttributes = try? FileManager.default.attributesOfItem(atPath: bookmarkFileURL.path(percentEncoded: false)),
- let fileSize = fileAttributes[FileAttributeKey.size] as? UInt64,
- fileSize <= 2000,
- let fileData = try? Data(contentsOf: bookmarkFileURL) else {
+ let fileAttributes = try? FileManager.default.attributesOfItem(
+ atPath: bookmarkFileURL.path(percentEncoded: false)),
+ let fileSize = fileAttributes[FileAttributeKey.size] as? UInt64,
+ fileSize <= 2000,
+ let fileData = try? Data(contentsOf: bookmarkFileURL)
+ else {
return nil
}
-
+
print("Bookmark: Parsing Hotline bookmark file...")
-
- let fileName = bookmarkFileURL.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
-
+
+ let fileName = bookmarkFileURL.deletingPathExtension().lastPathComponent.trimmingCharacters(
+ in: .whitespacesAndNewlines)
+
self.init(fileData: fileData, name: fileName)
-
-// var fileDataArray: [UInt8] = [UInt8](fileData)
-//
-// guard let headerValue = fileDataArray.consumeUInt32(),
-// headerValue.fourCharCode() == "HTsc",
-// let versionNumber = fileDataArray.consumeUInt16(),
-// versionNumber == 1,
-// fileDataArray.consume(128), // Skip 128 reserved bytes.
-// let loginLength = fileDataArray.consumeUInt16(),
-// let loginData: Data = fileDataArray.consumeBytes(32),
-// let passwordLength = fileDataArray.consumeUInt16(),
-// let passwordData: Data = fileDataArray.consumeBytes(32),
-// let addressLength = fileDataArray.consumeUInt16(),
-// let addressData: Data = fileDataArray.consumeBytes(256),
-// let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii),
-// let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii),
-// let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else {
-// return nil
-// }
-//
-// let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString)
-//
-// self.type = .server
-// self.name = fileName.isEmpty ? addressHost : fileName
-// self.address = addressHost
-// self.port = addressPort
-// self.login = loginString.isEmpty ? nil : loginString
-// self.password = passwordString.isEmpty ? nil : passwordString
+
+ // var fileDataArray: [UInt8] = [UInt8](fileData)
+ //
+ // guard let headerValue = fileDataArray.consumeUInt32(),
+ // headerValue.fourCharCode() == "HTsc",
+ // let versionNumber = fileDataArray.consumeUInt16(),
+ // versionNumber == 1,
+ // fileDataArray.consume(128), // Skip 128 reserved bytes.
+ // let loginLength = fileDataArray.consumeUInt16(),
+ // let loginData: Data = fileDataArray.consumeBytes(32),
+ // let passwordLength = fileDataArray.consumeUInt16(),
+ // let passwordData: Data = fileDataArray.consumeBytes(32),
+ // let addressLength = fileDataArray.consumeUInt16(),
+ // let addressData: Data = fileDataArray.consumeBytes(256),
+ // let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii),
+ // let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii),
+ // let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else {
+ // return nil
+ // }
+ //
+ // let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString)
+ //
+ // self.type = .server
+ // self.name = fileName.isEmpty ? addressHost : fileName
+ // self.address = addressHost
+ // self.port = addressPort
+ // self.login = loginString.isEmpty ? nil : loginString
+ // self.password = passwordString.isEmpty ? nil : passwordString
}
-
+
func bookmarkFileData() -> Data? {
guard let addressData = self.displayAddress.data(using: .ascii) else {
return nil
}
-
+
let loginData: Data = self.login?.data(using: .ascii) ?? Data()
let passwordData: Data = self.password?.data(using: .ascii) ?? Data()
-
+
var fileData: Data = Data()
-
- fileData.appendUInt32("HTsc".fourCharCode()) // magic
- fileData.appendUInt16(0x0001) // version
- fileData.append(Data(repeating: 0x00, count: 128)) // reserved
-
+
+ fileData.appendUInt32("HTsc".fourCharCode()) // magic
+ fileData.appendUInt16(0x0001) // version
+ fileData.append(Data(repeating: 0x00, count: 128)) // reserved
+
fileData.appendUInt16(UInt16(loginData.count))
fileData.append(loginData)
if loginData.count < 32 {
// Pad login data to 32 bytes
fileData.append(Data(repeating: 0x00, count: 32 - loginData.count))
}
-
+
fileData.appendUInt16(UInt16(passwordData.count))
fileData.append(passwordData)
if passwordData.count < 32 {
// Pad password data to 32 bytes
fileData.append(Data(repeating: 0x00, count: 32 - passwordData.count))
}
-
+
fileData.appendUInt16(UInt16(addressData.count))
fileData.append(addressData)
if passwordData.count < 256 {
// Pad address data to 256 bytes
fileData.append(Data(repeating: 0x00, count: 256 - addressData.count))
}
-
+
return fileData
}
-
+
static func populateDefaults(force: Bool = false, context: ModelContext) {
if force || Bookmark.fetchCount(context: context) == 0 {
Bookmark.add(Bookmark.DefaultBookmarks, context: context)
}
}
-
+
static func fetchAll(context: ModelContext) -> [Bookmark] {
let fetchDescriptor = FetchDescriptor<Bookmark>(sortBy: [.init(\.order)])
do {
let bookmarks: [Bookmark] = try context.fetch(fetchDescriptor)
return bookmarks
- }
- catch {
+ } catch {
return []
}
}
-
+
static func fetchCount(context: ModelContext) -> Int {
let descriptor = FetchDescriptor<Bookmark>()
return (try? context.fetchCount(descriptor)) ?? 0
}
-
+
static func deleteAll(context: ModelContext) {
try? context.delete(model: Bookmark.self)
}
-
+
static func add(_ bookmark: Bookmark, context: ModelContext) {
guard bookmark.type != .temporary else {
print("Bookmark: Attempting to add temporary bookmark to store. Aborting.")
return
}
-
+
let existingBookmarks = Bookmark.fetchAll(context: context)
-
+
// Reindex bookmarks before insert.
for existingBookmark in existingBookmarks {
existingBookmark.order += 1
}
-
+
// Insert new bookmark at start.
bookmark.order = 0
context.insert(bookmark)
}
-
+
static func add(_ bookmarks: [Bookmark], context: ModelContext) {
let existingBookmarks = Bookmark.fetchAll(context: context)
-
+
// Reindex bookmarks before insert.
for existingBookmark in existingBookmarks {
existingBookmark.order += bookmarks.count
}
-
+
// Insert new bookmarks at start.
var bookmarkIndex = 0
for newBookmark in bookmarks {
newBookmark.order = bookmarkIndex
context.insert(newBookmark)
bookmarkIndex += 1
-
+
print("Bookmark: added \(newBookmark.name)")
}
}
-
+
static func delete(_ bookmark: Bookmark, context: ModelContext) {
// Delete bookmark
context.delete(bookmark)
-
+
// Reindex bookmarks
let existingBookmarks = Bookmark.fetchAll(context: context)
var index = 0
@@ -301,17 +312,17 @@ final class Bookmark {
index += 1
}
}
-
+
static func delete(at indexes: IndexSet, context: ModelContext) {
var existingBookmarks = Bookmark.fetchAll(context: context)
-// existingBookmarks.remove(atOffsets: indexes)
+ // existingBookmarks.remove(atOffsets: indexes)
let bookmarksToDelete = indexes.map { existingBookmarks[$0] }
-
+
// Delete bookmark
for bookmark in bookmarksToDelete {
context.delete(bookmark)
}
-
+
// Reindex bookmarks
var index = 0
existingBookmarks.remove(atOffsets: indexes)
@@ -319,62 +330,65 @@ final class Bookmark {
existingBookmark.order = index
index += 1
}
-
+
do {
try context.save()
- }
- catch {
+ } catch {
print("Bookmark: Failed to save bookmark deletions")
}
}
-
+
static func move(_ indexes: IndexSet, to newIndex: Int, context: ModelContext) {
guard Bookmark.fetchCount(context: context) >= indexes.count else {
print("Bookmark: Not enough bookmarks to move requested set")
return
}
-
+
// Perform move
var existingBookmarks = Bookmark.fetchAll(context: context)
existingBookmarks.move(fromOffsets: indexes, toOffset: newIndex)
-
+
// Reindex bookmarks
var index = 0
for existingBookmark in existingBookmarks {
existingBookmark.order = index
index += 1
}
-
+
do {
try context.save()
- }
- catch {
+ } catch {
print("Bookmark: Failed to save bookmark reordering")
}
}
-
+
func fetchServers() async {
guard self.type == .tracker else {
// self.loading = false
return
}
-
+
DispatchQueue.main.sync {
self.loading = true
}
-
+
var fetchedBookmarks: [Bookmark] = []
-
+
let client = HotlineTrackerClient()
- if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: self.address, port: self.port) {
+ if let fetchedServers: [HotlineServer] = try? await client.fetchServers(
+ address: self.address, port: self.port)
+ {
for fetchedServer in fetchedServers {
if let serverName = fetchedServer.name {
- let server = Server(name: serverName, description: fetchedServer.description, address: fetchedServer.address, port: Int(fetchedServer.port), users: Int(fetchedServer.users))
+ let server = Server(
+ name: serverName, description: fetchedServer.description,
+ address: fetchedServer.address, port: Int(fetchedServer.port),
+ users: Int(fetchedServer.users))
fetchedBookmarks.append(Bookmark(temporaryServer: server))
}
}
}
-
+
let newServers = fetchedBookmarks
DispatchQueue.main.sync {
self.servers = newServers
diff --git a/Hotline/Models/ChatMessage.swift b/Hotline/Models/ChatMessage.swift
index e585cdf..36648d1 100644
--- a/Hotline/Models/ChatMessage.swift
+++ b/Hotline/Models/ChatMessage.swift
@@ -9,25 +9,24 @@ enum ChatMessageType {
struct ChatMessage: Identifiable {
let id = UUID()
-
+
let text: String
let type: ChatMessageType
let date: Date
let username: String?
-
+
static let parser = /^\s*([^\:]+):\s*([\s\S]+)$/
-
+
init(text: String, type: ChatMessageType, date: Date) {
self.type = type
self.date = date
-
- if
- type == .message,
- let match = text.firstMatch(of: ChatMessage.parser) {
+
+ if type == .message,
+ let match = text.firstMatch(of: ChatMessage.parser)
+ {
self.username = String(match.1)
self.text = String(match.2)
- }
- else {
+ } else {
self.username = nil
self.text = text
}
diff --git a/Hotline/Models/FileDetails.swift b/Hotline/Models/FileDetails.swift
index 20b1ee2..dc302e8 100644
--- a/Hotline/Models/FileDetails.swift
+++ b/Hotline/Models/FileDetails.swift
@@ -1,6 +1,6 @@
import UniformTypeIdentifiers
-struct FileDetails:Identifiable {
+struct FileDetails: Identifiable {
let id = UUID()
var name: String
var path: [String]
diff --git a/Hotline/Models/FileInfo.swift b/Hotline/Models/FileInfo.swift
index 2da7378..0eadd10 100644
--- a/Hotline/Models/FileInfo.swift
+++ b/Hotline/Models/FileInfo.swift
@@ -3,46 +3,46 @@ import UniformTypeIdentifiers
@Observable class FileInfo: Identifiable, Hashable {
let id: UUID
-
+
let path: [String]
let name: String
-
+
let type: String
let creator: String
let fileSize: UInt
-
+
let isFolder: Bool
let isUnavailable: Bool
-
+
var isDropboxFolder: Bool {
guard self.isFolder,
- (self.name.range(of: "upload", options: [.caseInsensitive]) != nil) || (self.name.range(of: "drop box", options: [.caseInsensitive]) != nil)
+ (self.name.range(of: "upload", options: [.caseInsensitive]) != nil)
+ || (self.name.range(of: "drop box", options: [.caseInsensitive]) != nil)
else {
return false
}
return true
}
-
+
var isAdminDropboxFolder: Bool {
self.isDropboxFolder && (self.name.range(of: "admin", options: [.caseInsensitive]) != nil)
}
-
+
var expanded: Bool = false
var children: [FileInfo]? = nil
-
+
var isPreviewable: Bool {
let fileExtension = (self.name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
if fileType.isSubtype(of: .image) {
return true
- }
- else if fileType.isSubtype(of: .text) {
+ } else if fileType.isSubtype(of: .text) {
return true
}
}
return false
}
-
+
var isImage: Bool {
let fileExtension = (self.name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
@@ -52,7 +52,7 @@ import UniformTypeIdentifiers
}
return false
}
-
+
init(hotlineFile: HotlineFile) {
self.id = UUID()
self.path = hotlineFile.path
@@ -62,17 +62,17 @@ import UniformTypeIdentifiers
self.fileSize = UInt(hotlineFile.fileSize)
self.isFolder = hotlineFile.isFolder
self.isUnavailable = (!self.isFolder && (self.fileSize == 0))
-
+
print(self.name, self.type, self.creator, self.isUnavailable)
if self.isFolder {
self.children = []
}
}
-
+
static func == (lhs: FileInfo, rhs: FileInfo) -> Bool {
return lhs.id == rhs.id
}
-
+
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
diff --git a/Hotline/Models/FilePreview.swift b/Hotline/Models/FilePreview.swift
index e5f49a3..bba29fa 100644
--- a/Hotline/Models/FilePreview.swift
+++ b/Hotline/Models/FilePreview.swift
@@ -18,52 +18,55 @@ enum FilePreviewType: Equatable {
final class FilePreview: HotlineFilePreviewClientDelegate {
@ObservationIgnored let info: PreviewFileInfo
@ObservationIgnored var client: HotlineFilePreviewClient? = nil
-
+
var state: FilePreviewState = .unloaded
var progress: Double = 0.0
-
+
var data: Data? = nil
-
+
#if os(iOS)
- var image: UIImage? = nil
+ var image: UIImage? = nil
#elseif os(macOS)
- var image: NSImage? = nil
+ var image: NSImage? = nil
#endif
-
+
var text: String? = nil
var styledText: NSAttributedString? = nil
-
+
var previewType: FilePreviewType {
let fileExtension = (info.name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
if fileType.isSubtype(of: .image) {
return .image
- }
- else if fileType.isSubtype(of: .text) {
+ } else if fileType.isSubtype(of: .text) {
return .text
}
}
return .unknown
}
-
+
init(info: PreviewFileInfo) {
self.info = info
-
- self.client = HotlineFilePreviewClient(address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size))
+
+ self.client = HotlineFilePreviewClient(
+ address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size))
self.client?.delegate = self
}
-
+
func download() {
self.client?.start()
}
-
+
func cancel() {
self.client?.cancel()
}
-
- func hotlineTransferStatusChanged(client: any HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) {
+
+ func hotlineTransferStatusChanged(
+ client: any HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus,
+ timeRemaining: TimeInterval
+ ) {
print("FilePreview: Download status changed:", status)
-
+
switch status {
case .unconnected:
state = .unloaded
@@ -88,24 +91,24 @@ final class FilePreview: HotlineFilePreviewClientDelegate {
progress = 1.0
}
}
-
+
func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) {
self.state = .loaded
self.data = data
-
+
switch self.previewType {
case .image:
#if os(iOS)
- self.image = UIImage(data: data)
+ self.image = UIImage(data: data)
#elseif os(macOS)
- self.image = NSImage(data: data)
+ self.image = NSImage(data: data)
#endif
case .text:
- let encoding: UInt = NSString.stringEncoding(for: data, convertedString: nil, usedLossyConversion: nil)
+ 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 {
+ } else {
self.text = String(data: data, encoding: .utf8)
}
case .unknown:
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 9ea29c5..1989d46 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -1,25 +1,27 @@
import SwiftUI
@Observable
-class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelegate, HotlineFilePreviewClientDelegate, HotlineFileUploadClientDelegate {
+class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelegate,
+ HotlineFilePreviewClientDelegate, HotlineFileUploadClientDelegate
+{
let id: UUID = UUID()
let trackerClient: HotlineTrackerClient
let client: HotlineClient
-
+
static func == (lhs: Hotline, rhs: Hotline) -> Bool {
return lhs.id == rhs.id
}
-
+
#if os(macOS)
- static func getClassicIcon(_ index: Int) -> NSImage? {
- return NSImage(named: "Classic/\(index)")
- }
+ static func getClassicIcon(_ index: Int) -> NSImage? {
+ return NSImage(named: "Classic/\(index)")
+ }
#elseif os(iOS)
- static func getClassicIcon(_ index: Int) -> UIImage? {
- return UIImage(named: "Classic/\(index)")
- }
+ static func getClassicIcon(_ index: Int) -> UIImage? {
+ return UIImage(named: "Classic/\(index)")
+ }
#endif
-
+
// The icon ordering here was painsakenly pulled manually
// from the original Hotline client to display the classic icons
// in the same order as the original client.
@@ -104,11 +106,11 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
6001, 6002, 6003, 6004, 6005, 6008, 6009,
6010, 6011, 6012, 6013, 6014, 6015, 6016,
6017, 6018, 6023, 6025, 6026, 6027, 6028,
- 6029, 6030, 6031, 6032, 6033, 6034, 6035
+ 6029, 6030, 6031, 6032, 6033, 6034, 6035,
]
-
+
var status: HotlineClientStatus = .disconnected
- var server: Server? {
+ var server: Server? {
didSet {
self.updateServerTitle()
}
@@ -132,258 +134,271 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
var files: [FileInfo] = []
var filesLoaded: Bool = false
var news: [NewsInfo] = []
- private var newsLookup: [String:NewsInfo] = [:]
+ private var newsLookup: [String: NewsInfo] = [:]
var newsLoaded: Bool = false
var accountsLoaded: Bool = false
- var instantMessages: [UInt16:[InstantMessage]] = [:]
+ var instantMessages: [UInt16: [InstantMessage]] = [:]
var transfers: [TransferInfo] = []
var downloads: [HotlineTransferClient] = []
- var unreadInstantMessages: [UInt16:UInt16] = [:]
+ var unreadInstantMessages: [UInt16: UInt16] = [:]
var unreadPublicChat: Bool = false
var errorDisplayed: Bool = false
var errorMessage: String? = nil
-
+
@ObservationIgnored var bannerClient: HotlineFilePreviewClient?
#if os(macOS)
- var bannerImage: NSImage? = nil
+ var bannerImage: NSImage? = nil
#elseif os(iOS)
- var bannerImage: UIImage? = nil
+ var bannerImage: UIImage? = nil
#endif
-
-
+
// MARK: -
-
+
init(trackerClient: HotlineTrackerClient, client: HotlineClient) {
self.trackerClient = trackerClient
self.client = client
self.client.delegate = self
}
-
+
// MARK: -
-
- @MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async -> [Server] {
+
+ @MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async
+ -> [Server]
+ {
var servers: [Server] = []
-
- if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers(address: tracker, port: port) {
+
+ if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers(
+ address: tracker, port: port)
+ {
for s in fetchedServers {
if let serverName = s.name {
- servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)))
+ servers.append(
+ Server(
+ name: serverName, description: s.description, address: s.address, port: Int(s.port),
+ users: Int(s.users)))
}
}
}
-
+
return servers
}
-
+
@MainActor func disconnectTracker() {
self.trackerClient.close()
}
-
- @MainActor func login(server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil) {
+
+ @MainActor func login(
+ server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil
+ ) {
self.server = server
self.serverName = server.name
self.username = username
self.iconID = iconID
-
- self.client.login(address: server.address, port: server.port, login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in
+
+ self.client.login(
+ address: server.address, port: server.port, login: server.login, password: server.password,
+ username: username, iconID: UInt16(iconID)
+ ) { [weak self] err, serverName, serverVersion in
self?.serverVersion = serverVersion ?? 123
if serverName != nil {
self?.serverName = serverName
}
-
+
callback?(err == nil)
}
}
-
- @MainActor func sendUserInfo(username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil) {
+
+ @MainActor func sendUserInfo(
+ username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil
+ ) {
self.username = username
self.iconID = iconID
-
- self.client.sendSetClientUserInfo(username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse)
+
+ self.client.sendSetClientUserInfo(
+ username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse)
}
-
+
@MainActor func getUserList() {
self.client.sendGetUserList()
}
-
+
@MainActor func disconnect() {
self.client.disconnect()
self.bannerClient?.cancel()
}
-
+
@MainActor func sendAgree() {
self.client.sendAgree(username: self.username, iconID: UInt16(self.iconID), options: .none)
}
-
+
@MainActor func sendInstantMessage(_ text: String, userID: UInt16) {
- let message = InstantMessage(direction: .outgoing, text: text.convertingLinksToMarkdown(), type: .message, date: Date())
-
+ let message = InstantMessage(
+ direction: .outgoing, text: text.convertingLinksToMarkdown(), type: .message, date: Date())
+
if self.instantMessages[userID] == nil {
self.instantMessages[userID] = [message]
- }
- else {
+ } else {
self.instantMessages[userID]!.append(message)
}
-
+
self.client.sendInstantMessage(message: text, userID: userID)
-
+
if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
}
}
-
+
func markPublicChatAsRead() {
self.unreadPublicChat = false
}
-
+
func hasUnreadInstantMessages(userID: UInt16) -> Bool {
return self.unreadInstantMessages[userID] != nil
}
-
+
func markInstantMessagesAsRead(userID: UInt16) {
self.unreadInstantMessages.removeValue(forKey: userID)
}
-
+
@MainActor func sendChat(_ text: String, announce: Bool = false) {
self.client.sendChat(message: text, announce: announce)
}
-
+
@MainActor func getMessageBoard() async -> [String] {
self.messageBoard = await withCheckedContinuation { [weak self] continuation in
- self?.client.sendGetMessageBoard() { err, messages in
+ self?.client.sendGetMessageBoard { err, messages in
continuation.resume(returning: (err != nil ? [] : messages))
}
}
-
+
self.messageBoardLoaded = true
-
+
return self.messageBoard
}
-
+
@MainActor func postToMessageBoard(text: String) {
self.client.sendPostMessageBoard(text: text)
}
-
+
@MainActor func getFileList(path: [String] = []) async -> [FileInfo] {
return await withCheckedContinuation { [weak self] continuation in
self?.client.sendGetFileList(path: path) { [weak self] files in
let parentFile = self?.findFile(in: self?.files ?? [], at: path)
-
+
var newFiles: [FileInfo] = []
for f in files {
newFiles.append(FileInfo(hotlineFile: f))
}
-
+
DispatchQueue.main.async {
if let parent = parentFile {
parent.children = newFiles
- }
- else if path.isEmpty {
+ } else if path.isEmpty {
self?.filesLoaded = true
self?.files = newFiles
}
-
+
continuation.resume(returning: newFiles)
}
}
}
}
-
- @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async -> String? {
+
+ @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async
+ -> String?
+ {
return await withCheckedContinuation { [weak self] continuation in
- self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor) { articleText in
-// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
-
-// var newCategories: [NewsInfo] = []
-// for category in categories {
-// newCategories.append(NewsInfo(hotlineNewsCategory: category))
-// }
-//
-// if let parent = existingNewsItem {
-// parent.children = newCategories
-// }
-// else if path.isEmpty {
-// self?.news = newCategories
-// }
-
+ self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor) {
+ articleText in
+ // let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+
+ // var newCategories: [NewsInfo] = []
+ // for category in categories {
+ // newCategories.append(NewsInfo(hotlineNewsCategory: category))
+ // }
+ //
+ // if let parent = existingNewsItem {
+ // parent.children = newCategories
+ // }
+ // else if path.isEmpty {
+ // self?.news = newCategories
+ // }
+
continuation.resume(returning: articleText)
}
}
-
+
}
-
+
@MainActor func getAccounts() async -> [HotlineAccount] {
return await withCheckedContinuation { [weak self] continuation in
- self?.client.sendGetAccounts() { articles in
+ self?.client.sendGetAccounts { articles in
continuation.resume(returning: articles)
}
}
}
-
+
@MainActor func getNewsList(at path: [String] = []) async {
return await withCheckedContinuation { [weak self] continuation in
let parentNewsGroup = self?.findNews(in: self?.news ?? [], at: path)
-
+
// Send a categories request for bundle paths or root (empty path)
if path.isEmpty || parentNewsGroup?.type == .bundle {
print("Hotline: Requesting categories at: /\(path.joined(separator: "/"))")
-
+
self?.client.sendGetNewsCategories(path: path) { @MainActor [weak self] categories in
// Create info for each category returned.
var newCategoryInfos: [NewsInfo] = []
-
+
// Transform hotline categories into NewsInfo objects.
for category in categories {
var newsCategoryInfo = NewsInfo(hotlineNewsCategory: category)
-
+
if let lookupPath = newsCategoryInfo.lookupPath {
// Merge returned category info with existing category info.
if let existingCategoryInfo = self?.newsLookup[lookupPath] {
print("Hotline: Merging category into existing category at \(lookupPath)")
-
+
existingCategoryInfo.count = newsCategoryInfo.count
existingCategoryInfo.name = newsCategoryInfo.name
existingCategoryInfo.path = newsCategoryInfo.path
existingCategoryInfo.categoryID = newsCategoryInfo.categoryID
newsCategoryInfo = existingCategoryInfo
- }
- else {
+ } else {
print("Hotline: New category added at \(lookupPath)")
self?.newsLookup[lookupPath] = newsCategoryInfo
}
}
-
+
newCategoryInfos.append(newsCategoryInfo)
}
-
+
if let parent = parentNewsGroup {
parent.children = newCategoryInfos
- }
- else if path.isEmpty {
+ } else if path.isEmpty {
self?.newsLoaded = true
self?.news = newCategoryInfos
}
-
+
continuation.resume()
}
- }
- else {
+ } else {
print("Hotline: Requesting articles at: /\(path.joined(separator: "/"))")
-
+
self?.client.sendGetNewsArticles(path: path) { @MainActor [weak self] articles in
print("Hotline: Organizing news at \(path.joined(separator: "/"))")
// Create info for each article returned.
var newArticleInfos: [NewsInfo] = []
-
+
for article in articles {
var newsArticleInfo = NewsInfo(hotlineNewsArticle: article)
-
+
if let lookupPath = newsArticleInfo.lookupPath {
// Merge returned category info with existing category info.
if let existingArticleInfo = self?.newsLookup[lookupPath] {
print("Hotline: Merging article into existing article at \(lookupPath)")
-
+
existingArticleInfo.count = newsArticleInfo.count
existingArticleInfo.name = newsArticleInfo.name
existingArticleInfo.path = newsArticleInfo.path
@@ -392,94 +407,96 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
existingArticleInfo.articleFlavors = newsArticleInfo.articleFlavors
existingArticleInfo.articleID = newsArticleInfo.articleID
newsArticleInfo = existingArticleInfo
- }
- else {
+ } else {
print("Hotline: New article added at \(lookupPath)")
self?.newsLookup[lookupPath] = newsArticleInfo
}
}
-
+
newArticleInfos.append(newsArticleInfo)
}
-
+
let organizedNewsArticles: [NewsInfo] = self?.organizeNewsArticles(newArticleInfos) ?? []
if let parent = parentNewsGroup {
parent.children = organizedNewsArticles
}
-
+
continuation.resume()
}
}
}
}
-
+
func organizeNewsArticles(_ flatArticles: [NewsInfo]) -> [NewsInfo] {
// Place articles under their parent.
var organized: [NewsInfo] = []
for article in flatArticles {
if let parentLookupPath = article.parentArticleLookupPath,
- let parentArticle = self.newsLookup[parentLookupPath] {
-// article.expanded = true
+ let parentArticle = self.newsLookup[parentLookupPath]
+ {
+ // article.expanded = true
if parentArticle.children.firstIndex(of: article) == nil {
article.expanded = true
parentArticle.children.append(article)
}
- }
- else {
+ } else {
organized.append(article)
}
}
-
+
return organized
}
-
- @MainActor func postNewsArticle(title: String, body: String, at path: [String], parentID: UInt32 = 0) async -> Bool {
-
-
+
+ @MainActor func postNewsArticle(
+ title: String, body: String, at path: [String], parentID: UInt32 = 0
+ ) async -> Bool {
+
return await withCheckedContinuation { [weak self] continuation in
guard let client = self?.client else {
continuation.resume(returning: false)
return
}
-
- client.postNewsArticle(title: title, text: body, path: path, parentID: parentID, callback: { success in
- print("Hotline: News article posted? \(success)")
- continuation.resume(returning: success)
- })
+
+ client.postNewsArticle(
+ title: title, text: body, path: path, parentID: parentID,
+ callback: { success in
+ print("Hotline: News article posted? \(success)")
+ continuation.resume(returning: success)
+ })
}
}
-
-// @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] {
-// return await withCheckedContinuation { [weak self] continuation in
-// guard let client = self?.client else {
-// continuation.resume(returning: [])
-// return
-// }
-//
-// client.sendGetNewsCategories(path: path) { [weak self] categories in
-// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
-//
-// var newCategories: [NewsInfo] = []
-// for category in categories {
-// let categoryInfo: NewsInfo = NewsInfo(hotlineNewsCategory: category)
-// newCategories.append(categoryInfo)
-// self?.newsLookup[categoryInfo.path.joined(separator: "/")] = categoryInfo
-// }
-//
-// DispatchQueue.main.async {
-// if let parent = parentNews {
-// parent.children = newCategories
-// }
-// else if path.isEmpty {
-// self?.news = newCategories
-// }
-//
-// continuation.resume(returning: newCategories)
-// }
-// }
-// }
-// }
-
+
+ // @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] {
+ // return await withCheckedContinuation { [weak self] continuation in
+ // guard let client = self?.client else {
+ // continuation.resume(returning: [])
+ // return
+ // }
+ //
+ // client.sendGetNewsCategories(path: path) { [weak self] categories in
+ // let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+ //
+ // var newCategories: [NewsInfo] = []
+ // for category in categories {
+ // let categoryInfo: NewsInfo = NewsInfo(hotlineNewsCategory: category)
+ // newCategories.append(categoryInfo)
+ // self?.newsLookup[categoryInfo.path.joined(separator: "/")] = categoryInfo
+ // }
+ //
+ // DispatchQueue.main.async {
+ // if let parent = parentNews {
+ // parent.children = newCategories
+ // }
+ // else if path.isEmpty {
+ // self?.news = newCategories
+ // }
+ //
+ // continuation.resume(returning: newCategories)
+ // }
+ // }
+ // }
+ // }
+
@MainActor func getArticles(at path: [String]) async -> [NewsInfo] {
return await withCheckedContinuation { [weak self] continuation in
self?.client.sendGetNewsArticles(path: path) { articles in
@@ -487,284 +504,315 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
}
}
}
-
- @MainActor func downloadFile(_ fileName: String, path: [String], complete callback: ((TransferInfo, URL) -> Void)? = nil) {
+
+ @MainActor func downloadFile(
+ _ fileName: String, path: [String], complete callback: ((TransferInfo, URL) -> Void)? = nil
+ ) {
var fullPath: [String] = []
if path.count > 1 {
- fullPath = Array(path[0..<path.count-1])
+ fullPath = Array(path[0..<path.count - 1])
}
-
- self.client.sendDownloadFile(name: fileName, path: fullPath) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in
+
+ self.client.sendDownloadFile(name: fileName, path: fullPath) {
+ [weak self]
+ success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount
+ in
print("GOT DOWNLOAD REPLY:")
print("\tSUCCESS?", success)
print("\tTRANSFER SIZE: \(downloadTransferSize.debugDescription)")
print("\tFILE SIZE: \(downloadFileSize.debugDescription)")
print("\tREFERENCE NUM: \(downloadReferenceNumber.debugDescription)")
print("\tWAITING COUNT: \(downloadWaitingCount.debugDescription)")
-
- if
- let self = self,
-// let server = self.server,
+
+ if let self = self,
+ // let server = self.server,
let address = self.server?.address,
let port = self.server?.port,
let referenceNumber = downloadReferenceNumber,
- let transferSize = downloadTransferSize {
-
- let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
-// let previewClient = HotlineFilePreviewClient(server: self.server, reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
-// let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
+ let transferSize = downloadTransferSize
+ {
+
+ let fileClient = HotlineFileDownloadClient(
+ address: address, port: UInt16(port), reference: referenceNumber,
+ size: UInt32(transferSize))
+ // let previewClient = HotlineFilePreviewClient(server: self.server, reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
+ // let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload)
fileClient.delegate = self
self.downloads.append(fileClient)
-
+
let transfer = TransferInfo(id: referenceNumber, title: fileName, size: UInt(transferSize))
transfer.downloadCallback = callback
self.transfers.append(transfer)
-
+
fileClient.start()
}
}
}
-
- @MainActor func downloadFileTo(url fileURL: URL, fileName: String, path: [String], progress progressCallback: ((TransferInfo, Double) -> Void)? = nil, complete callback: ((TransferInfo, URL) -> Void)? = nil) {
+
+ @MainActor func downloadFileTo(
+ url fileURL: URL, fileName: String, path: [String],
+ progress progressCallback: ((TransferInfo, Double) -> Void)? = nil,
+ complete callback: ((TransferInfo, URL) -> Void)? = nil
+ ) {
var fullPath: [String] = []
if path.count > 1 {
- fullPath = Array(path[0..<path.count-1])
+ fullPath = Array(path[0..<path.count - 1])
}
-
- self.client.sendDownloadFile(name: fileName, path: fullPath) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in
+
+ self.client.sendDownloadFile(name: fileName, path: fullPath) {
+ [weak self]
+ success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount
+ in
print("GOT DOWNLOAD REPLY:")
print("\tSUCCESS?", success)
print("\tTRANSFER SIZE: \(downloadTransferSize.debugDescription)")
print("\tFILE SIZE: \(downloadFileSize.debugDescription)")
print("\tREFERENCE NUM: \(downloadReferenceNumber.debugDescription)")
print("\tWAITING COUNT: \(downloadWaitingCount.debugDescription)")
-
- if
- let self = self,
+
+ if let self = self,
let address = self.server?.address,
let port = self.server?.port,
let referenceNumber = downloadReferenceNumber,
- let transferSize = downloadTransferSize {
-
- let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
+ let transferSize = downloadTransferSize
+ {
+
+ let fileClient = HotlineFileDownloadClient(
+ address: address, port: UInt16(port), reference: referenceNumber,
+ size: UInt32(transferSize))
fileClient.delegate = self
self.downloads.append(fileClient)
-
+
let transfer = TransferInfo(id: referenceNumber, title: fileName, size: UInt(transferSize))
transfer.downloadCallback = callback
transfer.progressCallback = progressCallback
self.transfers.append(transfer)
-
+
fileClient.start(to: fileURL)
}
}
}
-
- @MainActor func uploadFile(url fileURL: URL, path: [String], complete callback: ((TransferInfo) -> Void)? = nil) {
+
+ @MainActor func uploadFile(
+ url fileURL: URL, path: [String], complete callback: ((TransferInfo) -> Void)? = nil
+ ) {
let fileName = fileURL.lastPathComponent
-
+
guard fileURL.isFileURL, !fileName.isEmpty else {
print("NOT A FILE URL?")
return
}
-
+
let filePath = fileURL.path(percentEncoded: false)
-
+
var fileIsDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: filePath, isDirectory: &fileIsDirectory),
- fileIsDirectory.boolValue == false else {
+ fileIsDirectory.boolValue == false
+ else {
print("FILE IS A DIRECTORY?")
return
}
-
+
var fileSize: UInt = 0
-
+
// Data size
let fileAttributes = try? FileManager.default.attributesOfItem(atPath: filePath)
if let sizeAttribute = fileAttributes?[.size] as? NSNumber {
print("DATA SIZE \(sizeAttribute.uintValue)")
fileSize += sizeAttribute.uintValue
}
-
+
// Resource size
let resourceURL = fileURL.appendingPathComponent("..namedfork/rsrc")
-
-
+
print("RESOURCE PATH \(resourceURL)")
- let resourceAttributes = try? FileManager.default.attributesOfItem(atPath: resourceURL.path(percentEncoded: false))
+ let resourceAttributes = try? FileManager.default.attributesOfItem(
+ atPath: resourceURL.path(percentEncoded: false))
if let sizeAttribute = resourceAttributes?[.size] as? NSNumber {
print("RESOURCE SIZE \(sizeAttribute.uintValue)")
fileSize += sizeAttribute.uintValue
}
-
+
print("FILE SIZE? \(fileSize)")
-
+
guard fileSize > 0 else {
print("FILE IS EMPTY??")
return
}
-
+
print("FILE SIZE: \(fileSize) NAME: \(fileName) PATH: \(path)")
-
- self.client.sendUploadFile(name: fileName, path: path) { [weak self] success, uploadReferenceNumber in
+
+ self.client.sendUploadFile(name: fileName, path: path) {
+ [weak self] success, uploadReferenceNumber in
print("UPLOAD REFERENCE: \(String(describing: uploadReferenceNumber))")
-
+
if let self = self,
- let address = self.server?.address,
- let port = self.server?.port,
- let referenceNumber = uploadReferenceNumber,
- let fileClient = HotlineFileUploadClient(upload: fileURL, address: address, port: UInt16(port), reference: referenceNumber) {
-
+ let address = self.server?.address,
+ let port = self.server?.port,
+ let referenceNumber = uploadReferenceNumber,
+ let fileClient = HotlineFileUploadClient(
+ upload: fileURL, address: address, port: UInt16(port), reference: referenceNumber)
+ {
+
print("GOING TO UPLOAD")
-
+
fileClient.delegate = self
self.downloads.append(fileClient)
-
+
let transfer = TransferInfo(id: referenceNumber, title: fileName, size: fileSize)
transfer.uploadCallback = callback
self.transfers.append(transfer)
-
+
fileClient.start()
}
}
-
+
}
-
+
@MainActor func getFileDetails(_ fileName: String, path: [String]) async -> FileDetails? {
var fullPath: [String] = []
if path.count > 1 {
- fullPath = Array(path[0..<path.count-1])
+ fullPath = Array(path[0..<path.count - 1])
}
-
+
return await withCheckedContinuation { [weak self] continuation in
self?.client.sendGetFileInfo(name: fileName, path: fullPath) { info in
continuation.resume(returning: info)
}
}
}
-
+
@MainActor func deleteFile(_ fileName: String, path: [String]) async -> Bool {
var fullPath: [String] = []
if path.count > 1 {
- fullPath = Array(path[0..<path.count-1])
+ fullPath = Array(path[0..<path.count - 1])
}
-
+
return await withCheckedContinuation { [weak self] continuation in
self?.client.sendDeleteFile(name: fileName, path: fullPath) { success in
continuation.resume(returning: success)
}
}
}
-
- @MainActor func previewFile(_ fileName: String, path: [String], complete callback: ((PreviewFileInfo?) -> Void)? = nil) {
+
+ @MainActor func previewFile(
+ _ fileName: String, path: [String], complete callback: ((PreviewFileInfo?) -> Void)? = nil
+ ) {
var fullPath: [String] = []
if path.count > 1 {
- fullPath = Array(path[0..<path.count-1])
+ fullPath = Array(path[0..<path.count - 1])
}
-
- self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in
+
+ self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true) {
+ [weak self]
+ success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount
+ in
guard success else {
callback?(nil)
return
}
-
+
print("GOT DOWNLOAD REPLY:")
print("SUCCESS?", success)
print("TRANSFER SIZE: \(downloadTransferSize.debugDescription)")
print("FILE SIZE: \(downloadFileSize.debugDescription)")
print("REFERENCE NUM: \(downloadReferenceNumber.debugDescription)")
print("WAITING COUNT: \(downloadWaitingCount.debugDescription)")
-
+
var info: PreviewFileInfo? = nil
-
- if
- let address = self?.server?.address,
+
+ if let address = self?.server?.address,
let port = self?.server?.port,
let referenceNumber = downloadReferenceNumber,
- let transferSize = downloadTransferSize {
-
- info = PreviewFileInfo(id: referenceNumber, address: address, port: port, size: transferSize, name: fileName)
+ let transferSize = downloadTransferSize
+ {
+
+ info = PreviewFileInfo(
+ id: referenceNumber, address: address, port: port, size: transferSize, name: fileName)
}
-
+
callback?(info)
}
}
-
+
@MainActor func deleteTransfer(id: UInt32) {
if let b = self.bannerClient, b.referenceNumber == id {
b.cancel()
self.bannerClient = nil
return
}
-
+
if let i = self.transfers.firstIndex(where: { $0.id == id }) {
self.transfers.remove(at: i)
}
-
+
if let i = self.downloads.firstIndex(where: { $0.referenceNumber == id }) {
let fileClient = self.downloads.remove(at: i)
fileClient.cancel()
}
}
-
+
@MainActor func deleteAllTransfers() {
self.transfers = []
-
+
let downloads = self.downloads
self.downloads = []
-
+
for fileClient in downloads {
fileClient.cancel()
}
}
-
+
@MainActor func downloadBanner(force: Bool = false) {
guard self.serverVersion >= 150 else {
return
}
-
+
if self.bannerClient != nil || force {
self.bannerClient?.delegate = nil
self.bannerClient?.cancel()
self.bannerClient = nil
-
+
if force {
self.bannerImage = nil
}
}
-
+
if self.bannerImage != nil {
return
}
-
- self.client.sendDownloadBanner { [weak self] success, downloadReferenceNumber, downloadTransferSize in
+
+ self.client.sendDownloadBanner {
+ [weak self] success, downloadReferenceNumber, downloadTransferSize in
if !success {
return
}
-
- if
- let self = self,
+
+ if let self = self,
let address = self.server?.address,
let port = self.server?.port,
let referenceNumber = downloadReferenceNumber,
- let transferSize = downloadTransferSize {
- self.bannerClient = HotlineFilePreviewClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize))
+ let transferSize = downloadTransferSize
+ {
+ self.bannerClient = HotlineFilePreviewClient(
+ address: address, port: UInt16(port), reference: referenceNumber,
+ size: UInt32(transferSize))
self.bannerClient?.delegate = self
self.bannerClient?.start()
}
}
}
-
+
// MARK: - Hotline Delegate
-
+
@MainActor func hotlineStatusChanged(status: HotlineClientStatus) {
print("Hotline: Connection status changed to: \(status)")
-
+
if status == .disconnected {
self.serverVersion = 123
self.serverName = nil
self.access = nil
-
+
self.users = []
self.chat = []
self.messageBoard = []
@@ -773,32 +821,31 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
self.filesLoaded = false
self.news = []
self.newsLoaded = false
-
+
self.bannerImage = nil
if let b = self.bannerClient {
b.cancel()
self.bannerClient = nil
}
-
+
self.deleteAllTransfers()
- }
- else if status == .loggedIn {
+ } else if status == .loggedIn {
if Prefs.shared.playSounds && Prefs.shared.playLoggedInSound {
SoundEffectPlayer.shared.playSoundEffect(.loggedIn)
}
}
-
+
self.status = status
}
-
+
func hotlineGetUserInfo() -> (String, UInt16) {
return (self.username, UInt16(self.iconID))
}
-
+
func hotlineReceivedAgreement(text: String) {
self.chat.append(ChatMessage(text: text, type: .agreement, date: Date()))
}
-
+
func hotlineReceivedNewsPost(message: String) {
let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/
let matches = message.matches(of: messageBoardRegex)
@@ -809,44 +856,44 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
} else {
self.messageBoard.insert(message, at: 0)
}
-
+
SoundEffectPlayer.shared.playSoundEffect(.newNews)
}
-
+
func hotlineReceivedServerMessage(message: String) {
if Prefs.shared.playChatSound && Prefs.shared.playChatSound {
SoundEffectPlayer.shared.playSoundEffect(.serverMessage)
}
-
+
print("Hotline: received server message:\n\(message)")
self.chat.append(ChatMessage(text: message, type: .server, date: Date()))
}
-
+
func hotlineReceivedPrivateMessage(userID: UInt16, message: String) {
if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) {
let user = self.users[existingUserIndex]
print("Hotline: received private message from \(user.name): \(message)")
-
+
if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound {
if self.unreadInstantMessages[userID] == nil {
SoundEffectPlayer.shared.playSoundEffect(.serverMessage)
- }
- else {
+ } else {
SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
}
}
-
- let instantMessage = InstantMessage(direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, date: Date())
+
+ let instantMessage = InstantMessage(
+ direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message,
+ date: Date())
if self.instantMessages[userID] == nil {
self.instantMessages[userID] = [instantMessage]
- }
- else {
+ } else {
self.instantMessages[userID]!.append(instantMessage)
}
self.unreadInstantMessages[userID] = userID
}
}
-
+
func hotlineReceivedChatMessage(message: String) {
if Prefs.shared.playSounds && Prefs.shared.playChatSound {
SoundEffectPlayer.shared.playSoundEffect(.chatMessage)
@@ -854,11 +901,11 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
self.chat.append(ChatMessage(text: message, type: .message, date: Date()))
self.unreadPublicChat = true
}
-
+
func hotlineReceivedUserList(users: [HotlineUser]) {
var existingUserIDs: [UInt16] = []
var userList: [User] = []
-
+
for u in users {
if let i = self.users.firstIndex(where: { $0.id == u.id }) {
// If a user is already in the user list we have to assume
@@ -866,59 +913,62 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
// which means let's keep their existing info.
existingUserIDs.append(u.id)
userList.append(self.users[i])
- }
- else {
+ } else {
userList.append(User(hotlineUser: u))
}
}
-
+
if !existingUserIDs.isEmpty {
self.users = self.users.filter { !existingUserIDs.contains($0.id) }
}
-
+
self.users = userList + self.users
}
-
+
func hotlineUserChanged(user: HotlineUser) {
self.addOrUpdateHotlineUser(user)
}
-
+
func hotlineUserDisconnected(userID: UInt16) {
if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) {
let user = self.users.remove(at: existingUserIndex)
-
+
if Prefs.shared.showJoinLeaveMessages {
self.chat.append(ChatMessage(text: "\(user.name) left", type: .status, date: Date()))
}
-
+
if Prefs.shared.playSounds && Prefs.shared.playLeaveSound {
SoundEffectPlayer.shared.playSoundEffect(.userLogout)
}
}
}
-
+
func hotlineReceivedUserAccess(options: HotlineUserAccessOptions) {
print("Hotline: got access options")
HotlineUserAccessOptions.printAccessOptions(options)
-
+
self.access = options
}
-
+
func hotlineReceivedErrorMessage(code: UInt32, message: String?) {
print("Hotline: received error message \(code)", message.debugDescription)
-
- self.errorDisplayed = (message != nil) // Show error if there is a message to display.
+
+ self.errorDisplayed = (message != nil) // Show error if there is a message to display.
self.errorMessage = message
-
+
if self.errorDisplayed,
- Prefs.shared.playSounds && Prefs.shared.playErrorSound {
+ Prefs.shared.playSounds && Prefs.shared.playErrorSound
+ {
SoundEffectPlayer.shared.playSoundEffect(.error)
}
}
-
+
// MARK: - Hotline Transfer Delegate
-
- func hotlineTransferStatusChanged(client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) {
+
+ func hotlineTransferStatusChanged(
+ client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus,
+ timeRemaining: TimeInterval
+ ) {
switch status {
case .unconnected:
break
@@ -953,34 +1003,34 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
}
}
}
-
- func hotlineFileDownloadReceivedInfo(client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork) {
+
+ func hotlineFileDownloadReceivedInfo(
+ client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork
+ ) {
if let transfer = self.transfers.first(where: { $0.id == reference }) {
transfer.title = info.name
}
}
-
+
func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) {
if let b = self.bannerClient, b.referenceNumber == reference {
#if os(macOS)
- self.bannerImage = NSImage(data: data)
+ self.bannerImage = NSImage(data: data)
#elseif os(iOS)
- self.bannerImage = UIImage(data: data)
+ self.bannerImage = UIImage(data: data)
#endif
- }
- else
- if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
+ } else if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
let transfer = self.transfers[i]
transfer.previewCallback?(transfer, data)
self.transfers.remove(at: i)
}
-
+
if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) {
self.downloads.remove(at: i)
}
}
-
- func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL) {
+
+ func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL) {
if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
let transfer = self.transfers[i]
transfer.fileURL = at
@@ -989,12 +1039,12 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
SoundEffectPlayer.shared.playSoundEffect(.transferComplete)
}
}
-
+
if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) {
self.downloads.remove(at: i)
}
}
-
+
func hotlineFileUploadComplete(client: HotlineFileUploadClient, reference: UInt32) {
if let i = self.transfers.firstIndex(where: { $0.id == reference }) {
let transfer = self.transfers[i]
@@ -1003,31 +1053,30 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
SoundEffectPlayer.shared.playSoundEffect(.transferComplete)
}
}
-
+
if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) {
self.downloads.remove(at: i)
}
}
-
+
// MARK: - Utilities
-
+
func updateServerTitle() {
self.serverTitle = self.serverName ?? self.server?.name ?? server?.address ?? "Server"
}
-
+
private func addOrUpdateHotlineUser(_ user: HotlineUser) {
print("Hotline: users: \n\(self.users)")
if let i = self.users.firstIndex(where: { $0.id == user.id }) {
print("Hotline: updating user \(self.users[i].name)")
self.users[i] = User(hotlineUser: user)
- }
- else {
+ } else {
if !self.users.isEmpty {
if Prefs.shared.playSounds && Prefs.shared.playJoinSound {
SoundEffectPlayer.shared.playSoundEffect(.userLogin)
}
}
-
+
print("Hotline: added user: \(user.name)")
self.users.append(User(hotlineUser: user))
if Prefs.shared.showJoinLeaveMessages {
@@ -1035,55 +1084,53 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega
}
}
}
-
+
private func findFile(in filesToSearch: [FileInfo], at path: [String]) -> FileInfo? {
guard !path.isEmpty, !filesToSearch.isEmpty else { return nil }
-
+
let currentName = path[0]
-
+
for file in filesToSearch {
if file.name == currentName {
if path.count == 1 {
return file
- }
- else if let subfiles = file.children {
+ } else if let subfiles = file.children {
let remainingPath = Array(path[1...])
return self.findFile(in: subfiles, at: remainingPath)
}
}
}
-
+
return nil
}
-
+
private func findNews(in newsToSearch: [NewsInfo], at path: [String]) -> NewsInfo? {
guard !path.isEmpty, !newsToSearch.isEmpty, let currentName = path.first else { return nil }
-
+
for news in newsToSearch {
if news.name == currentName {
if path.count == 1 {
return news
- }
- else if !news.children.isEmpty {
+ } else if !news.children.isEmpty {
let remainingPath = Array(path[1...])
return self.findNews(in: news.children, at: remainingPath)
}
}
}
-
+
return nil
}
-
+
private func findNewsArticle(id articleID: UInt32, at path: [String]) -> NewsInfo? {
guard let parent = self.findNews(in: self.news, at: path), !parent.children.isEmpty else {
return nil
}
-
+
return parent.children.first { child in
guard let childArticleID = child.articleID else {
return false
}
-
+
return child.type == .article && child.articleID == childArticleID
}
}
diff --git a/Hotline/Models/NewsInfo.swift b/Hotline/Models/NewsInfo.swift
index 6ee28b8..caf532f 100644
--- a/Hotline/Models/NewsInfo.swift
+++ b/Hotline/Models/NewsInfo.swift
@@ -8,28 +8,28 @@ enum NewsInfoType {
@Observable class NewsInfo: Identifiable, Hashable {
let id: UUID = UUID()
-
+
var name: String
var count: UInt
let type: NewsInfoType
-
+
var categoryID: UUID?
var articleID: UInt?
var parentID: UInt?
-
+
var path: [String]
var expanded: Bool = false
var children: [NewsInfo] = []
-
+
var articleFlavors: [String]?
var articleUsername: String?
var articleDate: Date?
var read: Bool = false
-
+
var expandable: Bool {
self.type == .bundle || self.type == .category || self.children.count > 0
}
-
+
var lookupPath: String? {
switch self.type {
case .bundle, .category:
@@ -38,21 +38,21 @@ enum NewsInfoType {
guard let aid = self.articleID else {
return nil
}
-// if let pid = self.parentID, pid != 0 {
-// return "/\(self.path.joined(separator: "/"))/\(pid)/\(aid)"
-// }
+ // if let pid = self.parentID, pid != 0 {
+ // return "/\(self.path.joined(separator: "/"))/\(pid)/\(aid)"
+ // }
return "/\(self.path.joined(separator: "/"))/\(aid)"
}
}
-
+
var parentArticleLookupPath: String? {
switch self.type {
case .bundle, .category:
-// if self.path.count <= 1 {
-// return "/"
-// }
-// let parentPath = self.path[0..<self.path.count-1]
-// return "/\(parentPath.joined(separator: "/"))"
+ // if self.path.count <= 1 {
+ // return "/"
+ // }
+ // let parentPath = self.path[0..<self.path.count-1]
+ // return "/\(parentPath.joined(separator: "/"))"
return nil
case .article:
guard let pid = self.parentID, pid != 0 else {
@@ -61,7 +61,7 @@ enum NewsInfoType {
return "/\(self.path.joined(separator: "/"))/\(pid)"
}
}
-
+
init(hotlineNewsCategory: HotlineNewsCategory) {
self.categoryID = hotlineNewsCategory.id
self.articleID = nil
@@ -69,15 +69,14 @@ enum NewsInfoType {
self.name = hotlineNewsCategory.name
self.count = UInt(hotlineNewsCategory.count)
self.path = hotlineNewsCategory.path
-
+
if hotlineNewsCategory.type == 2 {
self.type = .bundle
- }
- else {
+ } else {
self.type = .category
}
}
-
+
init(hotlineNewsArticle: HotlineNewsArticle) {
self.articleID = UInt(hotlineNewsArticle.id)
self.parentID = hotlineNewsArticle.parentID == 0 ? nil : UInt(hotlineNewsArticle.parentID)
@@ -85,19 +84,19 @@ enum NewsInfoType {
self.categoryID = nil
self.name = hotlineNewsArticle.title
self.count = 0
-// self.count = UInt(hotlineNewsArticle.count)
+ // self.count = UInt(hotlineNewsArticle.count)
self.path = hotlineNewsArticle.path
self.type = .article
-
+
self.articleFlavors = hotlineNewsArticle.flavors.map { $0.0 }
self.articleUsername = hotlineNewsArticle.username
self.articleDate = hotlineNewsArticle.date
}
-
+
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
-
+
static func == (lhs: NewsInfo, rhs: NewsInfo) -> Bool {
return lhs.id == rhs.id
}
diff --git a/Hotline/Models/Preferences.swift b/Hotline/Models/Preferences.swift
index 207ce91..bcceec5 100644
--- a/Hotline/Models/Preferences.swift
+++ b/Hotline/Models/Preferences.swift
@@ -23,7 +23,7 @@ enum PrefsKeys: String {
@Observable
class Prefs {
init() {
- UserDefaults.standard.register(defaults:[
+ UserDefaults.standard.register(defaults: [
PrefsKeys.username.rawValue: "guest",
PrefsKeys.userIconID.rawValue: 191,
PrefsKeys.refusePrivateMessages.rawValue: false,
@@ -42,93 +42,141 @@ class Prefs {
PrefsKeys.showBannerToolbar.rawValue: true,
PrefsKeys.showJoinLeaveMessages.rawValue: true,
])
-
+
self.username = UserDefaults.standard.string(forKey: PrefsKeys.username.rawValue)!
self.userIconID = UserDefaults.standard.integer(forKey: PrefsKeys.userIconID.rawValue)
- self.refusePrivateMessages = UserDefaults.standard.bool(forKey: PrefsKeys.refusePrivateMessages.rawValue)
- self.refusePrivateChat = UserDefaults.standard.bool(forKey: PrefsKeys.refusePrivateChat.rawValue)
- self.enableAutomaticMessage = UserDefaults.standard.bool(forKey: PrefsKeys.enableAutomaticMessage.rawValue)
- self.automaticMessage = UserDefaults.standard.string(forKey: PrefsKeys.automaticMessage.rawValue)!
+ self.refusePrivateMessages = UserDefaults.standard.bool(
+ forKey: PrefsKeys.refusePrivateMessages.rawValue)
+ self.refusePrivateChat = UserDefaults.standard.bool(
+ forKey: PrefsKeys.refusePrivateChat.rawValue)
+ self.enableAutomaticMessage = UserDefaults.standard.bool(
+ forKey: PrefsKeys.enableAutomaticMessage.rawValue)
+ self.automaticMessage = UserDefaults.standard.string(
+ forKey: PrefsKeys.automaticMessage.rawValue)!
self.playSounds = UserDefaults.standard.bool(forKey: PrefsKeys.playSounds.rawValue)
self.playChatSound = UserDefaults.standard.bool(forKey: PrefsKeys.playChatSound.rawValue)
- self.playFileTransferCompleteSound = UserDefaults.standard.bool(forKey: PrefsKeys.playFileTransferCompleteSound.rawValue)
- self.playPrivateMessageSound = UserDefaults.standard.bool(forKey: PrefsKeys.playPrivateMessageSound.rawValue)
+ self.playFileTransferCompleteSound = UserDefaults.standard.bool(
+ forKey: PrefsKeys.playFileTransferCompleteSound.rawValue)
+ self.playPrivateMessageSound = UserDefaults.standard.bool(
+ forKey: PrefsKeys.playPrivateMessageSound.rawValue)
self.playJoinSound = UserDefaults.standard.bool(forKey: PrefsKeys.playJoinSound.rawValue)
self.playLeaveSound = UserDefaults.standard.bool(forKey: PrefsKeys.playLeaveSound.rawValue)
- self.playLoggedInSound = UserDefaults.standard.bool(forKey: PrefsKeys.playLoggedInSound.rawValue)
+ self.playLoggedInSound = UserDefaults.standard.bool(
+ forKey: PrefsKeys.playLoggedInSound.rawValue)
self.playErrorSound = UserDefaults.standard.bool(forKey: PrefsKeys.playErrorSound.rawValue)
- self.playChatInvitationSound = UserDefaults.standard.bool(forKey: PrefsKeys.playChatInvitationSound.rawValue)
- self.showBannerToolbar = UserDefaults.standard.bool(forKey: PrefsKeys.showBannerToolbar.rawValue)
- self.showJoinLeaveMessages = UserDefaults.standard.bool(forKey: PrefsKeys.showJoinLeaveMessages.rawValue)
+ self.playChatInvitationSound = UserDefaults.standard.bool(
+ forKey: PrefsKeys.playChatInvitationSound.rawValue)
+ self.showBannerToolbar = UserDefaults.standard.bool(
+ forKey: PrefsKeys.showBannerToolbar.rawValue)
+ self.showJoinLeaveMessages = UserDefaults.standard.bool(
+ forKey: PrefsKeys.showJoinLeaveMessages.rawValue)
}
-
+
public static let shared = Prefs()
var username: String {
didSet { UserDefaults.standard.set(self.username, forKey: PrefsKeys.username.rawValue) }
}
-
+
var userIconID: Int {
didSet { UserDefaults.standard.set(self.userIconID, forKey: PrefsKeys.userIconID.rawValue) }
}
-
+
var refusePrivateMessages: Bool {
- didSet { UserDefaults.standard.set(self.refusePrivateMessages, forKey: PrefsKeys.refusePrivateMessages.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.refusePrivateMessages, forKey: PrefsKeys.refusePrivateMessages.rawValue)
+ }
}
-
+
var playSounds: Bool {
didSet { UserDefaults.standard.set(self.playSounds, forKey: PrefsKeys.playSounds.rawValue) }
}
-
+
var playChatSound: Bool {
- didSet { UserDefaults.standard.set(self.playChatSound, forKey: PrefsKeys.playChatSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(self.playChatSound, forKey: PrefsKeys.playChatSound.rawValue)
+ }
}
-
+
var playFileTransferCompleteSound: Bool {
- didSet { UserDefaults.standard.set(self.playFileTransferCompleteSound, forKey: PrefsKeys.playFileTransferCompleteSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.playFileTransferCompleteSound, forKey: PrefsKeys.playFileTransferCompleteSound.rawValue
+ )
+ }
}
-
+
var playPrivateMessageSound: Bool {
- didSet { UserDefaults.standard.set(self.playPrivateMessageSound, forKey: PrefsKeys.playPrivateMessageSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.playPrivateMessageSound, forKey: PrefsKeys.playPrivateMessageSound.rawValue)
+ }
}
-
+
var playJoinSound: Bool {
- didSet { UserDefaults.standard.set(self.playJoinSound, forKey: PrefsKeys.playJoinSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(self.playJoinSound, forKey: PrefsKeys.playJoinSound.rawValue)
+ }
}
-
+
var playLeaveSound: Bool {
- didSet { UserDefaults.standard.set(self.playLeaveSound, forKey: PrefsKeys.playLeaveSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(self.playLeaveSound, forKey: PrefsKeys.playLeaveSound.rawValue)
+ }
}
-
+
var playLoggedInSound: Bool {
- didSet { UserDefaults.standard.set(self.playLoggedInSound, forKey: PrefsKeys.playLoggedInSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.playLoggedInSound, forKey: PrefsKeys.playLoggedInSound.rawValue)
+ }
}
-
+
var playErrorSound: Bool {
- didSet { UserDefaults.standard.set(self.playErrorSound, forKey: PrefsKeys.playErrorSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(self.playErrorSound, forKey: PrefsKeys.playErrorSound.rawValue)
+ }
}
-
+
var playChatInvitationSound: Bool {
- didSet { UserDefaults.standard.set(self.playChatInvitationSound, forKey: PrefsKeys.playChatInvitationSound.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.playChatInvitationSound, forKey: PrefsKeys.playChatInvitationSound.rawValue)
+ }
}
-
+
var refusePrivateChat: Bool {
- didSet { UserDefaults.standard.set(self.refusePrivateChat, forKey: PrefsKeys.refusePrivateChat.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.refusePrivateChat, forKey: PrefsKeys.refusePrivateChat.rawValue)
+ }
}
-
+
var enableAutomaticMessage: Bool {
- didSet { UserDefaults.standard.set(self.enableAutomaticMessage, forKey: PrefsKeys.enableAutomaticMessage.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.enableAutomaticMessage, forKey: PrefsKeys.enableAutomaticMessage.rawValue)
+ }
}
-
+
var automaticMessage: String {
- didSet { UserDefaults.standard.set(self.automaticMessage, forKey: PrefsKeys.automaticMessage.rawValue) }
+ didSet {
+ UserDefaults.standard.set(self.automaticMessage, forKey: PrefsKeys.automaticMessage.rawValue)
+ }
}
-
+
var showBannerToolbar: Bool {
- didSet { UserDefaults.standard.set(self.showBannerToolbar, forKey: PrefsKeys.showBannerToolbar.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.showBannerToolbar, forKey: PrefsKeys.showBannerToolbar.rawValue)
+ }
}
-
+
var showJoinLeaveMessages: Bool {
- didSet { UserDefaults.standard.set(self.showJoinLeaveMessages, forKey: PrefsKeys.showJoinLeaveMessages.rawValue) }
+ didSet {
+ UserDefaults.standard.set(
+ self.showJoinLeaveMessages, forKey: PrefsKeys.showJoinLeaveMessages.rawValue)
+ }
}
}
diff --git a/Hotline/Models/PreviewFileInfo.swift b/Hotline/Models/PreviewFileInfo.swift
index 7dd692f..06a2966 100644
--- a/Hotline/Models/PreviewFileInfo.swift
+++ b/Hotline/Models/PreviewFileInfo.swift
@@ -12,14 +12,13 @@ struct PreviewFileInfo: Identifiable, Codable {
var port: Int
var size: Int
var name: String
-
+
var previewType: FilePreviewType {
let fileExtension = (self.name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
if fileType.isSubtype(of: .image) {
return .image
- }
- else if fileType.isSubtype(of: .text) {
+ } else if fileType.isSubtype(of: .text) {
return .text
}
}
diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift
index 3f4c9e8..a3e3dde 100644
--- a/Hotline/Models/Server.swift
+++ b/Hotline/Models/Server.swift
@@ -4,14 +4,18 @@ struct Server: Codable {
var name: String?
var description: String?
var users: Int
-
+
var address: String
var port: Int
var login: String
var password: String
var autoconnect: Bool = false
-
- init(name: String?, description: String?, address: String, port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String? = nil, password: String? = nil, autoconnect: Bool? = false) {
+
+ init(
+ name: String?, description: String?, address: String,
+ port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String? = nil,
+ password: String? = nil, autoconnect: Bool? = false
+ ) {
self.name = name
self.description = description
self.address = address.lowercased()
@@ -21,27 +25,27 @@ struct Server: Codable {
self.password = password ?? ""
self.autoconnect = autoconnect ?? false
}
-
+
init?(url: URL) {
guard url.scheme?.lowercased() == "hotline" else {
return nil
}
-
+
guard let host = url.host(percentEncoded: false) else {
return nil
}
-
+
self.name = nil
self.description = nil
self.users = 0
-
+
self.address = host.lowercased()
self.port = url.port ?? HotlinePorts.DefaultServerPort
-
+
self.login = url.user(percentEncoded: false) ?? ""
self.password = url.password(percentEncoded: false) ?? ""
}
-
+
static func parseServerAddressAndPort(_ address: String) -> (String, Int) {
let url = URL(string: "hotline://\(address)")
let port = url?.port ?? HotlinePorts.DefaultServerPort
@@ -56,7 +60,8 @@ extension Server: Identifiable {
extension Server: Equatable {
static func == (lhs: Server, rhs: Server) -> Bool {
- return (lhs.address == rhs.address) && (lhs.port == rhs.port) && (lhs.login == rhs.login) && (lhs.password == rhs.password)
+ return (lhs.address == rhs.address) && (lhs.port == rhs.port) && (lhs.login == rhs.login)
+ && (lhs.password == rhs.password)
}
}
diff --git a/Hotline/Models/Tracker.swift b/Hotline/Models/Tracker.swift
index 171cb35..0ebcf2f 100644
--- a/Hotline/Models/Tracker.swift
+++ b/Hotline/Models/Tracker.swift
@@ -3,12 +3,12 @@ import SwiftUI
@Observable final class Tracker {
let address: String
let port: Int
-
+
init(address: String, port: Int = HotlinePorts.DefaultTrackerPort) {
self.address = address
self.port = port
}
-
+
static func parseTrackerAddressAndPort(_ address: String) -> (String, Int) {
let url = URL(string: "hotlinetracker://\(address)")
let port = url?.port ?? HotlinePorts.DefaultTrackerPort
diff --git a/Hotline/Models/TransferInfo.swift b/Hotline/Models/TransferInfo.swift
index 50b23f5..8aacf1d 100644
--- a/Hotline/Models/TransferInfo.swift
+++ b/Hotline/Models/TransferInfo.swift
@@ -3,32 +3,32 @@ import SwiftUI
@Observable
class TransferInfo: Identifiable, Equatable, Hashable {
var id: UInt32
-
+
var title: String
var size: UInt
var progress: Double = 0.0
var timeRemaining: TimeInterval = 0.0
var completed: Bool = false
var failed: Bool = false
-
+
// For file based transfers (i.e. not previews)
var fileURL: URL? = nil
-
+
var progressCallback: ((TransferInfo, Double) -> Void)? = nil
var downloadCallback: ((TransferInfo, URL) -> Void)? = nil
var uploadCallback: ((TransferInfo) -> Void)? = nil
var previewCallback: ((TransferInfo, Data) -> Void)? = nil
-
+
init(id: UInt32, title: String, size: UInt) {
self.id = id
self.title = title
self.size = size
}
-
+
static func == (lhs: TransferInfo, rhs: TransferInfo) -> Bool {
return lhs.id == rhs.id
}
-
+
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
diff --git a/Hotline/Models/User.swift b/Hotline/Models/User.swift
index 21a8fff..90b0801 100644
--- a/Hotline/Models/User.swift
+++ b/Hotline/Models/User.swift
@@ -12,21 +12,21 @@ struct User: Identifiable {
var name: String
var iconID: UInt
var status: UserStatus
-
+
var isAdmin: Bool { self.status.contains(.admin) }
var isIdle: Bool { self.status.contains(.idle) }
-
+
init(hotlineUser: HotlineUser) {
var status: UserStatus = UserStatus()
if hotlineUser.isIdle { status.update(with: .idle) }
if hotlineUser.isAdmin { status.update(with: .admin) }
-
+
self.id = hotlineUser.id
self.name = hotlineUser.name
self.iconID = UInt(hotlineUser.iconID)
self.status = status
}
-
+
init(id: UInt16, name: String, iconID: UInt, status: UserStatus) {
self.id = id
self.name = name