diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-05-23 12:50:16 -0700 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-05-23 12:50:16 -0700 |
| commit | 23b3f338f0818a53b2499692e999a0528e5d9753 (patch) | |
| tree | 19f2d018824cdc41c0aa37dde6bde5d9054be9f2 /Hotline | |
| parent | 842611754debec7d1814e44e6a2cf302e84c0d66 (diff) | |
Add Bookmark exporting to Server window context menu.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Application-macOS.swift | 3 | ||||
| -rw-r--r-- | Hotline/Info.plist | 14 | ||||
| -rw-r--r-- | Hotline/Models/Bookmark.swift | 100 | ||||
| -rw-r--r-- | Hotline/Utility/BookmarkDocument.swift | 32 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 26 |
5 files changed, 153 insertions, 22 deletions
diff --git a/Hotline/Application-macOS.swift b/Hotline/Application-macOS.swift index 016cb60..9aec187 100644 --- a/Hotline/Application-macOS.swift +++ b/Hotline/Application-macOS.swift @@ -22,9 +22,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { AppLaunchState.shared.launchState = .launched - let blah = NSFileTypeForHFSTypeCode("HTbm".fourCharCode()) - print("FILETYPE: \(blah)", blah!, "'HTbm'") - CKContainer.default().accountStatus { status, error in switch status { case .noAccount: diff --git a/Hotline/Info.plist b/Hotline/Info.plist index ac7a2c9..b43d991 100644 --- a/Hotline/Info.plist +++ b/Hotline/Info.plist @@ -8,10 +8,10 @@ <key>CFBundleTypeName</key> <string>Hotline Bookmark</string> <key>LSHandlerRank</key> - <string>Alternate</string> + <string>None</string> <key>LSItemContentTypes</key> <array> - <string>'HTbm'</string> + <string>'HTbm'</string> </array> </dict> </array> @@ -38,25 +38,21 @@ </array> </dict> </array> - <key>UTImportedTypeDeclarations</key> + <key>UTExportedTypeDeclarations</key> <array> <dict> - <key>UTTypeConformsTo</key> - <array/> <key>UTTypeDescription</key> <string>Hotline Bookmark</string> <key>UTTypeIconFiles</key> <array/> <key>UTTypeIdentifier</key> - <string>'HTbm'</string> + <string>'HTbm'</string> <key>UTTypeTagSpecification</key> <dict> <key>public.filename-extension</key> <array> - <string>.htbm</string> + <string>.hlbm</string> </array> - <key>public.mime-type</key> - <array/> </dict> </dict> </array> diff --git a/Hotline/Models/Bookmark.swift b/Hotline/Models/Bookmark.swift index 234a196..115e5a9 100644 --- a/Hotline/Models/Bookmark.swift +++ b/Hotline/Models/Bookmark.swift @@ -96,16 +96,11 @@ final class Bookmark { self.serverDescription = server.description } - init?(fileURL bookmarkFileURL: URL) { - guard 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 { + init?(fileData: Data, name: String? = nil) { + guard fileData.count <= 2000 else { return nil } - - print("Bookmark: Parsing Hotline bookmark file...") - + var fileDataArray: [UInt8] = [UInt8](fileData) guard let headerValue = fileDataArray.consumeUInt32(), @@ -128,13 +123,100 @@ final class Bookmark { let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString) self.type = .server - self.name = addressHost + if let name = name, !name.isEmpty { + self.name = name + } + else { + self.name = addressHost + } self.address = addressHost self.port = addressPort 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 { + return nil + } + + print("Bookmark: Parsing Hotline bookmark file...") + + 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 + } + + 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.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) diff --git a/Hotline/Utility/BookmarkDocument.swift b/Hotline/Utility/BookmarkDocument.swift new file mode 100644 index 0000000..47fa442 --- /dev/null +++ b/Hotline/Utility/BookmarkDocument.swift @@ -0,0 +1,32 @@ +import SwiftUI +import Foundation +import UniformTypeIdentifiers + +struct BookmarkDocument: FileDocument { + static var readableContentTypes: [UTType] { [.data, UTType(filenameExtension: "hlbm")!] } + static var writableContentTypes: [UTType] { [.data, UTType(filenameExtension: "hlbm")!] } + + var bookmark: Bookmark + + init(bookmark: Bookmark) { + self.bookmark = bookmark + } + + init(configuration: ReadConfiguration) throws { + guard configuration.file.isRegularFile, + let data = configuration.file.regularFileContents, + let fileName = configuration.file.preferredFilename, + let bookmark = Bookmark(fileData: data, name: (fileName as NSString).deletingPathExtension) + else { + throw CocoaError(.fileReadCorruptFile) + } + self.bookmark = bookmark + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + let wrapper = FileWrapper(regularFileWithContents: self.bookmark.bookmarkFileData()!) + wrapper.fileAttributes[FileAttributeKey.hfsCreatorCode.rawValue] = "HTLC".fourCharCode() + wrapper.fileAttributes[FileAttributeKey.hfsTypeCode.rawValue] = "HTbm".fourCharCode() + return wrapper + } +} diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift index 5a9db9b..5ab4c65 100644 --- a/Hotline/macOS/TrackerView.swift +++ b/Hotline/macOS/TrackerView.swift @@ -1,9 +1,9 @@ import SwiftUI import SwiftData +import Foundation import UniformTypeIdentifiers struct TrackerView: View { -// @Environment(BookmarksOld.self) private var bookmarks: BookmarksOld @Environment(\.colorScheme) private var colorScheme @Environment(\.openWindow) private var openWindow @Environment(\.controlActiveState) private var controlActiveState @@ -14,6 +14,8 @@ struct TrackerView: View { @State private var trackerSheetBookmark: Bookmark? = nil @State private var attemptedPrepopulate: Bool = false @State private var fileDropActive = false + @State private var bookmarkExportActive = false + @State private var bookmarkExport: BookmarkDocument? = nil @Query(sort: \Bookmark.order) private var bookmarks: [Bookmark] @State private var selection: Bookmark? = nil @@ -96,6 +98,17 @@ struct TrackerView: View { } } + if item.type == .server { + Button { + bookmarkExport = BookmarkDocument(bookmark: item) + bookmarkExportActive = true + } label: { + Label("Export Bookmark...", systemImage: "bookmark.square") + } + } + + Divider() + Button { Bookmark.delete(item, context: modelContext) } label: { @@ -121,6 +134,17 @@ struct TrackerView: View { openWindow(id: "server", value: server) } } + .fileExporter(isPresented: $bookmarkExportActive, document: bookmarkExport, contentTypes: [.data], defaultFilename: "\(bookmarkExport?.bookmark.name ?? "Hotline Bookmark").hlbm", onCompletion: { result in + switch result { + case .success(let fileURL): + print("Hotline Bookmark: Successfully exported:", fileURL) + case .failure(let err): + print("Hotline Bookmark: Failed to export:", err) + } + + bookmarkExport = nil + bookmarkExportActive = false + }, onCancellation: {}) .onKeyPress(.rightArrow) { if let bookmark = selection, |