From a9ebc5715cdb957c09d36b4bc0871c66a44e33ea Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 15 Sep 2024 17:35:45 -0400 Subject: [PATCH] Version 5.1 --- .../Pulse/Helpers/CoreData+Extensions.swift | 18 +-- Sources/Pulse/Helpers/Helpers.swift | 32 +++-- Sources/Pulse/Helpers/PulseDocument.swift | 16 +-- Sources/Pulse/Helpers/Regex.swift | 17 +-- .../LoggerStore+Configuration.swift | 2 +- .../LoggerStore/LoggerStore+Entities.swift | 4 +- .../Pulse/LoggerStore/LoggerStore+Model.swift | 2 +- Sources/Pulse/LoggerStore/LoggerStore.swift | 32 ++--- .../RemoteLogger-Connection.swift | 38 +++--- .../RemoteLogger/RemoteLogger-Protocol.swift | 114 ++++++++++------- Sources/Pulse/RemoteLogger/RemoteLogger.swift | 6 +- .../URLSessionProxyDelegate.swift | 6 +- .../Extensions/Foundation+Extensions.swift | 20 +-- .../NSAttributedString+Extensions.swift | 4 +- .../PulseUI/Extensions/Pulse+Extensions.swift | 97 ++++++++++++-- .../Extensions/SwiftUI+Extensions.swift | 62 ++------- .../Features/Console/ConsoleDataSource.swift | 2 +- .../Features/Console/ConsoleDelegate.swift | 22 ---- .../Features/Console/ConsoleEnvironment.swift | 8 +- .../Features/Console/ConsoleView-ios.swift | 8 +- .../Console/List/ConsoleListContentView.swift | 16 ++- .../Console/List/ConsoleListView.swift | 6 +- .../Console/Views/ConsoleContextMenu.swift | 2 +- .../Console/Views/ConsoleEntityCell.swift | 6 +- .../Console/Views/ConsoleHelperViews.swift | 4 +- .../Views/ConsoleListDisplaySettings.swift | 93 ++++++++++---- .../Console/Views/ConsoleMessageCell.swift | 25 ++-- .../Console/Views/ConsoleRouterView.swift | 24 ++-- .../Console/Views/ConsoleTaskCell.swift | 120 +++++++++--------- .../Console/Views/ConsoleToolbarView.swift | 33 ++--- .../Features/FileViewer/FileViewer.swift | 4 +- .../RichTextView/RichTextView-watchos.swift | 2 +- .../RichTextView/RichTextView.swift | 13 +- .../RichTextView/RichTextViewModel.swift | 13 +- .../RichTextViewSearchToobar-ios.swift | 2 +- .../RichTextView/WrappedTextView.swift | 9 +- .../Cells/ConsoleSearchLogLevelsCell.swift | 12 +- .../Cells/ConsoleSearchToggleCell.swift | 13 +- .../Features/Filters/ConsoleFiltersView.swift | 8 +- .../Views/ConsoleDomainsSelectionView.swift | 2 +- .../Views/ConsoleLabelsSelectionView.swift | 2 +- .../ConsoleSearchListSelectionView.swift | 48 +++++-- .../Views/ConsoleSearchSectionHeader.swift | 31 +---- .../Filters/Views/ConsoleSection.swift | 20 ++- .../Views/ConsoleSessionsPickerView.swift | 14 +- .../Inspector/Cells/NetworkCookiesCell.swift | 8 +- .../Inspector/Cells/NetworkMetricsCell.swift | 2 +- .../Cells/NetworkRequestInfoCell.swift | 12 +- .../Cells/NetworkRequestStatusCell.swift | 26 ++-- .../NetworkRequestStatusSectionView.swift | 4 +- .../Cells/NetworkResponseBodyCell.swift | 2 +- .../Inspector/NetworkDetailsView.swift | 8 +- .../NetworkInspectorView-shared.swift | 2 +- .../Inspector/NetworkInspectorView-tvos.swift | 2 +- .../NetworkInspectorView-watchos.swift | 2 +- .../Inspector/NetworkInspectorView.swift | 14 +- .../ConsoleMessageDetailsView.swift | 20 +-- .../ConsoleMessageMetadataView.swift | 24 ++-- .../RemoteLoggerEnterPasswordView.swift | 2 +- .../Remote/RemoteLoggerErrorView.swift | 6 +- .../RemoteLoggerSelectedDeviceView.swift | 2 +- .../Remote/RemoteLoggerSettingsView.swift | 6 +- .../Search/ConsoleSearchListContentView.swift | 4 +- .../Search/ConsoleSearchViewModel.swift | 10 +- .../Services/ConsoleSearchOccurrence.swift | 32 ++--- .../Services/ConsoleSearchOperation.swift | 33 +++-- .../Search/Services/ConsoleSearchScope.swift | 10 +- .../Search/Services/ConsoleSearchTerm.swift | 13 +- .../Views/ConsoleSearchContextMenu.swift | 2 +- .../ConsoleSearchResultsSectionView.swift | 49 ++++--- .../Views/ConsoleSearchSuggestionView.swift | 2 +- .../Views/ConsoleSearchSuggestionsView.swift | 4 +- .../Search/Views/ConsoleSearchToolbar.swift | 4 +- .../Features/Sessions/SessionListView.swift | 21 ++- .../Features/Sessions/SessionPickerView.swift | 2 +- .../Features/Sessions/SessionsView.swift | 6 +- .../Features/Settings/SettingsView-ios.swift | 25 +--- .../Features/Settings/StoreDetailsView.swift | 3 +- Sources/PulseUI/Helpers/Components.swift | 41 ------ .../Helpers/FileViewModelContext.swift | 29 +++-- Sources/PulseUI/Helpers/Formatters.swift | 30 ++--- .../PulseUI/Helpers/LoggerStoreIndex.swift | 14 +- .../Helpers/ManagedObjectsCountObserver.swift | 14 +- .../Helpers/ManagedObjectsObserver.swift | 10 +- Sources/PulseUI/Helpers/ShareItems.swift | 24 ++-- .../Helpers/StatusLabelViewModel.swift | 14 +- .../PulseUI/Helpers/StringSearchOptions.swift | 34 +++-- Sources/PulseUI/Helpers/TextHelper.swift | 35 +++-- Sources/PulseUI/Helpers/TextRenderer.swift | 101 ++++++++------- .../PulseUI/Helpers/TextRendererJSON.swift | 41 +++--- .../PulseUI/Helpers/UIKit+Extensions.swift | 2 +- Sources/PulseUI/Helpers/UXKit.swift | 61 +++++---- Sources/PulseUI/Helpers/UserSettings.swift | 10 +- Sources/PulseUI/Mocks/MockStore.swift | 8 +- Sources/PulseUI/Mocks/MockTask.swift | 62 ++++----- Sources/PulseUI/Views/Checkbox.swift | 9 +- Sources/PulseUI/Views/ContextMenus.swift | 65 +++++++--- Sources/PulseUI/Views/DateRangePicker.swift | 32 ++--- Sources/PulseUI/Views/ImageViewer.swift | 28 ++-- Sources/PulseUI/Views/InfoRow.swift | 28 ++-- .../Views/KeyValueSectionViewModel.swift | 34 +++-- .../PulseUI/Views/LoggerStoreSizeChart.swift | 21 +-- .../Metrics/NetworkInspectorMetricsView.swift | 4 +- .../NetworkInspectorTransactionView.swift | 28 ++-- .../NetworkInspectorTransferInfoView.swift | 38 +++--- .../Metrics/TimingViewModel+Metrics.swift | 2 +- .../PulseUI/Views/PDFRepresentedView.swift | 24 ++-- Sources/PulseUI/Views/PinView.swift | 33 +++++ Sources/PulseUI/Views/PlaceholderView.swift | 16 ++- Sources/PulseUI/Views/SearchBar-macos.swift | 6 +- Sources/PulseUI/Views/SectionHeaderView.swift | 13 +- Sources/PulseUI/Views/ShareStoreView.swift | 13 +- Sources/PulseUI/Views/ShareView.swift | 30 +++-- Sources/PulseUI/Views/TimingView.swift | 54 ++++---- Sources/PulseUI/Views/WebView.swift | 25 ++-- 115 files changed, 1342 insertions(+), 1085 deletions(-) delete mode 100644 Sources/PulseUI/Features/Console/ConsoleDelegate.swift delete mode 100644 Sources/PulseUI/Helpers/Components.swift create mode 100644 Sources/PulseUI/Views/PinView.swift diff --git a/Sources/Pulse/Helpers/CoreData+Extensions.swift b/Sources/Pulse/Helpers/CoreData+Extensions.swift index 0a8d3e479..7a93a3786 100644 --- a/Sources/Pulse/Helpers/CoreData+Extensions.swift +++ b/Sources/Pulse/Helpers/CoreData+Extensions.swift @@ -5,30 +5,30 @@ import CoreData extension NSManagedObjectContext { - func fetch(_ entity: T.Type, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> [T] { + package func fetch(_ entity: T.Type, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> [T] { let request = NSFetchRequest(entityName: String(describing: entity)) configure(request) return try fetch(request) } - func fetch(_ entity: T.Type, sortedBy keyPath: KeyPath, ascending: Bool = true, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> [T] { + package func fetch(_ entity: T.Type, sortedBy keyPath: KeyPath, ascending: Bool = true, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> [T] { try fetch(entity) { $0.sortDescriptors = [NSSortDescriptor(keyPath: keyPath, ascending: ascending)] } } - func first(_ entity: T.Type, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> T? { + package func first(_ entity: T.Type, _ configure: (NSFetchRequest) -> Void = { _ in }) throws -> T? { try fetch(entity) { $0.fetchLimit = 1 configure($0) }.first } - func count(for entity: T.Type) throws -> Int { + package func count(for entity: T.Type) throws -> Int { try count(for: NSFetchRequest(entityName: String(describing: entity))) } - func performAndReturn(_ closure: () throws -> T) throws -> T { + package func performAndReturn(_ closure: () throws -> T) throws -> T { var result: Result? performAndWait { result = Result { try closure() } @@ -48,7 +48,7 @@ extension NSPersistentContainer { return container } - func loadStore() throws { + package func loadStore() throws { var loadError: Swift.Error? loadPersistentStores { description, error in if let error = error { @@ -130,8 +130,8 @@ extension NSRelationshipDescription { } } -enum KeyValueEncoding { - static func encodeKeyValuePairs(_ pairs: [String: String]?, sanitize: Bool = false) -> String { +package enum KeyValueEncoding { + package static func encodeKeyValuePairs(_ pairs: [String: String]?, sanitize: Bool = false) -> String { var output = "" let sorted = (pairs ?? [:]).sorted { $0.key < $1.key } for (name, value) in sorted { @@ -143,7 +143,7 @@ enum KeyValueEncoding { return output } - static func decodeKeyValuePairs(_ string: String) -> [String: String] { + package static func decodeKeyValuePairs(_ string: String) -> [String: String] { let pairs = string.components(separatedBy: "\n") var output: [String: String] = [:] for pair in pairs { diff --git a/Sources/Pulse/Helpers/Helpers.swift b/Sources/Pulse/Helpers/Helpers.swift index 14924fd36..d961d107e 100644 --- a/Sources/Pulse/Helpers/Helpers.swift +++ b/Sources/Pulse/Helpers/Helpers.swift @@ -6,11 +6,11 @@ import Foundation import CoreData import CommonCrypto -var Files: FileManager { FileManager.default } +package var Files: FileManager { FileManager.default } extension FileManager { @discardableResult - func createDirectoryIfNeeded(at url: URL) -> Bool { + package func createDirectoryIfNeeded(at url: URL) -> Bool { guard !fileExists(atPath: url.path) else { return false } try? createDirectory(at: url, withIntermediateDirectories: true, attributes: [:]) return true @@ -18,22 +18,22 @@ extension FileManager { } extension URL { - func appending(filename: String) -> URL { + package func appending(filename: String) -> URL { appendingPathComponent(filename, isDirectory: false) } - func appending(directory: String) -> URL { + package func appending(directory: String) -> URL { appendingPathComponent(directory, isDirectory: true) } - static var temp: URL { + package static var temp: URL { let url = Files.temporaryDirectory .appending(directory: "com.github.kean.logger") Files.createDirectoryIfNeeded(at: url) return url } - static var logs: URL { + package static var logs: URL { #if os(tvOS) let searchPath = FileManager.SearchPathDirectory.cachesDirectory #else @@ -58,7 +58,7 @@ extension Data { /// print("http://test.com".data(using: .utf8)!.sha1) /// // prints "c6b6cafcb77f54d43cd1bd5361522a5e0c074b65" /// ``` - var sha1: Data { + package var sha1: Data { Data(withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) CC_SHA1(bytes.baseAddress, CC_LONG(count), &hash) @@ -66,7 +66,7 @@ extension Data { }) } - var hexString: String { + package var hexString: String { map { String(format: "%02x", $0) }.joined() } } @@ -114,10 +114,14 @@ extension URL { } } -struct LoggerBlogDataStore { - let getDecompressedData: (LoggerBlobHandleEntity) -> Data? +package struct LoggerBlogDataStore { + package let getDecompressedData: (LoggerBlobHandleEntity) -> Data? - init(_ store: LoggerStore) { + package init(_ getDecompressedData: @escaping (LoggerBlobHandleEntity) -> Data?) { + self.getDecompressedData = getDecompressedData + } + + package init(_ store: LoggerStore) { self.getDecompressedData = { [weak store] in store?.getDecompressedData(for: $0) } @@ -128,7 +132,7 @@ struct LoggerBlogDataStore { } /// The key for `NSManagedObjectContext` `userInfo`. - static let loggerStoreKey = "com.github.kean.pulse.associated-logger-store" + package static let loggerStoreKey = "com.github.kean.pulse.associated-logger-store" } struct TemporaryDirectory { @@ -145,11 +149,11 @@ struct TemporaryDirectory { } extension Data { - func compressed() throws -> Data { + package func compressed() throws -> Data { try (self as NSData).compressed(using: .lzfse) as Data } - func decompressed() throws -> Data { + package func decompressed() throws -> Data { try (self as NSData).decompressed(using: .lzfse) as Data } } diff --git a/Sources/Pulse/Helpers/PulseDocument.swift b/Sources/Pulse/Helpers/PulseDocument.swift index 99a7e5307..4857efc3c 100644 --- a/Sources/Pulse/Helpers/PulseDocument.swift +++ b/Sources/Pulse/Helpers/PulseDocument.swift @@ -4,11 +4,11 @@ import CoreData -final class PulseDocument { - let container: NSPersistentContainer - var context: NSManagedObjectContext { container.viewContext } +package final class PulseDocument { + package let container: NSPersistentContainer + package var context: NSManagedObjectContext { container.viewContext } - init(documentURL: URL) throws { + package init(documentURL: URL) throws { guard Files.fileExists(atPath: documentURL.deletingLastPathComponent().path) else { throw LoggerStore.Error.fileDoesntExist } @@ -20,7 +20,7 @@ final class PulseDocument { try container.loadStore() } - func close() throws { + package func close() throws { let coordinator = container.persistentStoreCoordinator for store in coordinator.persistentStores { try coordinator.remove(store) @@ -40,7 +40,7 @@ final class PulseDocument { }() } -final class PulseBlobEntity: NSManagedObject { - @NSManaged var key: String - @NSManaged var data: Data +package final class PulseBlobEntity: NSManagedObject { + @NSManaged package var key: String + @NSManaged package var data: Data } diff --git a/Sources/Pulse/Helpers/Regex.swift b/Sources/Pulse/Helpers/Regex.swift index 797ddc1a4..237acb64d 100644 --- a/Sources/Pulse/Helpers/Regex.swift +++ b/Sources/Pulse/Helpers/Regex.swift @@ -4,18 +4,19 @@ import Foundation -final class Regex: @unchecked Sendable { +package final class Regex: @unchecked Sendable { private let regex: NSRegularExpression - struct Options: OptionSet { - let rawValue: Int + package struct Options: OptionSet { + package let rawValue: Int + package init(rawValue: Int) { self.rawValue = rawValue } - static let caseInsensitive = Options(rawValue: 1 << 0) - static let multiline = Options(rawValue: 1 << 1) - static let dotMatchesLineSeparators = Options(rawValue: 1 << 2) + package static let caseInsensitive = Options(rawValue: 1 << 0) + package static let multiline = Options(rawValue: 1 << 1) + package static let dotMatchesLineSeparators = Options(rawValue: 1 << 2) } - init(_ pattern: String, _ options: Options = []) throws { + package init(_ pattern: String, _ options: Options = []) throws { var ops = NSRegularExpression.Options() if options.contains(.caseInsensitive) { ops.insert(.caseInsensitive) } if options.contains(.multiline) { ops.insert(.anchorsMatchLines) } @@ -24,7 +25,7 @@ final class Regex: @unchecked Sendable { self.regex = try NSRegularExpression(pattern: pattern, options: ops) } - func isMatch(_ s: String) -> Bool { + package func isMatch(_ s: String) -> Bool { let range = NSRange(s.startIndex.. Event? = { $0 } - var isAutoStartingSession = true + package var isAutoStartingSession = true /// Initializes the configuration. /// diff --git a/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift b/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift index 157917dc3..64c8e140a 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift @@ -281,9 +281,9 @@ public final class LoggerBlobHandleEntity: NSManagedObject { /// A decompressed blob size. @NSManaged public var decompressedSize: Int32 - @NSManaged var isUncompressed: Bool + @NSManaged package var isUncompressed: Bool - @NSManaged var rawContentType: String? + @NSManaged package var rawContentType: String? /// A blob content type. public var contentType: NetworkLogger.ContentType? { diff --git a/Sources/Pulse/LoggerStore/LoggerStore+Model.swift b/Sources/Pulse/LoggerStore/LoggerStore+Model.swift index 09062864e..381f38968 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore+Model.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore+Model.swift @@ -8,7 +8,7 @@ extension LoggerStore { /// Returns Core Data model used by the store. /// /// - warning: Model has to be loaded only once. - nonisolated(unsafe) static let model: NSManagedObjectModel = { + nonisolated(unsafe) package static let model: NSManagedObjectModel = { typealias Entity = NSEntityDescription typealias Attribute = NSAttributeDescription typealias Relationship = NSRelationshipDescription diff --git a/Sources/Pulse/LoggerStore/LoggerStore.swift b/Sources/Pulse/LoggerStore/LoggerStore.swift index a09663036..a951df7c9 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore.swift @@ -252,7 +252,7 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { return container } - func startSession(_ session: Session, info: Info.AppInfo) { + package func startSession(_ session: Session, info: Info.AppInfo) { backgroundContext.performAndWait { self.session = session saveEntity(for: session, info: info) @@ -342,7 +342,7 @@ extension LoggerStore { } /// Handles event emitted by the external store. - func handleExternalEvent(_ event: Event) { + package func handleExternalEvent(_ event: Event) { perform { _ in self._handle(event) } } @@ -710,7 +710,7 @@ extension LoggerStore { // MARK: - Performing Changes - func perform(_ changes: @escaping (NSManagedObjectContext) -> Void) { + package func perform(_ changes: @escaping (NSManagedObjectContext) -> Void) { if options.contains(.synchronous) { backgroundContext.performAndWait { changes(backgroundContext) @@ -1252,17 +1252,17 @@ extension LoggerStore { // MARK: - LoggerStore (Manifest) extension LoggerStore { - private struct Manifest: Codable { - var storeId: UUID - var version: Version - var lastSweepDate: Date? + package struct Manifest: Codable { + package var storeId: UUID + package var version: Version + package var lastSweepDate: Date? - init(storeId: UUID, version: Version) { + package init(storeId: UUID, version: Version) { self.storeId = storeId self.version = version } - init?(url: URL) { + package init?(url: URL) { guard let data = try? Data(contentsOf: url), let manifest = try? JSONDecoder().decode(Manifest.self, from: data) else { return nil @@ -1273,14 +1273,14 @@ extension LoggerStore { } extension Version { - static let minimumSupportedVersion = Version(3, 1, 0) - static let currentStoreVersion = Version(3, 6, 0) - static let currentProtocolVersion = Version(4, 0, 0) + package static let minimumSupportedVersion = LoggerStore.Version(3, 1, 0) + package static let currentStoreVersion = LoggerStore.Version(3, 6, 0) + package static let currentProtocolVersion = LoggerStore.Version(4, 0, 0) } // MARK: - Constants -let manifestFilename = "manifest.json" -let databaseFilename = "logs.sqlite" -let infoFilename = "info.json" -let blobsDirectoryName = "blobs" +package let manifestFilename = "manifest.json" +package let databaseFilename = "logs.sqlite" +package let infoFilename = "info.json" +package let blobsDirectoryName = "blobs" diff --git a/Sources/Pulse/RemoteLogger/RemoteLogger-Connection.swift b/Sources/Pulse/RemoteLogger/RemoteLogger-Connection.swift index fa457cf0f..7d80cb98c 100644 --- a/Sources/Pulse/RemoteLogger/RemoteLogger-Connection.swift +++ b/Sources/Pulse/RemoteLogger/RemoteLogger-Connection.swift @@ -8,29 +8,29 @@ import CryptoKit import OSLog @MainActor -protocol RemoteLoggerConnectionDelegate: AnyObject { +package protocol RemoteLoggerConnectionDelegate: AnyObject { func connection(_ connection: RemoteLogger.Connection, didChangeState newState: NWConnection.State) func connection(_ connection: RemoteLogger.Connection, didReceiveEvent event: RemoteLogger.Connection.Event) } extension RemoteLogger { - final class Connection: @unchecked Sendable { - var endpoint: NWEndpoint { connection.endpoint } + package final class Connection: @unchecked Sendable { + package var endpoint: NWEndpoint { connection.endpoint } private let connection: NWConnection private var buffer = Data() private var id: UInt32 = 0 private var handlers: [UInt32: (Data?, Error?) -> Void] = [:] private let log: OSLog - weak var delegate: RemoteLoggerConnectionDelegate? + package weak var delegate: RemoteLoggerConnectionDelegate? private let queue = DispatchQueue(label: "com.github.kean.pulse.remote-logger-connection") - convenience init(endpoint: NWEndpoint, using parameters: NWParameters) { + package convenience init(endpoint: NWEndpoint, using parameters: NWParameters) { self.init(NWConnection(to: endpoint, using: parameters)) } - init(_ connection: NWConnection, delegate: RemoteLoggerConnectionDelegate? = nil) { + package init(_ connection: NWConnection, delegate: RemoteLoggerConnectionDelegate? = nil) { self.connection = connection self.delegate = delegate @@ -38,7 +38,7 @@ extension RemoteLogger { self.log = isLogEnabled ? OSLog(subsystem: "com.github.kean.pulse", category: "RemoteLogger") : .disabled } - func start() { + package func start() { connection.stateUpdateHandler = { [weak self] state in guard let self else { return } DispatchQueue.main.async { @@ -49,15 +49,15 @@ extension RemoteLogger { connection.start(queue: queue) } - enum Event { + package enum Event { case packet(Packet) case error(Error) case completed } - struct Packet { - let code: UInt8 - let body: Data + package struct Packet { + package let code: UInt8 + package let body: Data } private func receive() { @@ -130,7 +130,7 @@ extension RemoteLogger { } } - func send(code: UInt8, data: Data) { + package func send(code: UInt8, data: Data) { do { let data = try encode(code: code, body: data) connection.send(content: data, completion: .contentProcessed({ [weak self] error in @@ -147,7 +147,7 @@ extension RemoteLogger { os_log("Failed to send data: %{public}@", log: log, type: .error, "\(error)") } - func send(code: UInt8, entity: T) { + package func send(code: UInt8, entity: T) { do { let data = try JSONEncoder().encode(entity) send(code: code, data: data) @@ -156,7 +156,7 @@ extension RemoteLogger { } } - func sendMessage(path: Path, entity: T, _ completion: ((Data?, Error?) -> Void)? = nil) { + package func sendMessage(path: Path, entity: T, _ completion: ((Data?, Error?) -> Void)? = nil) { do { sendMessage(path: path, data: try JSONEncoder().encode(entity), completion) } catch { @@ -164,7 +164,7 @@ extension RemoteLogger { } } - func sendMessage(path: Path, data: Data? = nil, _ completion: ((Data?, Error?) -> Void)? = nil) { + package func sendMessage(path: Path, data: Data? = nil, _ completion: ((Data?, Error?) -> Void)? = nil) { let message = Message(id: id, options: [], path: path, data: data ?? Data()) if id == UInt32.max { @@ -191,7 +191,7 @@ extension RemoteLogger { } } - func sendResponse(for message: Message, entity: T) { + package func sendResponse(for message: Message, entity: T) { do { sendResponse(for: message, data: try JSONEncoder().encode(entity)) } catch { @@ -199,7 +199,7 @@ extension RemoteLogger { } } - func sendResponse(for message: Message, data: Data) { + package func sendResponse(for message: Message, data: Data) { let message = Message(id: message.id, options: [.response], path: message.path, data: data) do { let data = try Message.encode(message) @@ -209,7 +209,7 @@ extension RemoteLogger { } } - func cancel() { + package func cancel() { connection.cancel() } } @@ -267,7 +267,7 @@ extension RemoteLogger { } extension NWParameters { - convenience init(passcode: String) { + package convenience init(passcode: String) { let tcpOptions = NWProtocolTCP.Options() tcpOptions.enableKeepalive = true tcpOptions.keepaliveIdle = 4 diff --git a/Sources/Pulse/RemoteLogger/RemoteLogger-Protocol.swift b/Sources/Pulse/RemoteLogger/RemoteLogger-Protocol.swift index d070a0090..4d4879ef2 100644 --- a/Sources/Pulse/RemoteLogger/RemoteLogger-Protocol.swift +++ b/Sources/Pulse/RemoteLogger/RemoteLogger-Protocol.swift @@ -6,7 +6,7 @@ import Foundation import Network extension RemoteLogger { - enum PacketCode: UInt8, Equatable { + package enum PacketCode: UInt8, Equatable { // Handshake case clientHello = 0 // PacketClientHello case serverHello = 1 // ServerHelloResponse @@ -29,18 +29,18 @@ extension RemoteLogger { case message = 13 } - struct PacketClientHello: Codable { - let version: String? - let deviceId: UUID - let deviceInfo: LoggerStore.Info.DeviceInfo - let appInfo: LoggerStore.Info.AppInfo - let session: LoggerStore.Session? // Added: 3.5.7 + package struct PacketClientHello: Codable { + package let version: String? + package let deviceId: UUID + package let deviceInfo: LoggerStore.Info.DeviceInfo + package let appInfo: LoggerStore.Info.AppInfo + package let session: LoggerStore.Session? // Added: 3.5.7 } - struct Empty: Codable { + package struct Empty: Codable { } - struct PacketNetworkMessage { + package struct PacketNetworkMessage { private struct Manifest: Codable { let messageSize: UInt32 let requestBodySize: UInt32 @@ -53,7 +53,7 @@ extension RemoteLogger { } } - static func encode(_ event: LoggerStore.Event.NetworkTaskCompleted) throws -> Data { + package static func encode(_ event: LoggerStore.Event.NetworkTaskCompleted) throws -> Data { var contents = [Data]() var slimEvent = event @@ -79,7 +79,7 @@ extension RemoteLogger { return data } - static func decode(_ data: Data) throws -> LoggerStore.Event.NetworkTaskCompleted { + package static func decode(_ data: Data) throws -> LoggerStore.Event.NetworkTaskCompleted { guard data.count >= Manifest.size else { throw PacketParsingError.notEnoughData // Should never happen } @@ -113,14 +113,14 @@ extension RemoteLogger { } } - struct Message { - struct Header { - let id: UInt32 - let options: Options - let pathSize: UInt32 - let dataSize: UInt32 + package struct Message { + package struct Header { + package let id: UInt32 + package let options: Options + package let pathSize: UInt32 + package let dataSize: UInt32 - init?(_ data: Data) { + package init?(_ data: Data) { guard data.count >= headerSize else { return nil } self.id = UInt32(data.from(0, size: 4)) self.options = Options(rawValue: data[4]) @@ -129,16 +129,17 @@ extension RemoteLogger { } } - struct Options: OptionSet { - let rawValue: UInt8 + package struct Options: OptionSet { + package let rawValue: UInt8 + package init(rawValue: UInt8) { self.rawValue = rawValue } - static let response = Options(rawValue: 1 << 0) + package static let response = Options(rawValue: 1 << 0) } - let id: UInt32 - let options: Options - let path: Path - let data: Data + package let id: UInt32 + package let options: Options + package let path: Path + package let data: Data // - id (UInt32) // - options (UInt8) @@ -146,7 +147,7 @@ extension RemoteLogger { // - data size (UInt32) private static let headerSize = 13 - static func encode(_ message: Message) throws -> Data { + package static func encode(_ message: Message) throws -> Data { guard let path = try? JSONEncoder().encode(message.path) else { throw URLError(.unknown, userInfo: [:]) // Should never happen } @@ -163,7 +164,7 @@ extension RemoteLogger { return data } - static func decode(_ data: Data) throws -> Message { + package static func decode(_ data: Data) throws -> Message { guard let header = Header(data) else { throw PacketParsingError.notEnoughData // Should never happen } @@ -176,12 +177,12 @@ extension RemoteLogger { } } - enum PacketParsingError: Error { + package enum PacketParsingError: Error { case notEnoughData case unsupportedContentSize } - enum Path: Codable { + package enum Path: Codable { case updateMocks case getMockedResponse(mockID: UUID) /// Payload: ``LoggerStore/Event/MessageCreated``. @@ -190,33 +191,45 @@ extension RemoteLogger { case openTaskDetails } - struct ServerHelloResponse: Codable { - let version: String + package struct ServerHelloResponse: Codable { + package let version: String + + package init(version: String) { + self.version = version + } } } extension RemoteLogger.Connection { - func send(code: RemoteLogger.PacketCode, data: Data) { + package func send(code: RemoteLogger.PacketCode, data: Data) { send(code: code.rawValue, data: data) } - func send(code: RemoteLogger.PacketCode, entity: T) { + package func send(code: RemoteLogger.PacketCode, entity: T) { send(code: code.rawValue, entity: entity) } - func send(code: RemoteLogger.PacketCode) { + package func send(code: RemoteLogger.PacketCode) { send(code: code.rawValue, entity: RemoteLogger.Empty()) } } -struct URLSessionMock: Hashable, Codable { - let mockID: UUID - var pattern: String - var method: String? - var skip: Int? - var count: Int? +package struct URLSessionMock: Hashable, Codable { + package let mockID: UUID + package var pattern: String + package var method: String? + package var skip: Int? + package var count: Int? + + package init(mockID: UUID, pattern: String, method: String? = nil, skip: Int? = nil, count: Int? = nil) { + self.mockID = mockID + self.pattern = pattern + self.method = method + self.skip = skip + self.count = count + } - func isMatch(_ request: URLRequest) -> Bool { + package func isMatch(_ request: URLRequest) -> Bool { if let lhs = request.httpMethod, let rhs = method, lhs.uppercased() != rhs.uppercased() { return false @@ -227,7 +240,7 @@ struct URLSessionMock: Hashable, Codable { return isMatch(url) } - func isMatch(_ url: String) -> Bool { + package func isMatch(_ url: String) -> Bool { guard let regex = try? Regex(pattern, [.caseInsensitive]) else { return false } @@ -235,11 +248,18 @@ struct URLSessionMock: Hashable, Codable { } } -struct URLSessionMockedResponse: Codable { - let errorCode: Int? - let statusCode: Int? - let headers: [String: String]? - var body: String? +package struct URLSessionMockedResponse: Codable { + package let errorCode: Int? + package let statusCode: Int? + package let headers: [String: String]? + package var body: String? + + package init(errorCode: Int?, statusCode: Int?, headers: [String : String]?, body: String? = nil) { + self.errorCode = errorCode + self.statusCode = statusCode + self.headers = headers + self.body = body + } } // MARK: - Helpers (Binary Protocol) diff --git a/Sources/Pulse/RemoteLogger/RemoteLogger.swift b/Sources/Pulse/RemoteLogger/RemoteLogger.swift index 524c46260..4072fc2f3 100644 --- a/Sources/Pulse/RemoteLogger/RemoteLogger.swift +++ b/Sources/Pulse/RemoteLogger/RemoteLogger.swift @@ -388,7 +388,7 @@ public final class RemoteLogger: ObservableObject, RemoteLoggerConnectionDelegat // MARK: RemoteLoggerConnectionDelegate - func connection(_ connection: Connection, didChangeState newState: NWConnection.State) { + package func connection(_ connection: Connection, didChangeState newState: NWConnection.State) { os_log("Connection did change state to %{public}@", log: log, "\(newState)") switch newState { @@ -407,7 +407,7 @@ public final class RemoteLogger: ObservableObject, RemoteLoggerConnectionDelegat } } - func connection(_ connection: Connection, didReceiveEvent event: Connection.Event) { + package func connection(_ connection: Connection, didReceiveEvent event: Connection.Event) { switch event { case .packet(let packet): do { @@ -743,5 +743,5 @@ extension RemoteLogger.ConnectionState { } extension RemoteLogger { - static let serviceType = "_pulse._tcp" + package static let serviceType = "_pulse._tcp" } diff --git a/Sources/Pulse/URLSessionProxy/URLSessionProxyDelegate.swift b/Sources/Pulse/URLSessionProxy/URLSessionProxyDelegate.swift index c859ae71f..36cfb68f9 100644 --- a/Sources/Pulse/URLSessionProxy/URLSessionProxyDelegate.swift +++ b/Sources/Pulse/URLSessionProxy/URLSessionProxyDelegate.swift @@ -6,7 +6,7 @@ import Foundation /// Automates URLSession request tracking. /// -/// - important: On iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, it automatically +/// - important: On iOS 16, tvOS 16, macOS 13, watchOS 9.0, it automatically /// tracks new task creation using the `urlSession(_:didCreateTask:)` delegate /// method which allows the logger to start tracking network requests right /// after their creation. On earlier versions, you can (optionally) call @@ -33,7 +33,7 @@ public final class URLSessionProxyDelegate: NSObject, URLSessionTaskDelegate, UR #selector(URLSessionDownloadDelegate.urlSession(_:downloadTask:didFinishDownloadingTo:)), #selector(URLSessionDownloadDelegate.urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)) ] - if #available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, *) { + if #available(iOS 16, tvOS 16, macOS 13, watchOS 9, *) { interceptedSelectors.insert(#selector(URLSessionTaskDelegate.urlSession(_:didCreateTask:))) } self.interceptedSelectors = interceptedSelectors @@ -46,7 +46,7 @@ public final class URLSessionProxyDelegate: NSObject, URLSessionTaskDelegate, UR public func urlSession(_ session: Foundation.URLSession, didCreateTask task: URLSessionTask) { createdTask.value = task logger.logTaskCreated(task) - if #available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, *) { + if #available(iOS 16, tvOS 16, macOS 13, watchOS 9, *) { taskDelegate?.urlSession?(session, didCreateTask: task) } } diff --git a/Sources/PulseUI/Extensions/Foundation+Extensions.swift b/Sources/PulseUI/Extensions/Foundation+Extensions.swift index cb4cd2be4..57c559c1e 100644 --- a/Sources/PulseUI/Extensions/Foundation+Extensions.swift +++ b/Sources/PulseUI/Extensions/Foundation+Extensions.swift @@ -8,7 +8,7 @@ import CoreData import Combine extension Character { - init?(_ code: unichar) { + package init?(_ code: unichar) { guard let scalar = UnicodeScalar(code) else { return nil } @@ -16,15 +16,15 @@ extension Character { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) extension AttributedString { - init(_ string: String, _ configure: (inout AttributeContainer) -> Void) { + package init(_ string: String, _ configure: (inout AttributeContainer) -> Void) { var attributes = AttributeContainer() configure(&attributes) self.init(string, attributes: attributes) } - mutating func append(_ string: String, _ configure: (inout AttributeContainer) -> Void) { + package mutating func append(_ string: String, _ configure: (inout AttributeContainer) -> Void) { var attributes = AttributeContainer() configure(&attributes) self.append(AttributedString(string, attributes: attributes)) @@ -32,13 +32,13 @@ extension AttributedString { } extension NSManagedObject { - func reset() { + package func reset() { managedObjectContext?.refresh(self, mergeChanges: false) } } extension NSManagedObjectContext { - func getDistinctValues(entityName: String, property: String) -> Set { + package func getDistinctValues(entityName: String, property: String) -> Set { let request = NSFetchRequest(entityName: entityName) request.resultType = .dictionaryResultType request.returnsDistinctResults = true @@ -51,7 +51,7 @@ extension NSManagedObjectContext { } extension tls_ciphersuite_t { - var description: String { + package var description: String { switch self { case .RSA_WITH_3DES_EDE_CBC_SHA: return "RSA_WITH_3DES_EDE_CBC_SHA" case .RSA_WITH_AES_128_CBC_SHA: return "RSA_WITH_AES_128_CBC_SHA" @@ -85,7 +85,7 @@ extension tls_ciphersuite_t { } extension tls_protocol_version_t { - var description: String { + package var description: String { switch self { case .TLSv10: return "TLS 1.0" case .TLSv11: return "TLS 1.1" @@ -98,7 +98,7 @@ extension tls_protocol_version_t { } } -func descriptionForURLErrorCode(_ code: Int) -> String { +package func descriptionForURLErrorCode(_ code: Int) -> String { switch code { case NSURLErrorUnknown: return "Unknown" case NSURLErrorCancelled: return "Cancelled" @@ -154,7 +154,7 @@ func descriptionForURLErrorCode(_ code: Int) -> String { } extension URLRequest.CachePolicy { - var description: String { + package var description: String { switch self { case .useProtocolCachePolicy: return "useProtocolCachePolicy" case .reloadIgnoringLocalCacheData: return "reloadIgnoringLocalCacheData" diff --git a/Sources/PulseUI/Extensions/NSAttributedString+Extensions.swift b/Sources/PulseUI/Extensions/NSAttributedString+Extensions.swift index de67f668f..27b9ec8e6 100644 --- a/Sources/PulseUI/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/PulseUI/Extensions/NSAttributedString+Extensions.swift @@ -5,11 +5,11 @@ import Foundation extension NSMutableAttributedString { - func append(_ string: String, _ attributes: [NSAttributedString.Key: Any] = [:]) { + package func append(_ string: String, _ attributes: [NSAttributedString.Key: Any] = [:]) { append(NSAttributedString(string: string, attributes: attributes)) } - func addAttributes(_ attributes: [NSAttributedString.Key: Any]) { + package func addAttributes(_ attributes: [NSAttributedString.Key: Any]) { addAttributes(attributes, range: NSRange(location: 0, length: string.count)) } } diff --git a/Sources/PulseUI/Extensions/Pulse+Extensions.swift b/Sources/PulseUI/Extensions/Pulse+Extensions.swift index ac31e4da5..26ecc4a3a 100644 --- a/Sources/PulseUI/Extensions/Pulse+Extensions.swift +++ b/Sources/PulseUI/Extensions/Pulse+Extensions.swift @@ -7,13 +7,13 @@ import Pulse import SwiftUI import CoreData -enum LoggerEntity { +package enum LoggerEntity { /// Regular log, not task attached. case message(LoggerMessageEntity) /// Either a log with an attached task, or a task itself. case task(NetworkTaskEntity) - init(_ entity: NSManagedObject) { + package init(_ entity: NSManagedObject) { if let message = entity as? LoggerMessageEntity { if let task = message.task { self = .task(task) @@ -27,20 +27,20 @@ enum LoggerEntity { } } - var task: NetworkTaskEntity? { + package var task: NetworkTaskEntity? { if case .task(let task) = self { return task } return nil } } extension LoggerMessageEntity { - var logLevel: LoggerStore.Level { + package var logLevel: LoggerStore.Level { LoggerStore.Level(rawValue: level) ?? .debug } } extension NetworkTaskEntity.State { - var tintColor: Color { + package var tintColor: Color { switch self { case .pending: return .orange case .success: return .green @@ -48,7 +48,7 @@ extension NetworkTaskEntity.State { } } - var iconSystemName: String { + package var iconSystemName: String { switch self { case .pending: return "clock.fill" case .success: return "checkmark.circle.fill" @@ -58,15 +58,15 @@ extension NetworkTaskEntity.State { } extension LoggerSessionEntity { - var formattedDate: String { + package var formattedDate: String { formattedDate(isCompact: false) } - var searchTags: [String] { + package var searchTags: [String] { possibleFormatters.map { $0.string(from: createdAt) } } - func formattedDate(isCompact: Bool = false) -> String { + package func formattedDate(isCompact: Bool = false) -> String { if isCompact { return compactDateFormatter.string(from: createdAt) } else { @@ -74,7 +74,7 @@ extension LoggerSessionEntity { } } - var fullVersion: String? { + package var fullVersion: String? { guard let version = version else { return nil } @@ -102,7 +102,7 @@ private let possibleFormatters: [DateFormatter] = [ #if !os(watchOS) extension NetworkTaskEntity { - func cURLDescription() -> String { + package func cURLDescription() -> String { guard let request = currentRequest ?? originalRequest, let url = request.url else { return "$ curl command generation failed" @@ -133,17 +133,81 @@ extension NetworkTaskEntity { #endif extension NetworkTaskEntity { - func getFormattedContent(options: ConsoleListDisplaySettings) -> String? { + package struct InfoItem: Identifiable { + package let id = UUID() + package let field: ConsoleListDisplaySettings.TaskField + package let value: String + + package var title: String { + switch field { + case .method: return "HTTP Method" + case .requestSize: return "Request Size" + case .responseSize: return "Response Size" + case .responseContentType: return "Response Content Type" + case .duration: return "Duration" + case .host: return "Host" + case .statusCode: return "Status Code" + case .taskType: return "Task Type" + case .taskDescription: return "Task Description" + case .requestHeaderField(let name): return name + case .responseHeaderField(let name): return name + } + } + } + + package func makeInfoItem(for field: ConsoleListDisplaySettings.TaskField) -> InfoItem? { + guard let value = makeInfoText(for: field) else { return nil } + return InfoItem(field: field, value: value) + } + + package func makeInfoText(for field: ConsoleListDisplaySettings.TaskField) -> String? { + switch field { + case .method: + httpMethod + case .requestSize: + byteCount(for: requestBodySize) + case .responseSize: + byteCount(for: responseBodySize) + case .responseContentType: + responseContentType.map(NetworkLogger.ContentType.init)?.lastComponent.uppercased() + case .duration: + ConsoleFormatter.duration(for: self) + case .host: + host + case .statusCode: + statusCode != 0 ? statusCode.description : nil + case .taskType: + NetworkLogger.TaskType(rawValue: taskType)?.urlSessionTaskClassName + case .taskDescription: + taskDescription + case .requestHeaderField(let key): + (currentRequest?.headers ?? [:])[key] + case .responseHeaderField(let key): + (response?.headers ?? [:])[key] + } + } + + package func getShortTitle(options: ConsoleListDisplaySettings) -> String { if options.content.showTaskDescription, let taskDescription, !taskDescription.isEmpty { return taskDescription } + guard let url else { + return "" + } + return URL(string: url)?.lastPathComponent ?? url + } + + package func getFormattedContent(settings: ConsoleListDisplaySettings.ContentSettings) -> String? { + if settings.showTaskDescription, let taskDescription, !taskDescription.isEmpty { + return taskDescription + } guard let url else { return nil } - return NetworkTaskEntity.formattedURL(url, components: options.content.components) + return NetworkTaskEntity.formattedURL(url, components: settings.components) } - static func formattedURL(_ url: String, components displayed: Set) -> String? { + package static func formattedURL(_ url: String, components displayed: Set) -> String? { guard !displayed.isEmpty else { return nil } @@ -173,3 +237,8 @@ extension NetworkTaskEntity { return string } } + +private func byteCount(for size: Int64) -> String { + guard size > 0 else { return "0 KB" } + return ByteCountFormatter.string(fromByteCount: size) +} diff --git a/Sources/PulseUI/Extensions/SwiftUI+Extensions.swift b/Sources/PulseUI/Extensions/SwiftUI+Extensions.swift index 555c5c863..c4670a132 100644 --- a/Sources/PulseUI/Extensions/SwiftUI+Extensions.swift +++ b/Sources/PulseUI/Extensions/SwiftUI+Extensions.swift @@ -20,31 +20,11 @@ extension Color { #endif extension View { - func invisible() -> some View { + package func invisible() -> some View { self.hidden().accessibilityHidden(true) } } -extension ContentSizeCategory { - var scale: CGFloat { - switch self { - case .extraSmall: return 0.7 - case .small: return 0.8 - case .medium: return 1.0 - case .large: return 1.0 - case .extraLarge: return 1.0 - case .extraExtraLarge: return 1.2 - case .extraExtraExtraLarge: return 1.3 - case .accessibilityMedium: return 1.4 - case .accessibilityLarge: return 1.6 - case .accessibilityExtraLarge: return 1.9 - case .accessibilityExtraExtraLarge: return 2.1 - case .accessibilityExtraExtraExtraLarge: return 2.4 - @unknown default: return 1.0 - } - } -} - #if os(iOS) || os(visionOS) enum Keyboard { @@ -66,42 +46,16 @@ enum Keyboard { // MARK: - Backport -struct Backport { - let content: Content +package struct Backport { + package let content: Content } extension View { - var backport: Backport { Backport(content: self) } -} - -extension Backport { - @ViewBuilder - func presentationDetents(_ detents: Set) -> some View { -#if os(iOS) || os(visionOS) - if #available(iOS 16, *) { - let detents = detents.map { (detent) -> SwiftUI.PresentationDetent in - switch detent { - case .large: return .large - case .medium: return .medium - } - } - self.content.presentationDetents(Set(detents)) - } else { - self.content - } -#else - self.content -#endif - } - - enum PresentationDetent { - case large - case medium - } + package var backport: Backport { Backport(content: self) } } extension View { - func inlineNavigationTitle(_ title: String) -> some View { + package func inlineNavigationTitle(_ title: String) -> some View { self.navigationTitle(title) #if os(iOS) || os(visionOS) .navigationBarTitleDisplayMode(.inline) @@ -110,7 +64,7 @@ extension View { } /// Allows you to use `@StateObject` only for memory management (without observing). -final class IgnoringUpdates: ObservableObject { - var value: T - init(_ value: T) { self.value = value } +package final class IgnoringUpdates: ObservableObject { + package var value: T + package init(_ value: T) { self.value = value } } diff --git a/Sources/PulseUI/Features/Console/ConsoleDataSource.swift b/Sources/PulseUI/Features/Console/ConsoleDataSource.swift index 2d8c9a9a1..06afeea4d 100644 --- a/Sources/PulseUI/Features/Console/ConsoleDataSource.swift +++ b/Sources/PulseUI/Features/Console/ConsoleDataSource.swift @@ -183,7 +183,7 @@ private final class ConsoleFetchDelegate: NSObject, NSFetchedResultsControllerDe } } -enum ConsoleUpdateEvent { +package enum ConsoleUpdateEvent { /// Full refresh of data. case refresh /// Incremental update. diff --git a/Sources/PulseUI/Features/Console/ConsoleDelegate.swift b/Sources/PulseUI/Features/Console/ConsoleDelegate.swift deleted file mode 100644 index b174ba90d..000000000 --- a/Sources/PulseUI/Features/Console/ConsoleDelegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import Pulse - -enum ConsoleViewDelegate { - static func getTitle(for task: NetworkTaskEntity) -> String? { - if let taskDescription = task.taskDescription, !taskDescription.isEmpty { - return taskDescription - } - return task.url - } - - static func getShortTitle(for task: NetworkTaskEntity) -> String { - guard let title = getTitle(for: task) else { - return "" - } - return URL(string: title)?.lastPathComponent ?? title - } -} diff --git a/Sources/PulseUI/Features/Console/ConsoleEnvironment.swift b/Sources/PulseUI/Features/Console/ConsoleEnvironment.swift index eea266466..71b38f094 100644 --- a/Sources/PulseUI/Features/Console/ConsoleEnvironment.swift +++ b/Sources/PulseUI/Features/Console/ConsoleEnvironment.swift @@ -117,8 +117,8 @@ public enum ConsoleMode: String { /// Displays only network tasks. case network - var hasLogs: Bool { self == .all || self == .logs } - var hasNetwork: Bool { self == .all || self == .network } + package var hasLogs: Bool { self == .all || self == .logs } + package var hasNetwork: Bool { self == .all || self == .network } } // MARK: Environment @@ -132,12 +132,12 @@ private struct ConsoleRouterKey: EnvironmentKey { } extension EnvironmentValues { - var store: LoggerStore { + package var store: LoggerStore { get { self[LoggerStoreKey.self] } set { self[LoggerStoreKey.self] = newValue } } - var router: ConsoleRouter { + package var router: ConsoleRouter { get { self[ConsoleRouterKey.self] } set { self[ConsoleRouterKey.self] = newValue } } diff --git a/Sources/PulseUI/Features/Console/ConsoleView-ios.swift b/Sources/PulseUI/Features/Console/ConsoleView-ios.swift index 635cecdc4..6313c99d4 100644 --- a/Sources/PulseUI/Features/Console/ConsoleView-ios.swift +++ b/Sources/PulseUI/Features/Console/ConsoleView-ios.swift @@ -20,14 +20,14 @@ public struct ConsoleView: View { } public var body: some View { - if #available(iOS 15, *) { + if #available(iOS 16, *) { contents } else { - PlaceholderView(imageName: "xmark.octagon", title: "Unsupported", subtitle: "Pulse requires iOS 15 or later").padding() + PlaceholderView(imageName: "xmark.octagon", title: "Unsupported", subtitle: "Pulse requires iOS 16 or later").padding() } } - @available(iOS 15, visionOS 1.0, *) + @available(iOS 16, visionOS 1, *) private var contents: some View { ConsoleListView() .navigationTitle(environment.title) @@ -53,7 +53,7 @@ public struct ConsoleView: View { return copy } - @available(iOS 15, visionOS 1.0, *) + @available(iOS 16, visionOS 1, *) @ViewBuilder private var trailingNavigationBarItems: some View { Button(action: { environment.router.isShowingShareStore = true }) { Image(systemName: "square.and.arrow.up") diff --git a/Sources/PulseUI/Features/Console/List/ConsoleListContentView.swift b/Sources/PulseUI/Features/Console/List/ConsoleListContentView.swift index 836f10bac..abd975472 100644 --- a/Sources/PulseUI/Features/Console/List/ConsoleListContentView.swift +++ b/Sources/PulseUI/Features/Console/List/ConsoleListContentView.swift @@ -9,7 +9,7 @@ import Pulse import Combine import SwiftUI -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleListContentView: View { @EnvironmentObject var viewModel: ConsoleListViewModel @@ -63,13 +63,17 @@ struct ConsoleListContentView: View { } #if os(iOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, *) -struct ConsoleStaticList: View { - let entities: [NSManagedObject] +@available(iOS 16, visionOS 1, *) +package struct ConsoleStaticList: View { + package let entities: [NSManagedObject] - var body: some View { + package init(entities: [NSManagedObject]) { + self.entities = entities + } + + package var body: some View { List { - ForEach(entities, id: \.objectID, content: Components.makeConsoleEntityCell) + ForEach(entities, id: \.objectID, content: ConsoleEntityCell.init) } .listStyle(.plain) #if os(iOS) || os(visionOS) diff --git a/Sources/PulseUI/Features/Console/List/ConsoleListView.swift b/Sources/PulseUI/Features/Console/List/ConsoleListView.swift index 28fea36d1..47a274e55 100644 --- a/Sources/PulseUI/Features/Console/List/ConsoleListView.swift +++ b/Sources/PulseUI/Features/Console/List/ConsoleListView.swift @@ -9,7 +9,7 @@ import CoreData import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleListView: View { @EnvironmentObject var environment: ConsoleEnvironment @EnvironmentObject var filters: ConsoleFiltersViewModel @@ -19,7 +19,7 @@ struct ConsoleListView: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) private struct _InternalConsoleListView: View { private let environment: ConsoleEnvironment @@ -58,7 +58,7 @@ private struct _InternalConsoleListView: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) private struct _ConsoleListView: View { @Environment(\.isSearching) private var isSearching @Environment(\.store) private var store diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleContextMenu.swift b/Sources/PulseUI/Features/Console/Views/ConsoleContextMenu.swift index 88e4fce8c..e39343ba5 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleContextMenu.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleContextMenu.swift @@ -9,7 +9,7 @@ import CoreData import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleContextMenu: View { @EnvironmentObject private var environment: ConsoleEnvironment @Environment(\.router) private var router diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift b/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift index 6db0e747a..1a16e5bea 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift @@ -9,7 +9,7 @@ import SwiftUI import Pulse import CoreData -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) struct ConsoleEntityCell: View { let entity: NSManagedObject @@ -23,7 +23,7 @@ struct ConsoleEntityCell: View { } } -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) private struct _ConsoleMessageCell: View { let message: LoggerMessageEntity @@ -59,7 +59,7 @@ private struct _ConsoleMessageCell: View { } } -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) private struct _ConsoleTaskCell: View { let task: NetworkTaskEntity @State private var shareItems: ShareItems? diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleHelperViews.swift b/Sources/PulseUI/Features/Console/Views/ConsoleHelperViews.swift index 22e8ff61e..d1959dfec 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleHelperViews.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleHelperViews.swift @@ -22,7 +22,7 @@ struct ConsoleTimestampView: View { } } -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) struct MockBadgeView: View { var body: some View { Text("MOCK") @@ -43,7 +43,7 @@ struct StatusIndicatorView: View { #if os(tvOS) .font(.system(size: 12)) #else - .font(.system(size: 10)) + .font(.system(size: 9)) #endif .clipShape(RoundedRectangle(cornerRadius: 3)) } diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleListDisplaySettings.swift b/Sources/PulseUI/Features/Console/Views/ConsoleListDisplaySettings.swift index 62dfd549f..6598bb371 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleListDisplaySettings.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleListDisplaySettings.swift @@ -18,18 +18,27 @@ public struct ConsoleListDisplaySettings: Hashable, Codable { /// Specifies what is displayed in the network task cell header. public struct HeaderSettings: Hashable, Codable { - public var fontSize: Int = defaultHeaderFooterFontSize + public var fontSize: Int /// The line limit for messages in the console. By default, `1`. - public var lineLimit: Int = 1 - - #if os(macOS) || os(tvOS) - /// Fields to display below the main text label. - public var fields: [TaskField] = [.responseSize, .duration, .host] - #else - /// Fields to display below the main text label. - public var fields: [TaskField] = [.responseSize, .duration] - #endif + public var lineLimit: Int + + /// Additinoal fields to display below the in the header. + public var fields: [TaskField] + + public init(fontSize: Int? = nil, lineLimit: Int = 1, fields: [TaskField]? = nil) { + self.fontSize = fontSize ?? ConsoleListDisplaySettings.defaultHeaderFooterFontSize + self.lineLimit = lineLimit + if let fields { + self.fields = fields + } else { +#if os(macOS) || os(tvOS) + self.fields = [.responseSize, .duration, .host] +#else + self.fields = [.responseSize, .duration] +#endif + } + } } /// Specifies how the main content of the is displaying, including the @@ -37,29 +46,61 @@ public struct ConsoleListDisplaySettings: Hashable, Codable { public struct ContentSettings: Hashable, Codable { /// If task description is available, show it instead of the `URL`. - public var showTaskDescription = false + public var showTaskDescription: Bool /// Show HTTP method when available. - public var showMethod = true + public var showMethod: Bool /// Defines what components to display in the list. By default, shows /// only path. - public var components: Set = [.path] + public var components: Set /// The default value is different based on the platform but typically /// matches the "body" font size. - public var fontSize: Int = defaultContentFontSize + public var fontSize: Int /// The line limit for messages in the console. By default, `3`. - public var lineLimit: Int = 3 + public var lineLimit: Int + + /// If enabled, use monospaced font to display the content. + public var isMonospaced = false + + public init( + showTaskDescription: Bool = false, + showMethod: Bool = true, + components: Set = [.path], + fontSize: Int? = nil, + lineLimit: Int = 3 + ) { + self.showTaskDescription = showTaskDescription + self.showMethod = showMethod + self.components = components + self.fontSize = fontSize ?? ConsoleListDisplaySettings.defaultContentFontSize + self.lineLimit = lineLimit + } } public struct FooterSettings: Sendable, Hashable, Codable { /// By default, matches the "footnote" style. - public var fontSize: Int = defaultHeaderFooterFontSize + public var fontSize: Int /// The line limit for messages in the console. By default, `1`. - public var lineLimit: Int = 1 + public var lineLimit: Int + + /// Fields to display horizonally below the main text label with a separator. + public var fields: [TaskField] + + /// Additional fields to display below the main list. + public var additionalFields: [TaskField] = [] + + /// If enabled, use monospaced font to display the content. + public var isMonospaced = false + + public init(fontSize: Int? = nil, lineLimit: Int = 1) { + self.fontSize = fontSize ?? ConsoleListDisplaySettings.defaultHeaderFooterFontSize + self.lineLimit = lineLimit + self.fields = [.host] + } } public enum URLComponent: String, CaseIterable, Codable { @@ -83,16 +124,18 @@ public struct ConsoleListDisplaySettings: Hashable, Codable { public init() {} } +extension ConsoleListDisplaySettings { #if os(watchOS) -let defaultContentFontSize = 17 -let defaultHeaderFooterFontSize = 14 + package static let defaultContentFontSize = 16 + package static let defaultHeaderFooterFontSize = 14 #elseif os(macOS) -let defaultContentFontSize = 13 -let defaultHeaderFooterFontSize = 11 + package static let defaultContentFontSize = 13 + package static let defaultHeaderFooterFontSize = 11 #elseif os(iOS) || os(visionOS) -let defaultContentFontSize = 17 -let defaultHeaderFooterFontSize = 13 + package static let defaultContentFontSize = 16 + package static let defaultHeaderFooterFontSize = 13 #elseif os(tvOS) -let defaultContentFontSize = 27 -let defaultHeaderFooterFontSize = 21 + package static let defaultContentFontSize = 27 + package static let defaultHeaderFooterFontSize = 21 #endif +} diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift b/Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift index 6697f7da7..ec9b18102 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleMessageCell.swift @@ -7,15 +7,20 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1, *) -struct ConsoleMessageCell: View { - let message: LoggerMessageEntity - var isDisclosureNeeded = false +@available(iOS 16, visionOS 1, *) +package struct ConsoleMessageCell: View { + package let message: LoggerMessageEntity + package var isDisclosureNeeded = false @ScaledMetric(relativeTo: .body) private var fontMultiplier = 17.0 @ObservedObject private var settings: UserSettings = .shared - var body: some View { + package init(message: LoggerMessageEntity, isDisclosureNeeded: Bool = false) { + self.message = message + self.isDisclosureNeeded = isDisclosureNeeded + } + + package var body: some View { let contents = VStack(alignment: .leading, spacing: 4) { header.dynamicTypeSize(...DynamicTypeSize.xxxLarge) Text(message.text) @@ -39,7 +44,9 @@ struct ConsoleMessageCell: View { .foregroundColor(titleColor) Spacer() #if !os(watchOS) - Components.makePinView(for: message) +#if canImport(RiftSupport) + PinView(message: message) +#endif ConsoleTimestampView(date: message.createdAt) .padding(.trailing, 3) #endif @@ -94,7 +101,7 @@ struct ListDisclosureIndicator: View { } extension UXColor { - static func textColor(for level: LoggerStore.Level) -> UXColor { + package static func textColor(for level: LoggerStore.Level) -> UXColor { switch level { case .trace: return .secondaryLabel case .debug, .info: return .label @@ -105,7 +112,7 @@ extension UXColor { } extension Color { - static func textColor(for level: LoggerStore.Level) -> Color { + package static func textColor(for level: LoggerStore.Level) -> Color { switch level { case .trace: return .secondary case .debug, .info: return .primary @@ -116,7 +123,7 @@ extension Color { } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleMessageCell_Previews: PreviewProvider { static var previews: some View { ConsoleMessageCell(message: try! LoggerStore.mock.messages()[0]) diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleRouterView.swift b/Sources/PulseUI/Features/Console/Views/ConsoleRouterView.swift index c70926849..5f59824d1 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleRouterView.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleRouterView.swift @@ -7,19 +7,21 @@ import CoreData import Pulse import Combine -final class ConsoleRouter: ObservableObject { +package final class ConsoleRouter: ObservableObject { #if os(macOS) - @Published var selection: ConsoleSelectedItem? + @Published package var selection: ConsoleSelectedItem? #endif - @Published var shareItems: ShareItems? - @Published var isShowingFilters = false - @Published var isShowingSettings = false - @Published var isShowingSessions = false - @Published var isShowingShareStore = false + @Published package var shareItems: ShareItems? + @Published package var isShowingFilters = false + @Published package var isShowingSettings = false + @Published package var isShowingSessions = false + @Published package var isShowingShareStore = false + + package init() {} } #if os(macOS) -enum ConsoleSelectedItem: Hashable { +package enum ConsoleSelectedItem: Hashable { case entity(NSManagedObjectID) case occurrence(NSManagedObjectID, ConsoleSearchOccurrence) } @@ -30,14 +32,14 @@ struct ConsoleRouterView: View { @EnvironmentObject var router: ConsoleRouter var body: some View { - if #available(iOS 15, macOS 13, *) { + if #available(iOS 16, macOS 13, *) { contents } } } #if os(iOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) extension ConsoleRouterView { var contents: some View { Text("").invisible() @@ -89,7 +91,7 @@ extension ConsoleRouterView { private var destinationShareStore: some View { NavigationView { ShareStoreView(onDismiss: { router.isShowingShareStore = false }) - }.backport.presentationDetents([.medium, .large]) + }.presentationDetents([.medium, .large]) } } diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift b/Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift index bccfc0497..a39dac0b4 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleTaskCell.swift @@ -7,8 +7,8 @@ import Pulse import Combine import CoreData -@available(iOS 15, visionOS 1, *) -struct ConsoleTaskCell: View { +@available(iOS 16, visionOS 1, *) +package struct ConsoleTaskCell: View { @ObservedObject var task: NetworkTaskEntity var isDisclosureNeeded = false @@ -16,19 +16,26 @@ struct ConsoleTaskCell: View { @ObservedObject private var settings: UserSettings = .shared @Environment(\.store) private var store: LoggerStore - enum EditableArea { - case header, timestamp, content, footer + package enum EditableArea { + case header, content, footer } - var highlightedArea: EditableArea? + package var highlightedArea: EditableArea? - var body: some View { + package init(task: NetworkTaskEntity, isDisclosureNeeded: Bool = false, highlightedArea: EditableArea? = nil) { + self.task = task + self.isDisclosureNeeded = isDisclosureNeeded + self.highlightedArea = highlightedArea + } + + package var body: some View { VStack(alignment: .leading, spacing: 4) { header - content.higlighted(highlightedArea == .content) - + makeContent(settings: settings.listDisplayOptions.content) + .higlighted(highlightedArea == .content) #if os(iOS) || os(watchOS) - footer.higlighted(highlightedArea == .footer) + makeFooter(settings: settings.listDisplayOptions.footer) + .higlighted(highlightedArea == .footer) #endif } } @@ -51,8 +58,10 @@ struct ConsoleTaskCell: View { info.higlighted(highlightedArea == .header) Spacer() +#if canImport(RiftSupport) + PinView(task: task) +#endif ConsoleTimestampView(date: task.createdAt) - .higlighted(highlightedArea == .timestamp) .padding(.trailing, 3) } .overlay(alignment: .leading) { @@ -60,7 +69,7 @@ struct ConsoleTaskCell: View { #if os(tvOS) .offset(x: -20) #else - .offset(x: -15) + .offset(x: -14) #endif } .overlay(alignment: .trailing) { @@ -100,19 +109,20 @@ struct ConsoleTaskCell: View { // MARK: – Content - private var content: some View { + private func makeContent(settings: ConsoleListDisplaySettings.ContentSettings) -> some View { + let design: Font.Design? = settings.isMonospaced ? .monospaced : nil var method: Text? { - guard settings.listDisplayOptions.content.showMethod, let method = task.httpMethod else { + guard settings.showMethod, let method = task.httpMethod else { return nil } return Text(method.appending(" ")) - .font(makeFont(size: settings.listDisplayOptions.content.fontSize).weight(.medium).smallCaps()) - .tracking(-0.3) + .font(makeFont(size: settings.fontSize, design: design).weight(.medium).smallCaps()) + .tracking(-0.2) } var main: Text { - Text(task.getFormattedContent(options: settings.listDisplayOptions) ?? "–") - .font(makeFont(size: settings.listDisplayOptions.content.fontSize)) + Text(task.getFormattedContent(settings: settings) ?? "–") + .font(makeFont(size: settings.fontSize, design: design)) } var text: Text { @@ -124,75 +134,65 @@ struct ConsoleTaskCell: View { } return text - .lineLimit(settings.listDisplayOptions.content.lineLimit) + .lineLimit(settings.lineLimit) } // MARK: – Footer @ViewBuilder - private var footer: some View { - if let host = task.host, !host.isEmpty { - Text(host) - .lineLimit(settings.listDisplayOptions.footer.lineLimit) - .font(makeFont(size: settings.listDisplayOptions.footer.fontSize)) + private func makeFooter(settings: ConsoleListDisplaySettings.FooterSettings) -> some View { + let design: Font.Design? = settings.isMonospaced ? .monospaced : nil + let fields = settings.fields.compactMap(task.makeInfoText) + if !fields.isEmpty { + Text(fields.joined(separator: " · ")) + .lineLimit(settings.lineLimit) + .font(makeFont(size: settings.fontSize, design: design)) .foregroundStyle(.secondary) } + let additional = settings.additionalFields.compactMap(task.makeInfoItem) + if !additional.isEmpty { + Divider().opacity(0.5).padding(.vertical, 2) + VStack(alignment: .leading, spacing: 4) { + ForEach(additional) { field in + (Text(field.title + ": ").fontWeight(.medium) + Text(field.value)) + .lineLimit(settings.lineLimit) + .font(makeFont(size: settings.fontSize, design: design)) + .foregroundStyle(.secondary) + } + } + } } // MARK: - Helpers - private func makeFont(size: Int) -> Font { - Font.system(size: CGFloat(size) * fontMultiplier) + private func makeFont(size: Int, weight: Font.Weight? = nil, design: Font.Design? = nil) -> Font { + Font.system(size: CGFloat(design == .monospaced ? size - 1 : size) * fontMultiplier, weight: weight, design: design) } } -private extension NetworkTaskEntity { - func makeInfoText(for field: ConsoleListDisplaySettings.TaskField) -> String? { - switch field { - case .method: - httpMethod - case .requestSize: - byteCount(for: requestBodySize) - case .responseSize: - byteCount(for: responseBodySize) - case .responseContentType: - responseContentType.map(NetworkLogger.ContentType.init)?.lastComponent.uppercased() - case .duration: - ConsoleFormatter.duration(for: self) - case .host: - host - case .statusCode: - statusCode != 0 ? statusCode.description : nil - case .taskType: - NetworkLogger.TaskType(rawValue: taskType)?.urlSessionTaskClassName - case .taskDescription: - taskDescription - case .requestHeaderField(let key): - (currentRequest?.headers ?? [:])[key] - case .responseHeaderField(let key): - (response?.headers ?? [:])[key] - } - } - - private func byteCount(for size: Int64) -> String { - guard size > 0 else { return "0 KB" } - return ByteCountFormatter.string(fromByteCount: size) - } -} +#if canImport(RiftSupport) +import RiftSupport private extension View { @ViewBuilder func higlighted(_ isHighlighted: Bool) -> some View { if isHighlighted { - self.modifier(Components.makeHighlightModifier()) + self.highlighted() } else { self } } } +#else +private extension View { + func higlighted(_ isHighlighted: Bool) -> some View { + self + } +} +#endif #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleTaskCell_Previews: PreviewProvider { static var previews: some View { ConsoleTaskCell(task: LoggerStore.preview.entity(for: .login)) diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleToolbarView.swift b/Sources/PulseUI/Features/Console/Views/ConsoleToolbarView.swift index e3b9d4314..2f5460fc8 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleToolbarView.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleToolbarView.swift @@ -9,20 +9,16 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleToolbarView: View { @EnvironmentObject private var environment: ConsoleEnvironment var body: some View { - if #available(iOS 16.0, *) { - ViewThatFits { - horizontal - vertical - } - .dynamicTypeSize(...DynamicTypeSize.accessibility2) - } else { + ViewThatFits { horizontal + vertical } + .dynamicTypeSize(...DynamicTypeSize.accessibility2) } private var horizontal: some View { @@ -100,13 +96,20 @@ private struct ConsoleToolbarTitle: View { } } -struct ConsoleModeButton: View { - let title: String - var details: String? - let isSelected: Bool - let action: () -> Void +package struct ConsoleModeButton: View { + package let title: String + package var details: String? + package let isSelected: Bool + package let action: () -> Void + + package init(title: String, details: String? = nil, isSelected: Bool, action: @escaping () -> Void) { + self.title = title + self.details = details + self.isSelected = isSelected + self.action = action + } - var body: some View { + package var body: some View { Button(action: action) { HStack(alignment: .firstTextBaseline, spacing: 4) { Text(title) @@ -131,7 +134,7 @@ struct ConsoleModeButton: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleListOptionsView: View { @EnvironmentObject private var filters: ConsoleFiltersViewModel diff --git a/Sources/PulseUI/Features/FileViewer/FileViewer.swift b/Sources/PulseUI/Features/FileViewer/FileViewer.swift index 35017b2be..751056d4e 100644 --- a/Sources/PulseUI/Features/FileViewer/FileViewer.swift +++ b/Sources/PulseUI/Features/FileViewer/FileViewer.swift @@ -90,8 +90,8 @@ private struct PreviewContainer: View { #if DEBUG -enum MockJSON { - static let allPossibleValues = """ +package enum MockJSON { + package static let allPossibleValues = """ { "actors": [ { diff --git a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView-watchos.swift b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView-watchos.swift index 0f13798cd..c1032f823 100644 --- a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView-watchos.swift +++ b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView-watchos.swift @@ -20,7 +20,7 @@ struct RichTextView: View { } #if os(watchOS) .toolbar { - if #available(watchOS 9.0, *) { + if #available(watchOS 9, *) { ShareLink(item: viewModel.text) } } diff --git a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView.swift b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView.swift index 1f25ba658..4cd25fcd0 100644 --- a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView.swift +++ b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView.swift @@ -9,6 +9,7 @@ import CoreData import Pulse import Combine +@available(iOS 16, visionOS 1, *) struct RichTextView: View { @ObservedObject var viewModel: RichTextViewModel var isTextViewBarItemsHidden = false @@ -42,17 +43,11 @@ struct RichTextView: View { @ViewBuilder private var contents: some View { - if #available(iOS 15, *) { - ContentView(viewModel: viewModel) - .searchable(text: $viewModel.searchTerm) - .disableAutocorrection(true) - } else { - WrappedTextView(viewModel: viewModel) - .edgesIgnoringSafeArea(.bottom) - } + ContentView(viewModel: viewModel) + .searchable(text: $viewModel.searchTerm) + .disableAutocorrection(true) } - @available(iOS 15, visionOS 1.0, *) private struct ContentView: View { @ObservedObject var viewModel: RichTextViewModel @Environment(\.isSearching) private var isSearching diff --git a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewModel.swift b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewModel.swift index 02a2d8248..010c0950f 100644 --- a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewModel.swift +++ b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewModel.swift @@ -212,14 +212,19 @@ private struct TextViewSearchContextKey: EnvironmentKey { } extension EnvironmentValues { - var textViewSearchContext: TextViewSearchContext? { + package var textViewSearchContext: TextViewSearchContext? { get { self[TextViewSearchContextKey.self] } set { self[TextViewSearchContextKey.self] = newValue } } } -struct TextViewSearchContext { - let searchTerm: ConsoleSearchTerm - let matchIndex: Int +package struct TextViewSearchContext { + package let searchTerm: ConsoleSearchTerm + package let matchIndex: Int + + package init(searchTerm: ConsoleSearchTerm, matchIndex: Int) { + self.searchTerm = searchTerm + self.matchIndex = matchIndex + } } diff --git a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewSearchToobar-ios.swift b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewSearchToobar-ios.swift index ebb2ea077..1415376d5 100644 --- a/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewSearchToobar-ios.swift +++ b/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextViewSearchToobar-ios.swift @@ -6,7 +6,7 @@ import SwiftUI -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RichTextViewSearchToobar: View { @ObservedObject var viewModel: RichTextViewModel diff --git a/Sources/PulseUI/Features/FileViewer/RichTextView/WrappedTextView.swift b/Sources/PulseUI/Features/FileViewer/RichTextView/WrappedTextView.swift index 65d58a8c0..4d7c00fa0 100644 --- a/Sources/PulseUI/Features/FileViewer/RichTextView/WrappedTextView.swift +++ b/Sources/PulseUI/Features/FileViewer/RichTextView/WrappedTextView.swift @@ -32,13 +32,8 @@ struct WrappedTextView: UIViewRepresentable { } func makeUIView(context: Context) -> UXTextView { - let textView: UITextView - if #available(iOS 16, *) { - // Disables the new TextKit 2 which is extremely slow on iOS 16 - textView = UITextView(usingTextLayoutManager: false) - } else { - textView = UITextView() - } + // Disables the new TextKit 2 which is extremely slow on iOS 16 and higher + let textView = UITextView(usingTextLayoutManager: false) configureTextView(textView) textView.delegate = context.coordinator textView.attributedText = viewModel.originalText diff --git a/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchLogLevelsCell.swift b/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchLogLevelsCell.swift index 8fa9fbce0..f596fe620 100644 --- a/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchLogLevelsCell.swift +++ b/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchLogLevelsCell.swift @@ -5,8 +5,12 @@ import SwiftUI import Pulse -struct ConsoleSearchLogLevelsCell: View { - @Binding var selection: Set +package struct ConsoleSearchLogLevelsCell: View { + @Binding package var selection: Set + + package init(selection: Binding>) { + self._selection = selection + } var isAllSelected: Bool { selection.count == LoggerStore.Level.allCases.count @@ -33,7 +37,7 @@ struct ConsoleSearchLogLevelsCell: View { } #if os(macOS) - var body: some View { + package var body: some View { VStack(alignment: .leading, spacing: -16) { HStack { Spacer() @@ -57,7 +61,7 @@ struct ConsoleSearchLogLevelsCell: View { } } #else - var body: some View { + package var body: some View { Section { ForEach(LoggerStore.Level.allCases, id: \.self) { level in HStack { diff --git a/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchToggleCell.swift b/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchToggleCell.swift index 104879025..cd820bfd3 100644 --- a/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchToggleCell.swift +++ b/Sources/PulseUI/Features/Filters/Cells/ConsoleSearchToggleCell.swift @@ -5,11 +5,16 @@ import SwiftUI import Pulse -struct ConsoleSearchToggleCell: View { - let title: String - @Binding var isOn: Bool +package struct ConsoleSearchToggleCell: View { + package let title: String + @Binding package var isOn: Bool - var body: some View { + package init(title: String, isOn: Binding) { + self.title = title + self._isOn = isOn + } + + package var body: some View { #if os(macOS) HStack { Toggle(title, isOn: $isOn) diff --git a/Sources/PulseUI/Features/Filters/ConsoleFiltersView.swift b/Sources/PulseUI/Features/Filters/ConsoleFiltersView.swift index 903130385..58175029c 100644 --- a/Sources/PulseUI/Features/Filters/ConsoleFiltersView.swift +++ b/Sources/PulseUI/Features/Filters/ConsoleFiltersView.swift @@ -8,7 +8,7 @@ import Combine #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleFiltersView: View { @EnvironmentObject var environment: ConsoleEnvironment // important: reloads mode @EnvironmentObject var viewModel: ConsoleFiltersViewModel @@ -50,7 +50,7 @@ struct ConsoleFiltersView: View { // MARK: - ConsoleFiltersView (Sections) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) extension ConsoleFiltersView { var sessionsSection: some View { ConsoleSection(isDividerHidden: true, header: { @@ -98,7 +98,7 @@ extension ConsoleFiltersView { #if DEBUG import CoreData -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleFiltersView_Previews: PreviewProvider { static var previews: some View { Group { @@ -118,7 +118,7 @@ struct ConsoleFiltersView_Previews: PreviewProvider { } } -@available(iOS 15, macOS 13, visionOS 1.0, *) +@available(iOS 16, macOS 13, visionOS 1, *) private func makePreview(isOnlyNetwork: Bool) -> some View { let store = LoggerStore.mock let entities: [NSManagedObject] = try! isOnlyNetwork ? store.tasks() : store.messages() diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleDomainsSelectionView.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleDomainsSelectionView.swift index 3b7e075b1..b96a30197 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleDomainsSelectionView.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleDomainsSelectionView.swift @@ -6,7 +6,7 @@ import SwiftUI import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleDomainsSelectionView: View { @ObservedObject var viewModel: ConsoleFiltersViewModel @EnvironmentObject private var index: LoggerStoreIndex diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleLabelsSelectionView.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleLabelsSelectionView.swift index b8a29fdfb..b340007a3 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleLabelsSelectionView.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleLabelsSelectionView.swift @@ -6,7 +6,7 @@ import SwiftUI import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleLabelsSelectionView: View { @ObservedObject var viewModel: ConsoleFiltersViewModel @EnvironmentObject private var index: LoggerStoreIndex diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleSearchListSelectionView.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleSearchListSelectionView.swift index ae0251580..b3f985e8a 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleSearchListSelectionView.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleSearchListSelectionView.swift @@ -5,27 +5,47 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1.0, *) -struct ConsoleSearchListSelectionView: View { - let title: String - let items: Data - let id: KeyPath - @Binding var selection: Set - let description: (Data.Element) -> String - @ViewBuilder let label: (Data.Element) -> Label +@available(iOS 16, visionOS 1, *) +package struct ConsoleSearchListSelectionView: View { + package let title: String + package let items: Data + package let id: KeyPath + @Binding package var selection: Set + package let description: (Data.Element) -> String + @ViewBuilder package let label: (Data.Element) -> Label #if os(iOS) || os(macOS) || os(visionOS) - var limit = 6 + package var limit = 6 #else - var limit = 3 + package var limit = 3 #endif + package init( + title: String, + items: Data, + id: KeyPath, + selection: Binding>, + description: @escaping (Data.Element) -> String, + @ViewBuilder label: @escaping (Data.Element) -> Label, + limit: Int? = nil + ) { + self.title = title + self.items = items + self.id = id + self._selection = selection + self.description = description + self.label = label + if let limit { + self.limit = limit + } + } + @State private var searchText = "" #if os(macOS) @State private var isExpanded = false - var body: some View { + package var body: some View { if items.isEmpty { emptyView } else { @@ -53,7 +73,7 @@ struct ConsoleSearchListSelectionView = [] diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleSearchSectionHeader.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleSearchSectionHeader.swift index a6b2daf5e..3edd1e092 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleSearchSectionHeader.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleSearchSectionHeader.swift @@ -4,6 +4,8 @@ import SwiftUI +#if os(iOS) || os(visionOS) + struct ConsoleSectionHeader: View { let icon: String let title: String @@ -22,28 +24,6 @@ struct ConsoleSectionHeader: View { self.isDefault = filter.wrappedValue == `default` ?? Filter() } -#if os(macOS) - var body: some View { - HStack { - HStack(spacing: 4) { - Image(systemName: icon) - .foregroundColor(.secondary) - Text(title) - .lineLimit(1) - .font(.headline) - .foregroundColor(.secondary) - } - Spacer() - if !isDefault { - Button(action: reset) { - Image(systemName: "arrow.uturn.left") - } - .foregroundColor(.secondary) - .disabled(isDefault) - } - }.buttonStyle(.plain) - } -#elseif os(iOS) || os(visionOS) var body: some View { HStack { Text(title) @@ -62,9 +42,6 @@ struct ConsoleSectionHeader: View { } } } -#else - var body: some View { - Text(title) - } -#endif } + +#endif diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleSection.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleSection.swift index 1234897c7..03f0d5648 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleSection.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleSection.swift @@ -4,12 +4,22 @@ import SwiftUI -struct ConsoleSection: View { - var isDividerHidden = false - @ViewBuilder var header: () -> Header - @ViewBuilder var content: () -> Content +package struct ConsoleSection: View { + package var isDividerHidden = false + @ViewBuilder package var header: () -> Header + @ViewBuilder package var content: () -> Content - var body: some View { + package init( + isDividerHidden: Bool = false, + @ViewBuilder header: @escaping () -> Header, + @ViewBuilder content: @escaping () -> Content + ) { + self.isDividerHidden = isDividerHidden + self.header = header + self.content = content + } + + package var body: some View { #if os(macOS) Section(content: { VStack(spacing: 8) { diff --git a/Sources/PulseUI/Features/Filters/Views/ConsoleSessionsPickerView.swift b/Sources/PulseUI/Features/Filters/Views/ConsoleSessionsPickerView.swift index 2bbc3dc37..00e9e37fc 100644 --- a/Sources/PulseUI/Features/Filters/Views/ConsoleSessionsPickerView.swift +++ b/Sources/PulseUI/Features/Filters/Views/ConsoleSessionsPickerView.swift @@ -6,21 +6,25 @@ import SwiftUI import Pulse import CoreData -@available(iOS 15, macOS 13, visionOS 1.0, *) -struct ConsoleSessionsPickerView: View { +@available(iOS 16, macOS 13, visionOS 1, *) +package struct ConsoleSessionsPickerView: View { @Binding var selection: Set @State private var isShowingPicker = false @Environment(\.store) private var store: LoggerStore + package static var makeSessionPicker: (_ selection: Binding>) -> AnyView = { + AnyView(SessionPickerView(selection: $0)) + } + #if os(watchOS) || os(tvOS) @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \LoggerSessionEntity.createdAt, ascending: false)]) private var sessions: FetchedResults #endif - var body: some View { + package var body: some View { #if os(iOS) || os(visionOS) - NavigationLink(destination: Components.makeSessionPicker(selection: $selection)) { + NavigationLink(destination: ConsoleSessionsPickerView.makeSessionPicker($selection)) { InfoRow(title: "Sessions", details: selectedSessionTitle) } #elseif os(macOS) @@ -32,7 +36,7 @@ struct ConsoleSessionsPickerView: View { Button("Select...") { isShowingPicker = true } } .popover(isPresented: $isShowingPicker, arrowEdge: .trailing) { - Components.makeSessionPicker(selection: $selection) + ConsoleSessionsPickerView.makeSessionPicker($selection) .frame(width: 260, height: 370) } diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkCookiesCell.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkCookiesCell.swift index c60b450e9..cc5487477 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkCookiesCell.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkCookiesCell.swift @@ -7,7 +7,7 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkCookiesCell: View { let viewModel: NetworkCookiesCellViewModel @@ -31,7 +31,7 @@ struct NetworkCookiesCell: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkCookiesCellViewModel { let title: String let details: String @@ -57,7 +57,7 @@ private func getCookies(from headers: [String: String]?, url: URL?) -> [HTTPCook return HTTPCookie.cookies(withResponseHeaderFields: headers, for: url) } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) private func makeAttributedString(for cookies: [HTTPCookie]) -> NSAttributedString { guard !cookies.isEmpty else { return NSAttributedString(string: "Empty") // Should never happen @@ -87,7 +87,7 @@ private func makeAttributedString(for cookies: [HTTPCookie]) -> NSAttributedStri } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkCookiesCell_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkMetricsCell.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkMetricsCell.swift index 1e575774b..4733e42bd 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkMetricsCell.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkMetricsCell.swift @@ -7,7 +7,7 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1, macOS 13, *) +@available(iOS 16, visionOS 1, macOS 13, *) struct NetworkMetricsCell: View { let task: NetworkTaskEntity diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestInfoCell.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestInfoCell.swift index e504a4e13..8f4a6a94c 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestInfoCell.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestInfoCell.swift @@ -25,12 +25,12 @@ struct NetworkRequestInfoCell: View { } } -final class NetworkRequestInfoCellViewModel { - let httpMethod: String - let url: String - let render: () -> NSAttributedString +package final class NetworkRequestInfoCellViewModel { + package let httpMethod: String + package let url: String + package let render: () -> NSAttributedString - init(task: NetworkTaskEntity, store: LoggerStore) { + package init(task: NetworkTaskEntity, store: LoggerStore) { self.httpMethod = task.httpMethod ?? "GET" self.url = task.url ?? "–" self.render = { @@ -40,7 +40,7 @@ final class NetworkRequestInfoCellViewModel { } } - init(transaction: NetworkTransactionMetricsEntity) { + package init(transaction: NetworkTransactionMetricsEntity) { self.httpMethod = transaction.request.httpMethod ?? "GET" self.url = transaction.request.url ?? "–" self.render = { TextRenderer(options: .sharing).make { $0.render(transaction) } } diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusCell.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusCell.swift index 70b185ad4..7a2a5eba1 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusCell.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusCell.swift @@ -5,12 +5,16 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1.0, *) -struct NetworkRequestStatusCell: View { - let viewModel: NetworkRequestStatusCellModel +@available(iOS 16, visionOS 1, *) +package struct NetworkRequestStatusCell: View { + package let viewModel: NetworkRequestStatusCellModel + + package init(viewModel: NetworkRequestStatusCellModel) { + self.viewModel = viewModel + } #if os(watchOS) - var body: some View { + package var body: some View { HStack(spacing: spacing) { Text(viewModel.status.title) .lineLimit(3) @@ -23,7 +27,7 @@ struct NetworkRequestStatusCell: View { } #else - var body: some View { + package var body: some View { HStack(spacing: spacing) { viewModel.status.text .lineLimit(1) @@ -57,18 +61,18 @@ struct NetworkRequestStatusCell: View { } } -struct NetworkRequestStatusCellModel { - let status: StatusLabelViewModel - let isMock: Bool +package struct NetworkRequestStatusCellModel { + package let status: StatusLabelViewModel + package let isMock: Bool fileprivate let duration: DurationViewModel? - init(task: NetworkTaskEntity, store: LoggerStore?) { + package init(task: NetworkTaskEntity, store: LoggerStore?) { self.status = StatusLabelViewModel(task: task, store: store) self.duration = DurationViewModel(task: task) self.isMock = task.isMocked } - init(transaction: NetworkTransactionMetricsEntity) { + package init(transaction: NetworkTransactionMetricsEntity) { status = StatusLabelViewModel(transaction: transaction) duration = DurationViewModel(transaction: transaction) isMock = false @@ -133,7 +137,7 @@ private let spacing: CGFloat? = nil #endif #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkRequestStatusCell_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusSectionView.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusSectionView.swift index 1de029637..deb9a7064 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusSectionView.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkRequestStatusSectionView.swift @@ -7,7 +7,7 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkRequestStatusSectionView: View { let viewModel: NetworkRequestStatusSectionViewModel @@ -44,7 +44,7 @@ final class NetworkRequestStatusSectionViewModel { } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkRequestStatusSectionView_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Features/Inspector/Cells/NetworkResponseBodyCell.swift b/Sources/PulseUI/Features/Inspector/Cells/NetworkResponseBodyCell.swift index 81ce6a3ec..452c94185 100644 --- a/Sources/PulseUI/Features/Inspector/Cells/NetworkResponseBodyCell.swift +++ b/Sources/PulseUI/Features/Inspector/Cells/NetworkResponseBodyCell.swift @@ -7,7 +7,7 @@ import Pulse #if !os(macOS) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkResponseBodyCell: View { let viewModel: NetworkResponseBodyCellViewModel diff --git a/Sources/PulseUI/Features/Inspector/NetworkDetailsView.swift b/Sources/PulseUI/Features/Inspector/NetworkDetailsView.swift index 099770a8e..7fa5264a2 100644 --- a/Sources/PulseUI/Features/Inspector/NetworkDetailsView.swift +++ b/Sources/PulseUI/Features/Inspector/NetworkDetailsView.swift @@ -5,12 +5,12 @@ import SwiftUI import Pulse -struct NetworkDetailsView: View { +package struct NetworkDetailsView: View { private var title: String private let viewModel: NetworkDetailsViewModel? @State private var isShowingShareSheet = false - init(title: String, viewModel: @escaping () -> KeyValueSectionViewModel?) { + package init(title: String, viewModel: @escaping () -> KeyValueSectionViewModel?) { self.title = title self.viewModel = NetworkDetailsViewModel { viewModel().map { viewModel in @@ -19,12 +19,12 @@ struct NetworkDetailsView: View { } } - init(title: String, text: @escaping () -> NSAttributedString?) { + package init(title: String, text: @escaping () -> NSAttributedString?) { self.title = title self.viewModel = NetworkDetailsViewModel(text) } - var body: some View { + package var body: some View { contents.inlineNavigationTitle(title) } diff --git a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-shared.swift b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-shared.swift index 5ead85042..58c2334aa 100644 --- a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-shared.swift +++ b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-shared.swift @@ -7,7 +7,7 @@ import SwiftUI import Pulse -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) extension NetworkInspectorView { @ViewBuilder static func makeRequestSection(task: NetworkTaskEntity, isCurrentRequest: Bool) -> some View { diff --git a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-tvos.swift b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-tvos.swift index f6e1db4c6..f5b6aa286 100644 --- a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-tvos.swift +++ b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-tvos.swift @@ -18,7 +18,7 @@ struct NetworkInspectorView: View { var body: some View { contents - .inlineNavigationTitle(ConsoleViewDelegate.getShortTitle(for: task)) + .inlineNavigationTitle(task.getShortTitle(options: settings.listDisplayOptions)) } var contents: some View { diff --git a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-watchos.swift b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-watchos.swift index 6683950f5..e8211a83d 100644 --- a/Sources/PulseUI/Features/Inspector/NetworkInspectorView-watchos.swift +++ b/Sources/PulseUI/Features/Inspector/NetworkInspectorView-watchos.swift @@ -18,7 +18,7 @@ struct NetworkInspectorView: View { var body: some View { contents - .inlineNavigationTitle(ConsoleViewDelegate.getShortTitle(for: task)) + .inlineNavigationTitle(task.getShortTitle(options: settings.listDisplayOptions)) // .toolbar { // if #available(watchOS 9, *), let url = viewModel.shareTaskAsHTML() { // ShareLink(item: url) diff --git a/Sources/PulseUI/Features/Inspector/NetworkInspectorView.swift b/Sources/PulseUI/Features/Inspector/NetworkInspectorView.swift index e99e3fb4a..37cd96472 100644 --- a/Sources/PulseUI/Features/Inspector/NetworkInspectorView.swift +++ b/Sources/PulseUI/Features/Inspector/NetworkInspectorView.swift @@ -9,8 +9,8 @@ import CoreData import Pulse import Combine -@available(iOS 15, visionOS 1, macOS 13, *) -struct NetworkInspectorView: View { +@available(iOS 16, visionOS 1, macOS 13, *) +package struct NetworkInspectorView: View { @ObservedObject var task: NetworkTaskEntity @State private var shareItems: ShareItems? @@ -18,7 +18,11 @@ struct NetworkInspectorView: View { @ObservedObject private var settings: UserSettings = .shared @Environment(\.store) private var store - var body: some View { + package init(task: NetworkTaskEntity) { + self.task = task + } + + package var body: some View { List { contents } @@ -27,7 +31,7 @@ struct NetworkInspectorView: View { .safeAreaInset(edge: .bottom) { OpenOnMacOverlay(entity: task) } - .inlineNavigationTitle(ConsoleViewDelegate.getShortTitle(for: task)) + .inlineNavigationTitle(task.getShortTitle(options: settings.listDisplayOptions)) .sheet(item: $shareItems, content: ShareView.init) .toolbar { ToolbarItemGroup(placement: .automatic) { @@ -95,7 +99,7 @@ struct NetworkInspectorView: View { } #if DEBUG -@available(iOS 15, visionOS 1, macOS 13, *) +@available(iOS 16, visionOS 1, macOS 13, *) struct NetworkInspectorView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/Sources/PulseUI/Features/MessageDetails/ConsoleMessageDetailsView.swift b/Sources/PulseUI/Features/MessageDetails/ConsoleMessageDetailsView.swift index 420339c64..7372e3e50 100644 --- a/Sources/PulseUI/Features/MessageDetails/ConsoleMessageDetailsView.swift +++ b/Sources/PulseUI/Features/MessageDetails/ConsoleMessageDetailsView.swift @@ -7,12 +7,16 @@ import Pulse #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) -@available(iOS 15, macOS 13, visionOS 1, *) -struct ConsoleMessageDetailsView: View { - let message: LoggerMessageEntity +@available(iOS 16, macOS 13, visionOS 1, *) +package struct ConsoleMessageDetailsView: View { + package let message: LoggerMessageEntity + + package init(message: LoggerMessageEntity) { + self.message = message + } #if os(iOS) || os(visionOS) - var body: some View { + package var body: some View { contents .inlineNavigationTitle("") .toolbar { @@ -29,7 +33,7 @@ struct ConsoleMessageDetailsView: View { } } #elseif os(watchOS) - var body: some View { + package var body: some View { ScrollView { VStack(spacing: 8) { NavigationLink(destination: ConsoleMessageMetadataView(message: message)) { @@ -40,7 +44,7 @@ struct ConsoleMessageDetailsView: View { } } #elseif os(tvOS) - var body: some View { + package var body: some View { contents } #endif @@ -57,7 +61,7 @@ struct ConsoleMessageDetailsView: View { } #if DEBUG -@available(iOS 15, macOS 13, visionOS 1, *) +@available(iOS 16, macOS 13, visionOS 1, *) struct ConsoleMessageDetailsView_Previews: PreviewProvider { static var previews: some View { NavigationView { @@ -71,7 +75,7 @@ struct ConsoleMessageDetailsView_Previews: PreviewProvider { #if DEBUG -func makeMockMessage() -> LoggerMessageEntity { +package func makeMockMessage() -> LoggerMessageEntity { let entity = LoggerMessageEntity(context: LoggerStore.mock.viewContext) entity.text = "test" entity.createdAt = Date() diff --git a/Sources/PulseUI/Features/MessageDetails/ConsoleMessageMetadataView.swift b/Sources/PulseUI/Features/MessageDetails/ConsoleMessageMetadataView.swift index 6bbb64f11..092123e3d 100644 --- a/Sources/PulseUI/Features/MessageDetails/ConsoleMessageMetadataView.swift +++ b/Sources/PulseUI/Features/MessageDetails/ConsoleMessageMetadataView.swift @@ -5,24 +5,32 @@ import SwiftUI import Pulse -@available(iOS 15, macOS 13, visionOS 1, *) +@available(iOS 16, macOS 13, visionOS 1, *) struct ConsoleMessageMetadataView: View { let message: LoggerMessageEntity + init(message: LoggerMessageEntity) { + self.message = message + } + var body: some View { - Components.makeRichTextView(string: string) -#if !os(macOS) + RichTextView(viewModel: .init(string: string)) .navigationTitle("Message Details") -#endif } private var string: NSAttributedString { let renderer = TextRenderer() + let sections = KeyValueSectionViewModel.makeMetadata(for: message) renderer.render(sections) return renderer.make() } +} - private var sections: [KeyValueSectionViewModel] { +extension KeyValueSectionViewModel { + package static func makeMetadata(for message: LoggerMessageEntity) -> [KeyValueSectionViewModel] { + let metadataItems: [(String, String?)] = message.metadata + .sorted(by: { $0.key < $1.key }) + .map { ($0.key, $0.value )} return [ KeyValueSectionViewModel(title: "Summary", color: .textColor(for: message.logLevel), items: [ ("Date", DateFormatter.fullDateFormatter.string(from: message.createdAt)), @@ -37,10 +45,6 @@ struct ConsoleMessageMetadataView: View { KeyValueSectionViewModel(title: "Metadata", color: .indigo, items: metadataItems) ] } - - private var metadataItems: [(String, String?)] { - message.metadata.sorted(by: { $0.key < $1.key }).map { ($0.key, $0.value )} - } } private extension String { @@ -50,7 +54,7 @@ private extension String { } #if DEBUG -@available(iOS 15, macOS 13, visionOS 1, *) +@available(iOS 16, macOS 13, visionOS 1, *) struct ConsoleMessageMetadataView_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Features/Remote/RemoteLoggerEnterPasswordView.swift b/Sources/PulseUI/Features/Remote/RemoteLoggerEnterPasswordView.swift index 24bc1a845..d28dfc380 100644 --- a/Sources/PulseUI/Features/Remote/RemoteLoggerEnterPasswordView.swift +++ b/Sources/PulseUI/Features/Remote/RemoteLoggerEnterPasswordView.swift @@ -7,7 +7,7 @@ import Network import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerEnterPasswordView: View { @ObservedObject var viewModel: RemoteLoggerSettingsViewModel @ObservedObject var logger: RemoteLogger = .shared diff --git a/Sources/PulseUI/Features/Remote/RemoteLoggerErrorView.swift b/Sources/PulseUI/Features/Remote/RemoteLoggerErrorView.swift index ea929e004..c6402c495 100644 --- a/Sources/PulseUI/Features/Remote/RemoteLoggerErrorView.swift +++ b/Sources/PulseUI/Features/Remote/RemoteLoggerErrorView.swift @@ -5,7 +5,7 @@ import SwiftUI import Network -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerErrorView: View { let error: NWError @@ -55,7 +55,7 @@ private struct RemoteLoggerPolicyDeniedView: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) private struct RemoteLoggerNoAuthView: View { var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -93,7 +93,7 @@ private let plistContents = """ """ #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct Previews_RemoteLoggerNoAuthView_Previews: PreviewProvider { static var previews: some View { Form { diff --git a/Sources/PulseUI/Features/Remote/RemoteLoggerSelectedDeviceView.swift b/Sources/PulseUI/Features/Remote/RemoteLoggerSelectedDeviceView.swift index f23a8349c..3f6233cca 100644 --- a/Sources/PulseUI/Features/Remote/RemoteLoggerSelectedDeviceView.swift +++ b/Sources/PulseUI/Features/Remote/RemoteLoggerSelectedDeviceView.swift @@ -6,7 +6,7 @@ import SwiftUI import Network import Pulse -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerSelectedDeviceView: View { @ObservedObject var logger: RemoteLogger = .shared diff --git a/Sources/PulseUI/Features/Remote/RemoteLoggerSettingsView.swift b/Sources/PulseUI/Features/Remote/RemoteLoggerSettingsView.swift index 812b2f40d..9c9cc0dd3 100644 --- a/Sources/PulseUI/Features/Remote/RemoteLoggerSettingsView.swift +++ b/Sources/PulseUI/Features/Remote/RemoteLoggerSettingsView.swift @@ -8,7 +8,7 @@ import Combine import Pulse import Network -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerSettingsView: View { @ObservedObject private var logger: RemoteLogger = .shared @ObservedObject var viewModel: RemoteLoggerSettingsViewModel @@ -130,7 +130,7 @@ struct RemoteLoggerSettingsView: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerSettingsRouterView: View { @ObservedObject private var logger: RemoteLogger = .shared @ObservedObject var viewModel: RemoteLoggerSettingsViewModel @@ -164,7 +164,7 @@ struct RemoteLoggerSettingsRouterView: View { } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct RemoteLoggerSettingsView_Previews: PreviewProvider { static var previews: some View { #if os(macOS) diff --git a/Sources/PulseUI/Features/Search/ConsoleSearchListContentView.swift b/Sources/PulseUI/Features/Search/ConsoleSearchListContentView.swift index dd5eb5a05..f095c0845 100644 --- a/Sources/PulseUI/Features/Search/ConsoleSearchListContentView.swift +++ b/Sources/PulseUI/Features/Search/ConsoleSearchListContentView.swift @@ -9,7 +9,7 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchListContentView: View { @EnvironmentObject private var viewModel: ConsoleSearchViewModel @@ -43,7 +43,7 @@ struct ConsoleSearchListContentView: View { } } -@available(iOS 15, visionOS 1, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchResultsListContentView: View { @EnvironmentObject private var viewModel: ConsoleSearchViewModel diff --git a/Sources/PulseUI/Features/Search/ConsoleSearchViewModel.swift b/Sources/PulseUI/Features/Search/ConsoleSearchViewModel.swift index 1f766719f..2381dddc5 100644 --- a/Sources/PulseUI/Features/Search/ConsoleSearchViewModel.swift +++ b/Sources/PulseUI/Features/Search/ConsoleSearchViewModel.swift @@ -7,7 +7,7 @@ import Pulse import CoreData import Combine -protocol ConsoleEntitiesSource { +package protocol ConsoleEntitiesSource { var events: PassthroughSubject { get } var entities: [NSManagedObject] { get } } @@ -18,7 +18,7 @@ final class ConsoleSearchBarViewModel: ObservableObject { @Published var text: String = "" } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) final class ConsoleSearchViewModel: ObservableObject, ConsoleSearchOperationDelegate { @Published var options: StringSearchOptions = .default @Published var scopes: Set = [] @@ -306,7 +306,7 @@ final class ConsoleSearchViewModel: ObservableObject, ConsoleSearchOperationDele } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchResultViewModel: Identifiable { var id: ConsoleSearchResultKey { ConsoleSearchResultKey(id: entity.objectID) } let entity: NSManagedObject @@ -331,12 +331,12 @@ struct ConsoleSearchParameters: Equatable, Hashable { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchSuggestionsViewModel { let searches: [ConsoleSearchSuggestion] } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchSuggestion: Identifiable { let id = UUID() let text: AttributedString diff --git a/Sources/PulseUI/Features/Search/Services/ConsoleSearchOccurrence.swift b/Sources/PulseUI/Features/Search/Services/ConsoleSearchOccurrence.swift index 055b89f4a..222024dfd 100644 --- a/Sources/PulseUI/Features/Search/Services/ConsoleSearchOccurrence.swift +++ b/Sources/PulseUI/Features/Search/Services/ConsoleSearchOccurrence.swift @@ -9,17 +9,17 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) -final class ConsoleSearchOccurrence: Identifiable, Equatable, Hashable { - let id = ConsoleSearchOccurrenceId() - let scope: ConsoleSearchScope - let match: ConsoleSearchMatch - var line: Int { match.lineNumber } - var range: NSRange { NSRange(match.range, in: match.line) } - lazy var preview = ConsoleSearchOccurrence.makePreview(for: match, attributes: previewAttibutes) - let searchContext: TextViewSearchContext - - init(scope: ConsoleSearchScope, +@available(iOS 16, visionOS 1, *) +package final class ConsoleSearchOccurrence: Identifiable, Equatable, Hashable { + package let id = ConsoleSearchOccurrenceId() + package let scope: ConsoleSearchScope + package let match: ConsoleSearchMatch + package var line: Int { match.lineNumber } + package var range: NSRange { NSRange(match.range, in: match.line) } + package lazy var preview = ConsoleSearchOccurrence.makePreview(for: match, attributes: previewAttibutes) + package let searchContext: TextViewSearchContext + + package init(scope: ConsoleSearchScope, match: ConsoleSearchMatch, searchContext: TextViewSearchContext) { self.scope = scope @@ -27,18 +27,18 @@ final class ConsoleSearchOccurrence: Identifiable, Equatable, Hashable { self.searchContext = searchContext } - static func == (lhs: ConsoleSearchOccurrence, rhs: ConsoleSearchOccurrence) -> Bool { + package static func == (lhs: ConsoleSearchOccurrence, rhs: ConsoleSearchOccurrence) -> Bool { lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { + package func hash(into hasher: inout Hasher) { id.hash(into: &hasher) } } private let previewAttibutes = TextHelper().attributes(role: .body2, style: .monospaced) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) extension ConsoleSearchOccurrence { static func makePreview(for match: ConsoleSearchMatch, attributes customAttributes: [NSAttributedString.Key: Any] = [:]) -> AttributedString { @@ -84,8 +84,8 @@ private extension Substring { } } -struct ConsoleSearchOccurrenceId: Hashable { - let id = UUID() +package struct ConsoleSearchOccurrenceId: Hashable { + package let id = UUID() } #endif diff --git a/Sources/PulseUI/Features/Search/Services/ConsoleSearchOperation.swift b/Sources/PulseUI/Features/Search/Services/ConsoleSearchOperation.swift index 1cba66d38..0e67f848a 100644 --- a/Sources/PulseUI/Features/Search/Services/ConsoleSearchOperation.swift +++ b/Sources/PulseUI/Features/Search/Services/ConsoleSearchOperation.swift @@ -9,13 +9,13 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) protocol ConsoleSearchOperationDelegate: AnyObject { func searchOperation(_ operation: ConsoleSearchOperation, didAddResults results: [ConsoleSearchResultViewModel]) func searchOperationDidFinish(_ operation: ConsoleSearchOperation, hasMore: Bool) } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) final class ConsoleSearchOperation { private let parameters: ConsoleSearchParameters private var entities: [NSManagedObject] @@ -188,30 +188,37 @@ final class ConsoleSearchOperation { } } -struct ConsoleSearchMatch { - let line: String +package struct ConsoleSearchMatch { + package let line: String /// Starts with `1. - let lineNumber: Int - let range: Range - let term: ConsoleSearchTerm + package let lineNumber: Int + package let range: Range + package let term: ConsoleSearchTerm - static let limit = 1000 + package static let limit = 1000 + + package init(line: String, lineNumber: Int, range: Range, term: ConsoleSearchTerm) { + self.line = line + self.lineNumber = lineNumber + self.range = range + self.term = term + } } -@available(iOS 15, visionOS 1.0, *) -final class ConsoleSearchService { +@available(iOS 16, visionOS 1, *) +package final class ConsoleSearchService { private let cache = NSCache() - init() { + package init() { cache.totalCostLimit = 16_000_000 cache.countLimit = 1000 } - func clearCache() { + package func clearCache() { cache.removeAllObjects() } - func getBodyString(for blob: LoggerBlobHandleEntity) -> String? { + package func getBodyString(for blob: LoggerBlobHandleEntity) -> String? { if let string = cache.object(forKey: blob.objectID)?.value { return string } diff --git a/Sources/PulseUI/Features/Search/Services/ConsoleSearchScope.swift b/Sources/PulseUI/Features/Search/Services/ConsoleSearchScope.swift index 0e0662042..f5365c3f8 100644 --- a/Sources/PulseUI/Features/Search/Services/ConsoleSearchScope.swift +++ b/Sources/PulseUI/Features/Search/Services/ConsoleSearchScope.swift @@ -6,7 +6,7 @@ import Foundation -enum ConsoleSearchScope: Equatable, Hashable, Codable, CaseIterable { +package enum ConsoleSearchScope: Equatable, Hashable, Codable, CaseIterable { // MARK: Logs case message case metadata @@ -19,7 +19,7 @@ enum ConsoleSearchScope: Equatable, Hashable, Codable, CaseIterable { case responseHeaders case responseBody - var isDisplayedInResults: Bool { + package var isDisplayedInResults: Bool { switch self { case .message, .url: return false @@ -28,12 +28,12 @@ enum ConsoleSearchScope: Equatable, Hashable, Codable, CaseIterable { } } - static let messageScopes: [ConsoleSearchScope] = [ + package static let messageScopes: [ConsoleSearchScope] = [ .message, .metadata ] - static let networkScopes: [ConsoleSearchScope] = [ + package static let networkScopes: [ConsoleSearchScope] = [ .url, .originalRequestHeaders, .currentRequestHeaders, @@ -42,7 +42,7 @@ enum ConsoleSearchScope: Equatable, Hashable, Codable, CaseIterable { .responseBody ] - var title: String { + package var title: String { switch self { case .url: return "URL" case .originalRequestHeaders: return "Original Request Headers" diff --git a/Sources/PulseUI/Features/Search/Services/ConsoleSearchTerm.swift b/Sources/PulseUI/Features/Search/Services/ConsoleSearchTerm.swift index 7d9bad32b..74b819173 100644 --- a/Sources/PulseUI/Features/Search/Services/ConsoleSearchTerm.swift +++ b/Sources/PulseUI/Features/Search/Services/ConsoleSearchTerm.swift @@ -4,9 +4,14 @@ import Foundation -struct ConsoleSearchTerm: Identifiable, Hashable, Codable { - var id: ConsoleSearchTerm { self } +package struct ConsoleSearchTerm: Identifiable, Hashable, Codable { + package var id: ConsoleSearchTerm { self } - var text: String - var options: StringSearchOptions + package var text: String + package var options: StringSearchOptions + + package init(text: String, options: StringSearchOptions) { + self.text = text + self.options = options + } } diff --git a/Sources/PulseUI/Features/Search/Views/ConsoleSearchContextMenu.swift b/Sources/PulseUI/Features/Search/Views/ConsoleSearchContextMenu.swift index 6233dc6bb..225fa7e83 100644 --- a/Sources/PulseUI/Features/Search/Views/ConsoleSearchContextMenu.swift +++ b/Sources/PulseUI/Features/Search/Views/ConsoleSearchContextMenu.swift @@ -9,7 +9,7 @@ import CoreData import Pulse import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchContextMenu: View { @EnvironmentObject private var viewModel: ConsoleSearchViewModel diff --git a/Sources/PulseUI/Features/Search/Views/ConsoleSearchResultsSectionView.swift b/Sources/PulseUI/Features/Search/Views/ConsoleSearchResultsSectionView.swift index d5a1074d8..25cf74e21 100644 --- a/Sources/PulseUI/Features/Search/Views/ConsoleSearchResultsSectionView.swift +++ b/Sources/PulseUI/Features/Search/Views/ConsoleSearchResultsSectionView.swift @@ -9,7 +9,7 @@ import Combine #if os(iOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, macOS 13, *) +@available(iOS 16, visionOS 1, macOS 13, *) struct ConsoleSearchResultView: View { let viewModel: ConsoleSearchResultViewModel var limit: Int = 4 @@ -100,7 +100,7 @@ struct ConsoleSearchResultView: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchResultDetailsView: View { let viewModel: ConsoleSearchResultViewModel @@ -114,9 +114,11 @@ struct ConsoleSearchResultDetailsView: View { } } -@available(iOS 15, visionOS 1.0, *) -struct PlainListGroupSeparator: View { - var body: some View { +@available(iOS 16, visionOS 1, *) +package struct PlainListGroupSeparator: View { + package init() {} + + package var body: some View { Rectangle().foregroundColor(.clear) // DIY separator .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowBackground(Color.separator.opacity(0.2)) @@ -129,12 +131,17 @@ struct PlainListGroupSeparator: View { #if os(iOS) || os(visionOS) || os(macOS) -@available(iOS 15, macOS 13, visionOS 1, *) -struct PlainListSectionHeader: View { - var title: String? - @ViewBuilder let content: () -> Content +@available(iOS 16, macOS 13, visionOS 1, *) +package struct PlainListSectionHeader: View { + package var title: String? + @ViewBuilder package let content: () -> Content - var body: some View { + package init(title: String? = nil, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.content = content + } + + package var body: some View { contents .padding(.top, 8) .listRowBackground(Color.separator.opacity(0.2)) @@ -157,18 +164,22 @@ struct PlainListSectionHeader: View { } } -@available(iOS 15, macOS 13, visionOS 1, *) +@available(iOS 16, macOS 13, visionOS 1, *) extension PlainListSectionHeader where Content == Text { init(title: String) { self.init(title: title, content: { Text(title) }) } } -@available(iOS 15, visionOS 1, *) -struct PlainListSeeAllView: View { +@available(iOS 16, visionOS 1, *) +package struct PlainListSeeAllView: View { let count: Int - var body: some View { + package init(count: Int) { + self.count = count + } + + package var body: some View { (Text("Show All").foregroundColor(.accentColor) + Text(" (\(count))")) .font(.subheadline) @@ -177,11 +188,15 @@ struct PlainListSeeAllView: View { } } -@available(iOS 15, visionOS 1.0, *) -struct PlainListSectionHeaderSeparator: View { +@available(iOS 16, visionOS 1, *) +package struct PlainListSectionHeaderSeparator: View { let title: String - var body: some View { + package init(title: String) { + self.title = title + } + + package var body: some View { HStack(alignment: .bottom, spacing: 0) { Text(title) .foregroundColor(.secondary) diff --git a/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionView.swift b/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionView.swift index cef021241..87fd9072f 100644 --- a/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionView.swift +++ b/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionView.swift @@ -9,7 +9,7 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchSuggestionView: View { let suggestion: ConsoleSearchSuggestion let action: () -> Void diff --git a/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionsView.swift b/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionsView.swift index 24e408728..f6de9ee82 100644 --- a/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionsView.swift +++ b/Sources/PulseUI/Features/Search/Views/ConsoleSearchSuggestionsView.swift @@ -9,7 +9,7 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchSuggestionsView: View { @EnvironmentObject private var viewModel: ConsoleSearchViewModel @@ -50,7 +50,7 @@ struct ConsoleSearchSuggestionsView: View { } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct Previews_ConsoleSearchSuggestionsView_Previews: PreviewProvider { static let environment = ConsoleEnvironment(store: .mock) diff --git a/Sources/PulseUI/Features/Search/Views/ConsoleSearchToolbar.swift b/Sources/PulseUI/Features/Search/Views/ConsoleSearchToolbar.swift index 9b9cd28ca..5b976d415 100644 --- a/Sources/PulseUI/Features/Search/Views/ConsoleSearchToolbar.swift +++ b/Sources/PulseUI/Features/Search/Views/ConsoleSearchToolbar.swift @@ -9,7 +9,7 @@ import Pulse import CoreData import Combine -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchToolbar: View { @EnvironmentObject private var viewModel: ConsoleSearchViewModel @@ -40,7 +40,7 @@ struct ConsoleSearchToolbar: View { } } -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct ConsoleSearchScopesPicker: View { @ObservedObject var viewModel: ConsoleSearchViewModel diff --git a/Sources/PulseUI/Features/Sessions/SessionListView.swift b/Sources/PulseUI/Features/Sessions/SessionListView.swift index 011d1f78b..8a88332df 100644 --- a/Sources/PulseUI/Features/Sessions/SessionListView.swift +++ b/Sources/PulseUI/Features/Sessions/SessionListView.swift @@ -10,7 +10,7 @@ import Combine #if os(iOS) || os(macOS) || os(visionOS) -@available(iOS 15, macOS 13, visionOS 1.0, *) +@available(iOS 16, macOS 13, visionOS 1, *) struct SessionListView: View { @Binding var selection: Set @Binding var sharedSessions: SelectedSessionsIDs? @@ -141,9 +141,13 @@ struct SessionListView: View { } } -struct SelectedSessionsIDs: Hashable, Identifiable { - var id: SelectedSessionsIDs { self } - let ids: Set +package struct SelectedSessionsIDs: Hashable, Identifiable { + package var id: SelectedSessionsIDs { self } + package let ids: Set + + package init(ids: Set) { + self.ids = ids + } } private let sectionTitleFormatter: DateFormatter = { @@ -157,7 +161,7 @@ private let sectionTitleFormatter: DateFormatter = { #endif @available(macOS 13, *) -struct ConsoleSessionCell: View { +package struct ConsoleSessionCell: View { let session: LoggerSessionEntity var isCompact = true @@ -166,7 +170,12 @@ struct ConsoleSessionCell: View { @Environment(\.editMode) private var editMode #endif - var body: some View { + package init(session: LoggerSessionEntity, isCompact: Bool = true) { + self.session = session + self.isCompact = isCompact + } + + package var body: some View { HStack(alignment: .lastTextBaseline) { Text(session.formattedDate(isCompact: isCompact)) .fontWeight(store.session.id == session.id ? .medium : .regular) diff --git a/Sources/PulseUI/Features/Sessions/SessionPickerView.swift b/Sources/PulseUI/Features/Sessions/SessionPickerView.swift index 52132e744..4453f4d2d 100644 --- a/Sources/PulseUI/Features/Sessions/SessionPickerView.swift +++ b/Sources/PulseUI/Features/Sessions/SessionPickerView.swift @@ -10,7 +10,7 @@ import Combine #if os(iOS) || os(macOS) || os(visionOS) -@available(iOS 15, macOS 13, visionOS 1.0, *) +@available(iOS 16, macOS 13, visionOS 1, *) struct SessionPickerView: View { @Binding var selection: Set diff --git a/Sources/PulseUI/Features/Sessions/SessionsView.swift b/Sources/PulseUI/Features/Sessions/SessionsView.swift index 01a76038b..6f5bbdbeb 100644 --- a/Sources/PulseUI/Features/Sessions/SessionsView.swift +++ b/Sources/PulseUI/Features/Sessions/SessionsView.swift @@ -10,7 +10,7 @@ import Combine #if os(iOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct SessionsView: View { @State private var selection: Set = [] @State private var sharedSessions: SelectedSessionsIDs? @@ -49,7 +49,7 @@ struct SessionsView: View { .sheet(item: $sharedSessions) { sessions in NavigationView { ShareStoreView(sessions: sessions.ids, onDismiss: { sharedSessions = nil }) - }.backport.presentationDetents([.medium, .large]) + }.presentationDetents([.medium, .large]) } } @@ -106,7 +106,7 @@ struct SessionsView: View { } #if DEBUG -@available(iOS 15.0, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct Previews_SessionsView_Previews: PreviewProvider { static let environment = ConsoleEnvironment(store: .mock) diff --git a/Sources/PulseUI/Features/Settings/SettingsView-ios.swift b/Sources/PulseUI/Features/Settings/SettingsView-ios.swift index e6743071f..4e7ee1d8b 100644 --- a/Sources/PulseUI/Features/Settings/SettingsView-ios.swift +++ b/Sources/PulseUI/Features/Settings/SettingsView-ios.swift @@ -8,7 +8,7 @@ import SwiftUI import Pulse import UniformTypeIdentifiers -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) public struct SettingsView: View { private let store: LoggerStore @State private var newHeaderName = "" @@ -25,27 +25,6 @@ public struct SettingsView: View { store === RemoteLogger.shared.store { RemoteLoggerSettingsView(viewModel: .shared) } - Section(header: Text("List headers"), footer: Text("These headers will be included in the list view")) { - ForEach(settings.displayHeaders, id: \.self) { - Text($0) - } - .onDelete { indices in - settings.displayHeaders.remove(atOffsets: indices) - } - HStack { - TextField("New Header", text: $newHeaderName) - Button(action: { - withAnimation { - settings.displayHeaders.append(newHeaderName) - newHeaderName = "" - } - }) { - Image(systemName: "plus.circle.fill") - .accessibilityLabel("Add header") - } - .disabled(newHeaderName.isEmpty) - } - } Section("Other") { NavigationLink(destination: StoreDetailsView(source: .store(store)), label: { Text("Store Info") @@ -58,7 +37,7 @@ public struct SettingsView: View { } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct SettingsView_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Features/Settings/StoreDetailsView.swift b/Sources/PulseUI/Features/Settings/StoreDetailsView.swift index 92b2c189b..d6c012ea4 100644 --- a/Sources/PulseUI/Features/Settings/StoreDetailsView.swift +++ b/Sources/PulseUI/Features/Settings/StoreDetailsView.swift @@ -5,6 +5,7 @@ import SwiftUI import Pulse +@available(iOS 16, tvOS 16, macOS 13, watchOS 9, visionOS 1, *) struct StoreDetailsView: View { @StateObject private var viewModel = StoreDetailsViewModel() @@ -47,7 +48,7 @@ struct StoreDetailsView: View { @ViewBuilder private var form: some View { Form { - if #available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, *), let info = viewModel.info { + if let info = viewModel.info { LoggerStoreSizeChart(info: info, sizeLimit: viewModel.storeSizeLimit) #if os(tvOS) .padding(.vertical) diff --git a/Sources/PulseUI/Helpers/Components.swift b/Sources/PulseUI/Helpers/Components.swift deleted file mode 100644 index b1453bda7..000000000 --- a/Sources/PulseUI/Helpers/Components.swift +++ /dev/null @@ -1,41 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). - -import SwiftUI -import Pulse -import CoreData - -struct Components { -#if os(iOS) || os(macOS) || os(visionOS) - @available(iOS 15, macOS 13, visionOS 1, *) - static func makeSessionPicker(selection: Binding>) -> some View { - SessionPickerView(selection: selection) - } -#endif - - static func makeRichTextView(string: NSAttributedString) -> some View { - RichTextView(viewModel: .init(string: string)) - } - - @available(iOS 15, macOS 13, visionOS 1, *) - static func makeConsoleEntityCell(entity: NSManagedObject) -> some View { -#if os(macOS) - EmptyView() -#else - ConsoleEntityCell(entity: entity) -#endif - } - - static func makePinView(for task: NetworkTaskEntity) -> some View { - EmptyView() - } - - static func makePinView(for message: LoggerMessageEntity) -> some View { - EmptyView() - } - - static func makeHighlightModifier() -> some ViewModifier { - EmptyModifier() - } -} diff --git a/Sources/PulseUI/Helpers/FileViewModelContext.swift b/Sources/PulseUI/Helpers/FileViewModelContext.swift index ffad9ce5d..5cd1ce795 100644 --- a/Sources/PulseUI/Helpers/FileViewModelContext.swift +++ b/Sources/PulseUI/Helpers/FileViewModelContext.swift @@ -5,17 +5,26 @@ import Pulse import Foundation -struct FileViewerViewModelContext { - var contentType: NetworkLogger.ContentType? - var originalSize: Int64 - var metadata: [String: String]? - var isResponse = true - var error: NetworkLogger.DecodingError? - var sourceURL: URL? +package struct FileViewerViewModelContext { + package var contentType: NetworkLogger.ContentType? + package var originalSize: Int64 + package var metadata: [String: String]? + package var isResponse = true + package var error: NetworkLogger.DecodingError? + package var sourceURL: URL? + + package init(contentType: NetworkLogger.ContentType? = nil, originalSize: Int64, metadata: [String : String]? = nil, isResponse: Bool = true, error: NetworkLogger.DecodingError? = nil, sourceURL: URL? = nil) { + self.contentType = contentType + self.originalSize = originalSize + self.metadata = metadata + self.isResponse = isResponse + self.error = error + self.sourceURL = sourceURL + } } extension NetworkTaskEntity { - var requestFileViewerContext: FileViewerViewModelContext { + package var requestFileViewerContext: FileViewerViewModelContext { FileViewerViewModelContext( contentType: originalRequest?.contentType, originalSize: requestBodySize, @@ -25,7 +34,7 @@ extension NetworkTaskEntity { ) } - var responseFileViewerContext: FileViewerViewModelContext { + package var responseFileViewerContext: FileViewerViewModelContext { FileViewerViewModelContext( contentType: response?.contentType, originalSize: responseBodySize, @@ -38,7 +47,7 @@ extension NetworkTaskEntity { /// - returns `nil` if the task is an unknown state. It may happen if the /// task is pending, but it's from the previous app run. - func state(in store: LoggerStore?) -> NetworkTaskEntity.State? { + package func state(in store: LoggerStore?) -> NetworkTaskEntity.State? { let state = self.state if state == .pending, let store, self.session != store.session.id { return nil diff --git a/Sources/PulseUI/Helpers/Formatters.swift b/Sources/PulseUI/Helpers/Formatters.swift index fb7c91d7a..4cca97c76 100644 --- a/Sources/PulseUI/Helpers/Formatters.swift +++ b/Sources/PulseUI/Helpers/Formatters.swift @@ -5,12 +5,12 @@ import Foundation import Pulse -enum DurationFormatter { - static func string(from timeInterval: TimeInterval) -> String { +package enum DurationFormatter { + package static func string(from timeInterval: TimeInterval) -> String { string(from: timeInterval, isPrecise: true) } - static func string(from timeInterval: TimeInterval, isPrecise: Bool) -> String { + package static func string(from timeInterval: TimeInterval, isPrecise: Bool) -> String { if timeInterval < 0.95 { if isPrecise { return String(format: "%.1f ms", timeInterval * 1000) @@ -32,14 +32,14 @@ enum DurationFormatter { extension DateFormatter { /// With timezone, so that if it's shared, we know the exact time. - static let fullDateFormatter: DateFormatter = { + package static let fullDateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US") dateFormatter.dateFormat = "MMM d, yyyy 'at' h:mm:ss a '('z')'" return dateFormatter }() - convenience init(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, isRelative: Bool = false) { + package convenience init(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, isRelative: Bool = false) { self.init() self.dateStyle = dateStyle self.timeStyle = timeStyle @@ -92,7 +92,7 @@ enum ConsoleFormatter { // MARK: Individual Components static func time(for date: Date) -> String { - if #available(iOS 15, *) { + if #available(iOS 16, *) { return ConsoleMessageCell.timeFormatter.string(from: date) } else { return "" @@ -142,12 +142,12 @@ enum ConsoleFormatter { } } -enum StatusCodeFormatter { - static func string(for statusCode: Int32) -> String { +package enum StatusCodeFormatter { + package static func string(for statusCode: Int32) -> String { string(for: Int(statusCode)) } - static func string(for statusCode: Int) -> String { + package static func string(for statusCode: Int) -> String { switch statusCode { case 0: return "Success" case 200: return "200 OK" @@ -159,8 +159,8 @@ enum StatusCodeFormatter { } } -enum ErrorFormatter { - static func shortErrorDescription(for task: NetworkTaskEntity) -> String { +package enum ErrorFormatter { + package static func shortErrorDescription(for task: NetworkTaskEntity) -> String { if task.errorCode != 0 { if task.errorDomain == URLError.errorDomain { return descriptionForURLErrorCode(Int(task.errorCode)) @@ -176,19 +176,19 @@ enum ErrorFormatter { } extension ByteCountFormatter { - static func string(fromBodySize count: Int64) -> String? { + package static func string(fromBodySize count: Int64) -> String? { guard count > 0 else { return nil } return string(fromByteCount: count) } - static func string(fromByteCount count: Int64) -> String { + package static func string(fromByteCount count: Int64) -> String { ByteCountFormatter.string(fromByteCount: count, countStyle: .file) } } -enum CountFormatter { +package enum CountFormatter { private static let numberFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 @@ -196,7 +196,7 @@ enum CountFormatter { return formatter }() - static func string(from count: Int) -> String { + package static func string(from count: Int) -> String { if count < 1000 { return "\(count)" } let number = NSNumber(floatLiteral: Double(count) / 1000.0) return (numberFormatter.string(from: number) ?? "–") + "k" diff --git a/Sources/PulseUI/Helpers/LoggerStoreIndex.swift b/Sources/PulseUI/Helpers/LoggerStoreIndex.swift index 82eb942a7..72152b262 100644 --- a/Sources/PulseUI/Helpers/LoggerStoreIndex.swift +++ b/Sources/PulseUI/Helpers/LoggerStoreIndex.swift @@ -8,16 +8,16 @@ import Combine import Pulse /// Keeps track of hosts, paths, etc. -final class LoggerStoreIndex: ObservableObject { - @Published private(set) var labels: Set = [] - @Published private(set) var files: Set = [] - @Published private(set) var hosts: Set = [] - @Published private(set) var paths: Set = [] +package final class LoggerStoreIndex: ObservableObject { + @Published private(set) package var labels: Set = [] + @Published private(set) package var files: Set = [] + @Published private(set) package var hosts: Set = [] + @Published private(set) package var paths: Set = [] private let context: NSManagedObjectContext private var cancellable: AnyCancellable? - convenience init(store: LoggerStore) { + convenience package init(store: LoggerStore) { self.init(context: store.backgroundContext) store.backgroundContext.perform { @@ -28,7 +28,7 @@ final class LoggerStoreIndex: ObservableObject { } } - init(context: NSManagedObjectContext) { + package init(context: NSManagedObjectContext) { self.context = context context.perform { self.prepopulate() diff --git a/Sources/PulseUI/Helpers/ManagedObjectsCountObserver.swift b/Sources/PulseUI/Helpers/ManagedObjectsCountObserver.swift index bbcbad46b..af6afb717 100644 --- a/Sources/PulseUI/Helpers/ManagedObjectsCountObserver.swift +++ b/Sources/PulseUI/Helpers/ManagedObjectsCountObserver.swift @@ -4,12 +4,12 @@ import CoreData -final class ManagedObjectsCountObserver: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { - let controller: NSFetchedResultsController +package final class ManagedObjectsCountObserver: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { + package let controller: NSFetchedResultsController - @Published private(set) var count = 0 + @Published private(set) package var count = 0 - init(entity: T.Type, context: NSManagedObjectContext, sortDescriptior: NSSortDescriptor) { + package init(entity: T.Type, context: NSManagedObjectContext, sortDescriptior: NSSortDescriptor) { let request = NSFetchRequest(entityName: "\(T.self)") request.fetchBatchSize = 1 request.sortDescriptors = [sortDescriptior] @@ -22,17 +22,17 @@ final class ManagedObjectsCountObserver: NSObject, ObservableObject, NSFetchedRe self.refresh() } - func setPredicate(_ predicate: NSPredicate?) { + package func setPredicate(_ predicate: NSPredicate?) { controller.fetchRequest.predicate = predicate refresh() } - func refresh() { + package func refresh() { try? controller.performFetch() self.count = controller.fetchedObjects?.count ?? 0 } - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + package func controllerDidChangeContent(_ controller: NSFetchedResultsController) { self.count = controller.fetchedObjects?.count ?? 0 } } diff --git a/Sources/PulseUI/Helpers/ManagedObjectsObserver.swift b/Sources/PulseUI/Helpers/ManagedObjectsObserver.swift index 19b57e1de..8cae28dda 100644 --- a/Sources/PulseUI/Helpers/ManagedObjectsObserver.swift +++ b/Sources/PulseUI/Helpers/ManagedObjectsObserver.swift @@ -8,12 +8,12 @@ import Pulse import Combine import SwiftUI -final class ManagedObjectsObserver: NSObject, NSFetchedResultsControllerDelegate { - @Published private(set) var objects: [T] = [] +package final class ManagedObjectsObserver: NSObject, NSFetchedResultsControllerDelegate { + @Published private(set) package var objects: [T] = [] private let controller: NSFetchedResultsController - init(request: NSFetchRequest, + package init(request: NSFetchRequest, context: NSManagedObjectContext, cacheName: String? = nil) { self.controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: cacheName) @@ -25,13 +25,13 @@ final class ManagedObjectsObserver: NSObject, NSFetchedResul controller.delegate = self } - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + package func controllerDidChangeContent(_ controller: NSFetchedResultsController) { objects = self.controller.fetchedObjects ?? [] } } extension ManagedObjectsObserver where T == LoggerSessionEntity { - static func sessions(for context: NSManagedObjectContext) -> ManagedObjectsObserver { + package static func sessions(for context: NSManagedObjectContext) -> ManagedObjectsObserver { let request = NSFetchRequest(entityName: "\(LoggerSessionEntity.self)") request.sortDescriptors = [NSSortDescriptor(keyPath: \LoggerSessionEntity.createdAt, ascending: false)] diff --git a/Sources/PulseUI/Helpers/ShareItems.swift b/Sources/PulseUI/Helpers/ShareItems.swift index 42bf84a7a..8da64428c 100644 --- a/Sources/PulseUI/Helpers/ShareItems.swift +++ b/Sources/PulseUI/Helpers/ShareItems.swift @@ -28,23 +28,23 @@ public enum ShareStoreOutput: String, RawRepresentable, Codable, CaseIterable { } } -public struct ShareItems: Identifiable { - public let id = UUID() - public let items: [Any] - public let size: Int64? - public let cleanup: () -> Void +package struct ShareItems: Identifiable { + package let id = UUID() + package let items: [Any] + package let size: Int64? + package let cleanup: () -> Void - init(_ items: [Any], size: Int64? = nil, cleanup: @escaping () -> Void = { }) { + package init(_ items: [Any], size: Int64? = nil, cleanup: @escaping () -> Void = { }) { self.items = items self.size = size self.cleanup = cleanup } } -enum ShareService { +package enum ShareService { private static var task: ShareStoreTask? - static func share(_ entities: [NSManagedObject], store: LoggerStore, as output: ShareOutput) async throws -> ShareItems { + package static func share(_ entities: [NSManagedObject], store: LoggerStore, as output: ShareOutput) async throws -> ShareItems { try await withUnsafeThrowingContinuation { continuation in ShareStoreTask(entities: entities, store: store, output: output) { if let value = $0 { @@ -56,17 +56,17 @@ enum ShareService { } } - static func share(_ message: LoggerMessageEntity, as output: ShareOutput) -> ShareItems { + package static func share(_ message: LoggerMessageEntity, as output: ShareOutput) -> ShareItems { let string = TextRenderer(options: .sharing).make { $0.render(message) } return share(string, as: output) } - static func share(_ task: NetworkTaskEntity, as output: ShareOutput, store: LoggerStore) -> ShareItems { + package static func share(_ task: NetworkTaskEntity, as output: ShareOutput, store: LoggerStore) -> ShareItems { let string = TextRenderer(options: .sharing).make { $0.render(task, content: .sharing, store: store) } return share(string, as: output) } - static func share(_ string: NSAttributedString, as output: ShareOutput) -> ShareItems { + package static func share(_ string: NSAttributedString, as output: ShareOutput) -> ShareItems { let string = sanitized(string, as: output) switch output { case .plainText: @@ -94,7 +94,7 @@ enum ShareService { } } - static func sanitized(_ string: NSAttributedString, as shareOutput: ShareOutput) -> NSAttributedString { + package static func sanitized(_ string: NSAttributedString, as shareOutput: ShareOutput) -> NSAttributedString { var ranges: [NSRange] = [] string.enumerateAttribute(.isTechnical, in: NSRange(location: 0, length: string.length)) { value, range, _ in if (value as? Bool) == true { diff --git a/Sources/PulseUI/Helpers/StatusLabelViewModel.swift b/Sources/PulseUI/Helpers/StatusLabelViewModel.swift index 547555144..1bd33facb 100644 --- a/Sources/PulseUI/Helpers/StatusLabelViewModel.swift +++ b/Sources/PulseUI/Helpers/StatusLabelViewModel.swift @@ -6,12 +6,12 @@ import Foundation import SwiftUI import Pulse -struct StatusLabelViewModel { - let systemImage: String - let tint: Color - let title: String +package struct StatusLabelViewModel { + package let systemImage: String + package let tint: Color + package let title: String - init(task: NetworkTaskEntity, store: LoggerStore?) { + package init(task: NetworkTaskEntity, store: LoggerStore?) { guard let state = task.state(in: store) else { self.systemImage = "questionmark.diamond.fill" self.tint = .secondary @@ -34,7 +34,7 @@ struct StatusLabelViewModel { } } - init(transaction: NetworkTransactionMetricsEntity) { + package init(transaction: NetworkTransactionMetricsEntity) { if let response = transaction.response { if response.isSuccess { self.systemImage = "checkmark.circle.fill" @@ -52,7 +52,7 @@ struct StatusLabelViewModel { } } - var text: Text { + package var text: Text { (Text(Image(systemName: systemImage)) + Text(" " + title)) .foregroundColor(tint) } diff --git a/Sources/PulseUI/Helpers/StringSearchOptions.swift b/Sources/PulseUI/Helpers/StringSearchOptions.swift index 60c95da8e..7511e709a 100644 --- a/Sources/PulseUI/Helpers/StringSearchOptions.swift +++ b/Sources/PulseUI/Helpers/StringSearchOptions.swift @@ -4,31 +4,37 @@ import Foundation -struct StringSearchOptions: Equatable, Hashable, Codable { - var kind: Kind = .text - var caseSensitivity: CaseSensitivity = .ignoringCase - var rule: MatchingRule = .contains +package struct StringSearchOptions: Equatable, Hashable, Codable { + package var kind: Kind + package var caseSensitivity: CaseSensitivity + package var rule: MatchingRule - static let `default` = StringSearchOptions() + package static let `default` = StringSearchOptions() - enum Kind: String, Hashable, Codable, CaseIterable { + package init(kind: Kind = .text, caseSensitivity: CaseSensitivity = .ignoringCase, rule: MatchingRule = .contains) { + self.kind = kind + self.caseSensitivity = caseSensitivity + self.rule = rule + } + + package enum Kind: String, Hashable, Codable, CaseIterable { case text = "Text" case wildcard = "Wildcard" case regex = "Regular Expression" } - enum CaseSensitivity: String, Hashable, Codable, CaseIterable { + package enum CaseSensitivity: String, Hashable, Codable, CaseIterable { case ignoringCase = "Ignoring Case" case matchingCase = "Matching Case" } - enum MatchingRule: String, Equatable, Hashable, Codable, CaseIterable { + package enum MatchingRule: String, Equatable, Hashable, Codable, CaseIterable { case begins = "Begins With" case contains = "Contains" case ends = "Ends With" } - var title: String { + package var title: String { switch kind { case .text: return rule.rawValue case .wildcard: return "Contains" @@ -36,7 +42,7 @@ struct StringSearchOptions: Equatable, Hashable, Codable { } } - func allEligibleMatchingRules() -> [MatchingRule]? { + package func allEligibleMatchingRules() -> [MatchingRule]? { switch kind { case .text, .wildcard: return MatchingRule.allCases case .regex: return nil @@ -45,7 +51,7 @@ struct StringSearchOptions: Equatable, Hashable, Codable { } extension String.CompareOptions { - init(_ options: StringSearchOptions) { + package init(_ options: StringSearchOptions) { self.init() if options.kind == .regex || options.kind == .wildcard { insert(.regularExpression) @@ -72,13 +78,13 @@ extension String.CompareOptions { extension String { /// Returns first range of substring. - func firstRange(of substring: String, options: String.CompareOptions = []) -> Range? { + package func firstRange(of substring: String, options: String.CompareOptions = []) -> Range? { range(of: substring, options: options, range: startIndex.. [Range] { + package func ranges(of target: String, options: StringSearchOptions) -> [Range] { var startIndex = target.startIndex var ranges = [Range]() let target = options.kind == .wildcard ? makeRegexForWildcard(target, rule: options.rule) : target @@ -93,7 +99,7 @@ extension String { } extension NSString { - func ranges(of substring: String, options: StringSearchOptions) -> [NSRange] { + package func ranges(of substring: String, options: StringSearchOptions) -> [NSRange] { var index = 0 var ranges = [NSRange]() let substring = options.kind == .wildcard ? makeRegexForWildcard(substring, rule: options.rule) : substring diff --git a/Sources/PulseUI/Helpers/TextHelper.swift b/Sources/PulseUI/Helpers/TextHelper.swift index d99d33111..e166ebdbc 100644 --- a/Sources/PulseUI/Helpers/TextHelper.swift +++ b/Sources/PulseUI/Helpers/TextHelper.swift @@ -6,13 +6,13 @@ import Foundation import SwiftUI /// Manages text attributes. -final class TextHelper { +package final class TextHelper { private var cachedAttributes: [AttributesKey: [NSAttributedString.Key: Any]] = [:] private var cachedFonts: [TextStyle: UXFont] = [:] - init() {} + package init() {} - func attributes( + package func attributes( role: TextRole, style: TextFontStyle = .proportional, weight: UXFont.Weight = .regular, @@ -26,7 +26,7 @@ final class TextHelper { .font: scaled(font: UXFont.systemFont(ofSize: 10)) ] - func attributes(style: TextStyle, color: UXColor?) -> [NSAttributedString.Key: Any] { + package func attributes(style: TextStyle, color: UXColor?) -> [NSAttributedString.Key: Any] { let key = AttributesKey(textStyle: style, color: color) if let attributes = cachedAttributes[key] { return attributes @@ -36,7 +36,7 @@ final class TextHelper { return attributes } - func font(style: TextStyle) -> UXFont { + package func font(style: TextStyle) -> UXFont { if let font = cachedFonts[style] { return font } @@ -120,14 +120,21 @@ final class TextHelper { } } -struct TextStyle: Hashable { - var role: TextRole - var style: TextFontStyle = .proportional - var weight: UXFont.Weight = .regular - var width: TextWidth = .standard +package struct TextStyle: Hashable { + package var role: TextRole + package var style: TextFontStyle + package var weight: UXFont.Weight + package var width: TextWidth + + package init(role: TextRole, style: TextFontStyle = .proportional, weight: UXFont.Weight = .regular, width: TextWidth = .standard) { + self.role = role + self.style = style + self.weight = weight + self.width = width + } } -enum TextRole { +package enum TextRole { /// Large title. case title /// Section headline (small). @@ -141,17 +148,17 @@ enum TextRole { /// Smaller body for console and other views where information has to be /// condensed. /// - /// Font size: iOS 15, macOS 12, tvOS 26, watchOS 14. + /// Font size: iOS 16, macOS 12, tvOS 26, watchOS 14. case body2 } -enum TextFontStyle { +package enum TextFontStyle { case proportional case monospaced case monospacedDigital } -enum TextWidth { +package enum TextWidth { case condensed case standard } diff --git a/Sources/PulseUI/Helpers/TextRenderer.swift b/Sources/PulseUI/Helpers/TextRenderer.swift index 8ac808606..083a55da8 100644 --- a/Sources/PulseUI/Helpers/TextRenderer.swift +++ b/Sources/PulseUI/Helpers/TextRenderer.swift @@ -12,14 +12,18 @@ import PDFKit #endif /// Low-level attributed string creation API. -final class TextRenderer { - struct Options { - var color: ColorMode = .full +package final class TextRenderer { + package struct Options { + package var color: ColorMode - static let sharing = Options(color: .automatic) + package init(color: ColorMode = .full) { + self.color = color + } + + package static let sharing = Options(color: .automatic) } - enum ColorMode: String, RawRepresentable { + package enum ColorMode: String, RawRepresentable { case monochrome case automatic case full @@ -33,35 +37,35 @@ final class TextRenderer { var renderedBodies: [NSManagedObjectID: NSAttributedString] = [:] private var string = NSMutableAttributedString() - init(options: Options = .init()) { + package init(options: Options = .init()) { self.options = options self.helper = TextHelper() } - func make(_ render: (TextRenderer) -> Void) -> NSAttributedString { + package func make(_ render: (TextRenderer) -> Void) -> NSAttributedString { render(self) return make() } - func make() -> NSMutableAttributedString { + package func make() -> NSMutableAttributedString { defer { string = NSMutableAttributedString() } return string } - func addSpacer() { + package func addSpacer() { string.append(spacer()) } - func spacer() -> NSAttributedString { + package func spacer() -> NSAttributedString { NSAttributedString(string: "\n", attributes: helper.spacerAttributes) } - func render(_ message: LoggerMessageEntity) { + package func render(_ message: LoggerMessageEntity) { string.append(ConsoleFormatter.subheadline(for: message) + "\n", helper.attributes(role: .subheadline, style: .monospacedDigital, width: .condensed, color: .secondaryLabel)) string.append(message.text + "\n", helper.attributes(role: .body2, color: textColor(for: message.logLevel))) } - func renderCompact(_ message: LoggerMessageEntity) { + package func renderCompact(_ message: LoggerMessageEntity) { var details = ConsoleFormatter.time(for: message.createdAt) if let label = ConsoleFormatter.label(for: message) { details += "\(ConsoleFormatter.separator)\(label)\(ConsoleFormatter.separator)" @@ -85,7 +89,7 @@ final class TextRenderer { } } - func render(_ task: NetworkTaskEntity, content: NetworkContent, store: LoggerStore) { + package func render(_ task: NetworkTaskEntity, content: NetworkContent, store: LoggerStore) { if content.contains(.largeHeader) { renderLargeHeader(for: task, store: store) } else if content.contains(.header) { @@ -182,7 +186,7 @@ final class TextRenderer { addSpacer() } - func renderCompact(_ task: NetworkTaskEntity, store: LoggerStore) { + package func renderCompact(_ task: NetworkTaskEntity, store: LoggerStore) { let isTitleColored = task.state == .failure && options.color != .monochrome let titleColor = isTitleColored ? UXColor.systemRed : UXColor.secondaryLabel let detailsColor = isTitleColored ? UXColor.systemRed : UXColor.label @@ -199,7 +203,7 @@ final class TextRenderer { string.append((task.httpMethod ?? "GET") + " " + (task.url ?? "–") + "\n", urlAttributes) } - func render(_ transaction: NetworkTransactionMetricsEntity) { + package func render(_ transaction: NetworkTransactionMetricsEntity) { do { let status = StatusLabelViewModel(transaction: transaction) let method = transaction.request.httpMethod ?? "GET" @@ -233,7 +237,7 @@ final class TextRenderer { string.deleteCharacters(in: NSRange(location: string.length - 1, length: 1)) } - func render(subheadline: String) -> NSAttributedString { + package func render(subheadline: String) -> NSAttributedString { render(subheadline + "\n", role: .subheadline, color: .secondaryLabel) } @@ -263,17 +267,17 @@ final class TextRenderer { } } - func render(json: Any, error: NetworkLogger.DecodingError? = nil) -> NSAttributedString { + package func render(json: Any, error: NetworkLogger.DecodingError? = nil) -> NSAttributedString { TextRendererJSON(json: json, error: error, options: options).render() } - func render(_ blob: LoggerBlobHandleEntity, _ data: Data, contentType: NetworkLogger.ContentType?, error: NetworkLogger.DecodingError?) -> NSAttributedString { + package func render(_ blob: LoggerBlobHandleEntity, _ data: Data, contentType: NetworkLogger.ContentType?, error: NetworkLogger.DecodingError?) -> NSAttributedString { let string = render(data, contentType: contentType, error: error) renderedBodies[blob.objectID] = string return string } - func render(_ data: Data, contentType: NetworkLogger.ContentType?, error: NetworkLogger.DecodingError?) -> NSAttributedString { + package func render(_ data: Data, contentType: NetworkLogger.ContentType?, error: NetworkLogger.DecodingError?) -> NSAttributedString { if let json = try? JSONSerialization.jsonObject(with: data, options: []) { return render(json: json, error: error) } @@ -307,7 +311,7 @@ final class TextRenderer { return KeyValueSectionViewModel.makeQueryItems(for: queryItems) } - func render(_ sections: [KeyValueSectionViewModel]) { + package func render(_ sections: [KeyValueSectionViewModel]) { for (index, section) in sections.enumerated() { string.append(render(section)) if index != sections.endIndex - 1 { @@ -316,7 +320,7 @@ final class TextRenderer { } } - func render(_ section: KeyValueSectionViewModel, details: String? = nil, style: TextFontStyle = .monospaced) -> NSAttributedString { + package func render(_ section: KeyValueSectionViewModel, details: String? = nil, style: TextFontStyle = .monospaced) -> NSAttributedString { let details = details.map { "(\($0))" } let title = [section.title, details].compactMap { $0 }.joined(separator: " ") @@ -332,7 +336,7 @@ final class TextRenderer { return string } - func render(_ values: [(String, String?)]?, color: Color, style: TextFontStyle = .monospaced) -> NSAttributedString { + package func render(_ values: [(String, String?)]?, color: Color, style: TextFontStyle = .monospaced) -> NSAttributedString { guard let values = values, !values.isEmpty else { return NSAttributedString(string: "–\n", attributes: helper.attributes(role: .body2, style: style)) } @@ -385,15 +389,15 @@ final class TextRenderer { return output } - func preformatted(_ string: String, color: UXColor? = nil) -> NSAttributedString { + package func preformatted(_ string: String, color: UXColor? = nil) -> NSAttributedString { render(string, role: .body2, style: .monospaced, color: color ?? .label) } - func append(_ string: NSAttributedString) { + package func append(_ string: NSAttributedString) { self.string.append(string) } - func render( + package func render( _ string: String, role: TextRole, style: TextFontStyle = .proportional, @@ -407,9 +411,9 @@ final class TextRenderer { } extension NSAttributedString.Key { - static let objectId = NSAttributedString.Key("pulse-object-id-key") - static let isTechnical = NSAttributedString.Key("pulse-technical-substring-key") - static let subheadline = NSAttributedString.Key("pulse-subheadline-key") + package static let objectId = NSAttributedString.Key("pulse-object-id-key") + package static let isTechnical = NSAttributedString.Key("pulse-technical-substring-key") + package static let subheadline = NSAttributedString.Key("pulse-subheadline-key") } // MARK: - Previews @@ -463,35 +467,36 @@ struct ConsoleTextRenderer_Previews: PreviewProvider { private let task = LoggerStore.preview.entity(for: .login) #endif -struct NetworkContent: OptionSet { - let rawValue: Int16 - - static let header = NetworkContent(rawValue: 1 << 0) - static let largeHeader = NetworkContent(rawValue: 1 << 1) - static let taskDetails = NetworkContent(rawValue: 1 << 2) - static let requestComponents = NetworkContent(rawValue: 1 << 3) - static let requestQueryItems = NetworkContent(rawValue: 1 << 4) - static let errorDetails = NetworkContent(rawValue: 1 << 5) - static let originalRequestHeaders = NetworkContent(rawValue: 1 << 6) - static let currentRequestHeaders = NetworkContent(rawValue: 1 << 7) - static let requestOptions = NetworkContent(rawValue: 1 << 8) - static let requestBody = NetworkContent(rawValue: 1 << 9) - static let responseHeaders = NetworkContent(rawValue: 1 << 10) - static let responseBody = NetworkContent(rawValue: 1 << 11) - - static let sharing: NetworkContent = [ +package struct NetworkContent: OptionSet { + package let rawValue: Int16 + package init(rawValue: Int16) { self.rawValue = rawValue } + + package static let header = NetworkContent(rawValue: 1 << 0) + package static let largeHeader = NetworkContent(rawValue: 1 << 1) + package static let taskDetails = NetworkContent(rawValue: 1 << 2) + package static let requestComponents = NetworkContent(rawValue: 1 << 3) + package static let requestQueryItems = NetworkContent(rawValue: 1 << 4) + package static let errorDetails = NetworkContent(rawValue: 1 << 5) + package static let originalRequestHeaders = NetworkContent(rawValue: 1 << 6) + package static let currentRequestHeaders = NetworkContent(rawValue: 1 << 7) + package static let requestOptions = NetworkContent(rawValue: 1 << 8) + package static let requestBody = NetworkContent(rawValue: 1 << 9) + package static let responseHeaders = NetworkContent(rawValue: 1 << 10) + package static let responseBody = NetworkContent(rawValue: 1 << 11) + + package static let sharing: NetworkContent = [ largeHeader, taskDetails, errorDetails, currentRequestHeaders, requestBody, responseHeaders, responseBody ] - static let preview: NetworkContent = [ + package static let preview: NetworkContent = [ largeHeader, taskDetails, errorDetails, responseBody ] - static let summary: NetworkContent = [ + package static let summary: NetworkContent = [ largeHeader, taskDetails, errorDetails, requestComponents, requestQueryItems, errorDetails, originalRequestHeaders, currentRequestHeaders, requestOptions, responseHeaders ] - static let all: NetworkContent = [ + package static let all: NetworkContent = [ largeHeader, taskDetails, errorDetails, requestComponents, requestQueryItems, errorDetails, originalRequestHeaders, currentRequestHeaders, requestOptions, requestBody, responseHeaders, responseBody ] } diff --git a/Sources/PulseUI/Helpers/TextRendererJSON.swift b/Sources/PulseUI/Helpers/TextRendererJSON.swift index 9815f3d2a..53fabd0be 100644 --- a/Sources/PulseUI/Helpers/TextRendererJSON.swift +++ b/Sources/PulseUI/Helpers/TextRendererJSON.swift @@ -9,7 +9,7 @@ import Pulse import AppKit #endif -final class TextRendererJSON { +package final class TextRendererJSON { // Input private let json: Any private var error: NetworkLogger.DecodingError? @@ -27,14 +27,14 @@ final class TextRendererJSON { private var errorRange: NSRange? private var string = "" - init(json: Any, error: NetworkLogger.DecodingError? = nil, options: TextRenderer.Options = .init()) { + package init(json: Any, error: NetworkLogger.DecodingError? = nil, options: TextRenderer.Options = .init()) { self.options = options self.helper = TextHelper() self.json = json self.error = error } - func render() -> NSAttributedString { + package func render() -> NSAttributedString { render(json: json, isFree: true) let output = NSMutableAttributedString(string: string, attributes: helper.attributes(role: .body2, style: .monospaced, color: color(for: .key))) @@ -210,8 +210,7 @@ final class TextRendererJSON { // MARK: Error - /// - note: Pulse Pro - static var makeErrorAttributes: ((Error) -> [NSAttributedString.Key: Any])? + package static var makeErrorAttributes: ((Error) -> [NSAttributedString.Key: Any])? func makeErrorAttributes() -> [NSAttributedString.Key: Any] { guard let error = error else { @@ -239,26 +238,26 @@ final class TextRendererJSON { } } -struct JSONColors { - static let punctuation = UXColor.dynamic( +package struct JSONColors { + package static let punctuation = UXColor.dynamic( light: .init(red: 113.0/255.0, green: 128.0/255.0, blue: 141.0/255.0, alpha: 1.0), dark: .init(red: 113.0/255.0, green: 128.0/255.0, blue: 141.0/255.0, alpha: 1.0) ) - static let key = UXColor.label - static let valueString = Palette.red - static let valueOther = UXColor.dynamic( + package static let key = UXColor.label + package static let valueString = Palette.red + package static let valueOther = UXColor.dynamic( light: .init(red: 28.0/255.0, green: 0.0/255.0, blue: 207.0/255.0, alpha: 1.0), dark: .init(red: 208.0/255.0, green: 191.0/255.0, blue: 105.0/255.0, alpha: 1.0) ) - static let null = Palette.pink + package static let null = Palette.pink } extension NSAttributedString.Key { - static let decodingError = NSAttributedString.Key(rawValue: "com.github.kean.pulse.decoding-error-key") - static let node = NSAttributedString.Key(rawValue: "com.github.kean.pulse.json-container-node") + package static let decodingError = NSAttributedString.Key(rawValue: "com.github.kean.pulse.decoding-error-key") + package static let node = NSAttributedString.Key(rawValue: "com.github.kean.pulse.json-container-node") } -enum JSONElement { +package enum JSONElement { case punctuation case key case valueString @@ -266,18 +265,18 @@ enum JSONElement { case null } -final class JSONContainerNode { - enum Kind { +package final class JSONContainerNode { + package enum Kind { case object case array } - let kind: Kind - let json: Any - var isExpanded = true - var expanded: NSAttributedString? + package let kind: Kind + package let json: Any + package var isExpanded = true + package var expanded: NSAttributedString? - init(kind: Kind, json: Any) { + package init(kind: Kind, json: Any) { self.kind = kind self.json = json } diff --git a/Sources/PulseUI/Helpers/UIKit+Extensions.swift b/Sources/PulseUI/Helpers/UIKit+Extensions.swift index 31b5d8d23..1659fe722 100644 --- a/Sources/PulseUI/Helpers/UIKit+Extensions.swift +++ b/Sources/PulseUI/Helpers/UIKit+Extensions.swift @@ -19,7 +19,7 @@ extension UIView { } extension UIApplication { - static var keyWindow: UIWindow? { + package static var keyWindow: UIWindow? { shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } diff --git a/Sources/PulseUI/Helpers/UXKit.swift b/Sources/PulseUI/Helpers/UXKit.swift index 3e23bbcd1..9c1a2cf4e 100644 --- a/Sources/PulseUI/Helpers/UXKit.swift +++ b/Sources/PulseUI/Helpers/UXKit.swift @@ -13,16 +13,16 @@ import SwiftUI // A set of typealias and APIs to make AppKit and UIKit more // compatible with each other -struct Palette { +package struct Palette { #if os(watchOS) - static var red: UXColor { Palette.darkRed } - static var pink: UXColor { Palette.darkPink } + package static var red: UXColor { Palette.darkRed } + package static var pink: UXColor { Palette.darkPink } #else - static var red: UXColor { + package static var red: UXColor { UXColor.dynamic(light: Palette.lightRed, dark: Palette.darkRed) } - static var pink: UXColor { + package static var pink: UXColor { UXColor.dynamic(light: Palette.lightPink, dark: Palette.darkPink) } #endif @@ -35,25 +35,25 @@ struct Palette { } #if os(macOS) -typealias UXColor = NSColor -typealias UXFont = NSFont -typealias UXTextView = NSTextView -typealias UXImage = NSImage -typealias UXPasteboard = NSPasteboard +package typealias UXColor = NSColor +package typealias UXFont = NSFont +package typealias UXTextView = NSTextView +package typealias UXImage = NSImage +package typealias UXPasteboard = NSPasteboard extension NSColor { - static var separator: NSColor { separatorColor } - static var label: NSColor { labelColor } - static var systemBackground: NSColor { windowBackgroundColor } - static var secondaryLabel: NSColor { secondaryLabelColor } - static var secondarySystemFill: NSColor { controlBackgroundColor } + package static var separator: NSColor { separatorColor } + package static var label: NSColor { labelColor } + package static var systemBackground: NSColor { windowBackgroundColor } + package static var secondaryLabel: NSColor { secondaryLabelColor } + package static var secondarySystemFill: NSColor { controlBackgroundColor } static var systemGray4: NSColor { systemGray.withAlphaComponent(0.7) } static var systemGray3: NSColor { systemGray.withAlphaComponent(0.8) } static var systemGray2: NSColor { systemGray.withAlphaComponent(0.9) } } extension NSColor { - static func dynamic(light: NSColor, dark: NSColor) -> NSColor { + package static func dynamic(light: NSColor, dark: NSColor) -> NSColor { NSColor(name: nil) { switch $0.name { case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark: @@ -66,12 +66,12 @@ extension NSColor { } #else -typealias UXColor = UIColor -typealias UXFont = UIFont -typealias UXImage = UIImage +package typealias UXColor = UIColor +package typealias UXFont = UIFont +package typealias UXImage = UIImage extension UIColor { - static func dynamic(light: UIColor, dark: UIColor) -> UIColor { + package static func dynamic(light: UIColor, dark: UIColor) -> UIColor { #if !os(watchOS) UIColor { switch $0.userInterfaceStyle { @@ -90,10 +90,10 @@ extension UIColor { #endif #if os(tvOS) -typealias UXTextView = UITextView +package typealias UXTextView = UITextView #elseif os(iOS) || os(visionOS) -typealias UXTextView = UITextView -typealias UXPasteboard = UIPasteboard +package typealias UXTextView = UITextView +package typealias UXPasteboard = UIPasteboard #endif #if os(tvOS) @@ -102,7 +102,6 @@ extension UIColor { static var systemGray2: UIColor { systemGray.withAlphaComponent(0.9) } static var systemGray3: UIColor { systemGray.withAlphaComponent(0.8) } static var systemGray4: UIColor { systemGray.withAlphaComponent(0.7) } - static var controlBackgroundColor: UIColor { .clear } } #endif @@ -119,7 +118,7 @@ extension UXColor { #if os(iOS) || os(visionOS) extension UITextView { - var isAutomaticLinkDetectionEnabled: Bool { + package var isAutomaticLinkDetectionEnabled: Bool { get { dataDetectorTypes.contains(.link) } set { if newValue { @@ -149,12 +148,12 @@ func runHapticFeedback(_ type: VisionHapticFeedabackTypePlaceholder = .success) #if os(macOS) extension NSTextView { - var attributedText: NSAttributedString? { + package var attributedText: NSAttributedString? { get { nil } set { textStorage?.setAttributedString(newValue ?? NSAttributedString()) } } - var text: String { + package var text: String { get { string } set { string = newValue } } @@ -171,11 +170,11 @@ func runHapticFeedback(_ type: NSHapticFeedabackTypePlaceholder = .success) { extension Image { #if os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) - init(uxImage: UXImage) { + package init(uxImage: UXImage) { self.init(uiImage: uxImage) } #elseif os(macOS) - init(uxImage: UXImage) { + package init(uxImage: UXImage) { self.init(nsImage: uxImage) } #endif @@ -183,7 +182,7 @@ extension Image { #if os(macOS) extension NSPasteboard { - var string: String? { + package var string: String? { get { string(forType: .string) ?? "" } set { guard let newValue = newValue else { return } @@ -194,7 +193,7 @@ extension NSPasteboard { } extension NSImage { - var cgImage: CGImage? { + package var cgImage: CGImage? { cgImage(forProposedRect: nil, context: nil, hints: nil) } } diff --git a/Sources/PulseUI/Helpers/UserSettings.swift b/Sources/PulseUI/Helpers/UserSettings.swift index c85247f6e..8faaf112f 100644 --- a/Sources/PulseUI/Helpers/UserSettings.swift +++ b/Sources/PulseUI/Helpers/UserSettings.swift @@ -26,15 +26,13 @@ public final class UserSettings: ObservableObject { @AppStorage("com.github.kean.pulse.sharingOutput") public var sharingOutput: ShareStoreOutput = .store - /// HTTP headers to display in a Console. By default, empty. + // Deprecated in Pulse 5.1. + @available(*, deprecated, message: "Replaced with listDisplayOptions.header.fields and listDisplayOptions.footer.fields") public var displayHeaders: [String] { - get { decode(rawDisplayHeaders) ?? [] } - set { rawDisplayHeaders = encode(newValue) ?? "[]" } + get { [] } + set { } } - @AppStorage("com.github.kean.pulse.display.headers") - var rawDisplayHeaders: String = "[]" - /// If `true`, the network inspector will show the current request by default. /// If `false`, show the original request. @AppStorage("com.github.kean.pulse.showCurrentRequest") diff --git a/Sources/PulseUI/Mocks/MockStore.swift b/Sources/PulseUI/Mocks/MockStore.swift index 648cde6cb..c97543baa 100644 --- a/Sources/PulseUI/Mocks/MockStore.swift +++ b/Sources/PulseUI/Mocks/MockStore.swift @@ -9,15 +9,15 @@ import CoreData #if DEBUG || STANDALONE_PULSE_APP extension LoggerStore { - static let mock: LoggerStore = { + package static let mock: LoggerStore = { let store = makeMockStore() _syncPopulateStore(store) return store }() - static let preview = makeMockStore() + package static let preview = makeMockStore() - func populate() { + package func populate() { _syncPopulateStore(self) } } @@ -396,7 +396,7 @@ private func getHeadersEstimatedSize(_ headers: [String: String]?) -> Int64 { } extension LoggerStore { - func entity(for task: MockTask) -> NetworkTaskEntity { + package func entity(for task: MockTask) -> NetworkTaskEntity { var configuration = NetworkLogger.Configuration() configuration.isWaitingForDecoding = true _logTask(task, urlSession: URLSession.shared, logger: NetworkLogger(store: self, configuration: configuration)) diff --git a/Sources/PulseUI/Mocks/MockTask.swift b/Sources/PulseUI/Mocks/MockTask.swift index 8a596264b..968ec66ef 100644 --- a/Sources/PulseUI/Mocks/MockTask.swift +++ b/Sources/PulseUI/Mocks/MockTask.swift @@ -7,45 +7,45 @@ import Pulse #if DEBUG || STANDALONE_PULSE_APP -struct MockTask { - var kind: Kind = .data - let originalRequest: URLRequest - var currentRequest: URLRequest { transactions.last!.request } - let response: URLResponse - let responseBody: Data - let transactions: [Transaction] - let delay: TimeInterval - var decodingError: Error? - var taskDescription: String? - - enum Kind { +package struct MockTask { + package var kind: Kind = .data + package let originalRequest: URLRequest + package var currentRequest: URLRequest { transactions.last!.request } + package let response: URLResponse + package let responseBody: Data + package let transactions: [Transaction] + package let delay: TimeInterval + package var decodingError: Error? + package var taskDescription: String? + + package enum Kind { case data case upload(size: Int64) case download(size: Int64) } - struct Transaction { - let fetchType: URLSessionTaskMetrics.ResourceFetchType - let request: URLRequest - let response: URLResponse - let duration: TimeInterval - var isReusedConnection: Bool = false + package struct Transaction { + package let fetchType: URLSessionTaskMetrics.ResourceFetchType + package let request: URLRequest + package let response: URLResponse + package let duration: TimeInterval + package var isReusedConnection: Bool = false } - var duration: TimeInterval { + package var duration: TimeInterval { transactions.map(\.duration).reduce(0, +) } } extension MockTask { - static let allEntities: [NetworkTaskEntity] = MockTask.allTasks.map(LoggerStore.preview.entity) + package static let allEntities: [NetworkTaskEntity] = MockTask.allTasks.map(LoggerStore.preview.entity) - static var allTasks: [MockTask] = [.login, .profile, .repos, .octocat, .downloadNuke, .createAPI, .uploadPulseArchive, .patchRepo] + package static var allTasks: [MockTask] = [.login, .profile, .repos, .octocat, .downloadNuke, .createAPI, .uploadPulseArchive, .patchRepo] /// A successful request the demonstrates: /// /// - Query parameters in URL - static let login = MockTask( + package static let login = MockTask( originalRequest: mockLoginOriginalRequest, response: mockLoginResponse, responseBody: mockLoginResponseBody, @@ -58,7 +58,7 @@ extension MockTask { /// A failing request: /// /// - HTTP status code (404) that doesn't pass validation - static let profile = MockTask( + package static let profile = MockTask( originalRequest: mockProfileOriginalRequest, response: mockProfileFailureResponse, responseBody: mockProfileFailureResponseBody, @@ -71,7 +71,7 @@ extension MockTask { /// A successful request that demonstrates: /// /// - Large response body to check FileViewer performance - static let repos = MockTask( + package static let repos = MockTask( originalRequest: mockReposOriginalRequest, response: mockReposResponse, responseBody: mockReposBody, @@ -85,7 +85,7 @@ extension MockTask { /// /// - Image in the response with a respective "Content-Type" /// - Local cache lookup with further validation (302) - static let octocat = MockTask( + package static let octocat = MockTask( originalRequest: mockOctocatOriginalRequest, response: mockOctocatResponse, responseBody: mockOcotocatResponseBody, @@ -96,7 +96,7 @@ extension MockTask { delay: 3.5 ) - static let downloadNuke = MockTask( + package static let downloadNuke = MockTask( kind: .download(size: 6695689), originalRequest: mockDownloadNukeOriginalRequest, response: mockDownloadNukeResponse, @@ -108,7 +108,7 @@ extension MockTask { delay: 3.5 ) - static let uploadPulseArchive = MockTask( + package static let uploadPulseArchive = MockTask( kind: .upload(size: 21851748), originalRequest: mockUploadPulseOriginalRequest, response: mockUploadPulseResponse, @@ -120,7 +120,7 @@ extension MockTask { taskDescription: "upload-pulse-archive" ) - static let createAPI = MockTask( + package static let createAPI = MockTask( originalRequest: mockCreateAPIOriginalRequest, response: mockCreateaAPIResponse, responseBody: mockCreateaAPIBody, @@ -136,7 +136,7 @@ extension MockTask { /// /// - Contains Query Items in the response body /// - Fails with a decoding error - static let patchRepo = MockTask( + package static let patchRepo = MockTask( originalRequest: mockPatchRepoOriginalRequest, response: mockPatchRepoResponse, responseBody: mockPatchRepoResponseBody, @@ -147,7 +147,7 @@ extension MockTask { decodingError: mockPatchRepoDecodingError ) - static let networkingFailure = MockTask( + package static let networkingFailure = MockTask( originalRequest: mockLoginOriginalRequest, response: mockLoginResponse, responseBody: mockLoginResponseBody, @@ -493,7 +493,7 @@ private let mockUploadPulseResponse = HTTPURLResponse(url: "https://objects-orig // MARK: - PDF -let mockPDF = Data(base64Encoded: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==")! +package let mockPDF = Data(base64Encoded: "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg==")! // MARK: Helpers diff --git a/Sources/PulseUI/Views/Checkbox.swift b/Sources/PulseUI/Views/Checkbox.swift index 9a96f7bc7..028268e58 100644 --- a/Sources/PulseUI/Views/Checkbox.swift +++ b/Sources/PulseUI/Views/Checkbox.swift @@ -4,11 +4,16 @@ import SwiftUI -struct Checkbox: View { +package struct Checkbox: View { @Binding var isOn: Bool let label: () -> Label - var body: some View { + package init(isOn: Binding, @ViewBuilder label: @escaping () -> Label) { + self._isOn = isOn + self.label = label + } + + package var body: some View { #if os(iOS) || os(visionOS) Button(action: { isOn.toggle() }) { HStack { diff --git a/Sources/PulseUI/Views/ContextMenus.swift b/Sources/PulseUI/Views/ContextMenus.swift index e1cd8b631..864922077 100644 --- a/Sources/PulseUI/Views/ContextMenus.swift +++ b/Sources/PulseUI/Views/ContextMenus.swift @@ -9,16 +9,21 @@ import Pulse import Combine import CoreData -enum ContextMenu { - @available(iOS 15, visionOS 1.0, *) - struct MessageContextMenu: View { - let message: LoggerMessageEntity +package enum ContextMenu { + @available(iOS 16, visionOS 1, *) + package struct MessageContextMenu: View { + package let message: LoggerMessageEntity - @Binding private(set) var shareItems: ShareItems? + @Binding private(set) package var shareItems: ShareItems? @EnvironmentObject private var filters: ConsoleFiltersViewModel - var body: some View { + package init(message: LoggerMessageEntity, shareItems: Binding) { + self.message = message + self._shareItems = shareItems + } + + package var body: some View { Section { Button(action: { shareItems = ShareService.share(message, as: .plainText) }) { Label("Share", systemImage: "square.and.arrow.up") @@ -55,17 +60,29 @@ enum ContextMenu { } } - struct NetworkTaskContextMenuItems: View { + package struct NetworkTaskContextMenuItems: View { let task: NetworkTaskEntity #if os(iOS) || os(visionOS) @Binding private(set) var sharedItems: ShareItems? + + package init(task: NetworkTaskEntity, sharedItems: Binding, isDetailsView: Bool = false) { + self.task = task + self._sharedItems = sharedItems + self.isDetailsView = isDetailsView + } #else @Binding private(set) var sharedTask: NetworkTaskEntity? + + package init(task: NetworkTaskEntity, sharedTask: Binding, isDetailsView: Bool = false) { + self.task = task + self._sharedTask = sharedTask + self.isDetailsView = isDetailsView + } #endif var isDetailsView = false - var body: some View { + package var body: some View { Section { #if os(iOS) || os(visionOS) ContextMenu.NetworkTaskShareMenu(task: task, shareItems: $sharedItems) @@ -159,17 +176,21 @@ enum ContextMenu { } } - struct NetworkTaskCopyMenu: View { + package struct NetworkTaskCopyMenu: View { let task: NetworkTaskEntity - var body: some View { + package init(task: NetworkTaskEntity) { + self.task = task + } + + package var body: some View { Menu(content: content) { Label("Copy", systemImage: "doc.on.doc") } } @ViewBuilder - func content() -> some View { + package func content() -> some View { if let url = task.url { Button(action: { UXPasteboard.general.string = url @@ -208,12 +229,17 @@ enum ContextMenu { } } -struct StringSearchOptionsMenu: View { +package struct StringSearchOptionsMenu: View { @Binding private(set) var options: StringSearchOptions var isKindNeeded = true + package init(options: Binding, isKindNeeded: Bool = true) { + self._options = options + self.isKindNeeded = isKindNeeded + } + #if os(macOS) - var body: some View { + package var body: some View { Menu(content: { contents }, label: { Image(systemName: "ellipsis.circle") }) @@ -223,7 +249,7 @@ struct StringSearchOptionsMenu: View { .menuIndicator(.hidden) } #else - var body: some View { + package var body: some View { contents } #endif @@ -251,7 +277,7 @@ struct StringSearchOptionsMenu: View { } #if os(iOS) || os(visionOS) -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct OpenOnMacOverlay: View { let entity: NSManagedObject @ObservedObject var logger: RemoteLogger = .shared @@ -302,11 +328,16 @@ private func openOnMac(_ entity: NSManagedObject) { } #endif -struct AttributedStringShareMenu: View { +package struct AttributedStringShareMenu: View { @Binding var shareItems: ShareItems? let string: () -> NSAttributedString - var body: some View { + package init(shareItems: Binding, string: @escaping () -> NSAttributedString) { + self._shareItems = shareItems + self.string = string + } + + package var body: some View { Button(action: { shareItems = ShareService.share(string(), as: .plainText) }) { Label("Share as Text", systemImage: "square.and.arrow.up") } diff --git a/Sources/PulseUI/Views/DateRangePicker.swift b/Sources/PulseUI/Views/DateRangePicker.swift index 7d9dfd7fc..8b378fe6b 100644 --- a/Sources/PulseUI/Views/DateRangePicker.swift +++ b/Sources/PulseUI/Views/DateRangePicker.swift @@ -9,12 +9,18 @@ import CoreData import Pulse import Combine -struct DateRangePicker: View { - let title: String - @Binding var date: Date? +@available(iOS 16, tvOS 16, macOS 13, watchOS 9, visionOS 1, *) +package struct DateRangePicker: View { + package let title: String + @Binding package var date: Date? + + package init(title: String, date: Binding) { + self.title = title + self._date = date + } #if os(macOS) - var body: some View { + package var body: some View { HStack { Text(title + " Date") Spacer() @@ -22,19 +28,15 @@ struct DateRangePicker: View { }.frame(height: 24) } #else - var body: some View { + package var body: some View { #if os(iOS) || os(visionOS) - if #available(iOS 16, *) { - ViewThatFits { - horizontal - - VStack(alignment: .leading) { - Text(title + " Date") - contents - } - } - } else { + ViewThatFits { horizontal + + VStack(alignment: .leading) { + Text(title + " Date") + contents + } } #else horizontal diff --git a/Sources/PulseUI/Views/ImageViewer.swift b/Sources/PulseUI/Views/ImageViewer.swift index 67abef8f7..86a792575 100644 --- a/Sources/PulseUI/Views/ImageViewer.swift +++ b/Sources/PulseUI/Views/ImageViewer.swift @@ -5,10 +5,14 @@ import SwiftUI import Pulse -struct ImageViewer: View { - let viewModel: ImagePreviewViewModel +package struct ImageViewer: View { + package let viewModel: ImagePreviewViewModel + + package init(viewModel: ImagePreviewViewModel) { + self.viewModel = viewModel + } - var body: some View { + package var body: some View { VStack(spacing: 16) { ImageThumbnailView(viewModel: viewModel) @@ -22,10 +26,14 @@ struct ImageViewer: View { } } -struct ImageThumbnailView: View { +package struct ImageThumbnailView: View { let viewModel: ImagePreviewViewModel - var body: some View { + package init(viewModel: ImagePreviewViewModel) { + self.viewModel = viewModel + } + + package var body: some View { Image(uxImage: viewModel.image) .resizable() .aspectRatio(contentMode: .fit) @@ -34,12 +42,12 @@ struct ImageThumbnailView: View { } } -struct ImagePreviewViewModel { - let image: UXImage - let info: KeyValueSectionViewModel - let context: FileViewerViewModelContext +package struct ImagePreviewViewModel { + package let image: UXImage + package let info: KeyValueSectionViewModel + package let context: FileViewerViewModelContext - init(image: UXImage, data: Data, context: FileViewerViewModelContext) { + package init(image: UXImage, data: Data, context: FileViewerViewModelContext) { func intValue(for key: String) -> Int? { context.metadata?[key].flatMap { Int($0) } } diff --git a/Sources/PulseUI/Views/InfoRow.swift b/Sources/PulseUI/Views/InfoRow.swift index c977f6872..33173a32f 100644 --- a/Sources/PulseUI/Views/InfoRow.swift +++ b/Sources/PulseUI/Views/InfoRow.swift @@ -4,11 +4,16 @@ import SwiftUI -struct InfoRow: View { - let title: String - let details: String? +package struct InfoRow: View { + package let title: String + package let details: String? - var body: some View { + package init(title: String, details: String?) { + self.title = title + self.details = details + } + + package var body: some View { HStack { Text(title) .lineLimit(1) @@ -22,10 +27,15 @@ struct InfoRow: View { } } -struct KeyValueRow: Identifiable { - let id: Int - let item: (String, String?) +package struct KeyValueRow: Identifiable { + package let id: Int + package let item: (String, String?) - var title: String { item.0 } - var details: String? { item.1 } + package var title: String { item.0 } + package var details: String? { item.1 } + + package init(id: Int, item: (String, String?)) { + self.id = id + self.item = item + } } diff --git a/Sources/PulseUI/Views/KeyValueSectionViewModel.swift b/Sources/PulseUI/Views/KeyValueSectionViewModel.swift index 33c5f75dd..6f53f354e 100644 --- a/Sources/PulseUI/Views/KeyValueSectionViewModel.swift +++ b/Sources/PulseUI/Views/KeyValueSectionViewModel.swift @@ -5,14 +5,20 @@ import SwiftUI import Pulse -struct KeyValueSectionViewModel { - var title: String - var color: Color - var items: [(String, String?)] = [] +package struct KeyValueSectionViewModel { + package var title: String + package var color: Color + package var items: [(String, String?)] = [] + + package init(title: String, color: Color, items: [(String, String?)]) { + self.title = title + self.color = color + self.items = items + } } extension KeyValueSectionViewModel { - static func makeParameters(for request: NetworkRequestEntity) -> KeyValueSectionViewModel { + package static func makeParameters(for request: NetworkRequestEntity) -> KeyValueSectionViewModel { var items: [(String, String?)] = [ ("Cache Policy", request.cachePolicy.description), ("Timeout Interval", DurationFormatter.string(from: TimeInterval(request.timeoutInterval), isPrecise: false)) @@ -33,14 +39,14 @@ extension KeyValueSectionViewModel { if request.httpShouldUsePipelining { items.append(("HTTP Should Use Pipelining", request.httpShouldUsePipelining.description)) } - if #available(iOS 15, *) { + if #available(iOS 16, *) { return KeyValueSectionViewModel(title: "Options", color: .indigo, items: items) } else { return KeyValueSectionViewModel(title: "Options", color: .purple, items: items) } } - static func makeTaskDetails(for task: NetworkTaskEntity) -> KeyValueSectionViewModel { + package static func makeTaskDetails(for task: NetworkTaskEntity) -> KeyValueSectionViewModel { func format(size: Int64) -> String { size > 0 ? ByteCountFormatter.string(fromByteCount: size) : "Empty" } @@ -58,7 +64,7 @@ extension KeyValueSectionViewModel { return KeyValueSectionViewModel(title: taskType, color: .primary, items: items) } - static func makeComponents(for url: URL) -> KeyValueSectionViewModel? { + package static func makeComponents(for url: URL) -> KeyValueSectionViewModel? { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil } @@ -77,7 +83,7 @@ extension KeyValueSectionViewModel { ].filter { $0.1?.isEmpty == false }) } - static func makeHeaders(title: String, headers: [String: String]?) -> KeyValueSectionViewModel { + package static func makeHeaders(title: String, headers: [String: String]?) -> KeyValueSectionViewModel { KeyValueSectionViewModel( title: title, color: .red, @@ -88,7 +94,7 @@ extension KeyValueSectionViewModel { ) } - static func makeErrorDetails(for task: NetworkTaskEntity) -> KeyValueSectionViewModel? { + package static func makeErrorDetails(for task: NetworkTaskEntity) -> KeyValueSectionViewModel? { guard task.errorCode != 0, task.state == .failure else { return nil } @@ -109,7 +115,7 @@ extension KeyValueSectionViewModel { return "\(code) (\(descriptionForURLErrorCode(Int(code))))" } - static func makeQueryItems(for url: URL) -> KeyValueSectionViewModel? { + package static func makeQueryItems(for url: URL) -> KeyValueSectionViewModel? { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems, !queryItems.isEmpty else { @@ -118,7 +124,7 @@ extension KeyValueSectionViewModel { return makeQueryItems(for: queryItems) } - static func makeQueryItems(for queryItems: [URLQueryItem]) -> KeyValueSectionViewModel? { + package static func makeQueryItems(for queryItems: [URLQueryItem]) -> KeyValueSectionViewModel? { KeyValueSectionViewModel( title: "Query", color: .purple, @@ -126,7 +132,7 @@ extension KeyValueSectionViewModel { ) } - static func makeDetails(for transaction: NetworkTransactionMetricsEntity) -> [KeyValueSectionViewModel] { + package static func makeDetails(for transaction: NetworkTransactionMetricsEntity) -> [KeyValueSectionViewModel] { return [ makeTiming(for: transaction), makeTransferSection(for: transaction), @@ -220,7 +226,7 @@ extension KeyValueSectionViewModel { ]) } - static func makeDetails(for cookie: HTTPCookie, color: Color) -> KeyValueSectionViewModel { + package static func makeDetails(for cookie: HTTPCookie, color: Color) -> KeyValueSectionViewModel { KeyValueSectionViewModel(title: cookie.name, color: color, items: [ ("Name", cookie.name), ("Value", cookie.value), diff --git a/Sources/PulseUI/Views/LoggerStoreSizeChart.swift b/Sources/PulseUI/Views/LoggerStoreSizeChart.swift index ef4aedbf1..ab41fa063 100644 --- a/Sources/PulseUI/Views/LoggerStoreSizeChart.swift +++ b/Sources/PulseUI/Views/LoggerStoreSizeChart.swift @@ -6,12 +6,17 @@ import SwiftUI import Pulse import Charts -@available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) -struct LoggerStoreSizeChart: View { - let info: LoggerStore.Info - let sizeLimit: Int64? +@available(iOS 16, tvOS 16, macOS 13, watchOS 9, visionOS 1, *) +package struct LoggerStoreSizeChart: View { + package let info: LoggerStore.Info + package let sizeLimit: Int64? - var body: some View { + package init(info: LoggerStore.Info, sizeLimit: Int64?) { + self.info = info + self.sizeLimit = sizeLimit + } + + package var body: some View { VStack { HStack { Text("Logs") @@ -63,14 +68,14 @@ struct LoggerStoreSizeChart: View { } } -@available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +@available(iOS 16, tvOS 16, macOS 13, watchOS 9.0, visionOS 1, *) private enum Category: String, Hashable, Plottable { case messages = "Logs" case responses = "Blobs" case free = "Free" } -@available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +@available(iOS 16, tvOS 16, macOS 13, watchOS 9.0, visionOS 1, *) private struct Series: Identifiable { let category: Category let bytes: Int64 @@ -78,7 +83,7 @@ private struct Series: Identifiable { } #if DEBUG -//@available(iOS 16.0, tvOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +//@available(iOS 16, tvOS 16, macOS 13, watchOS 9.0, visionOS 1, *) //struct LoggerStoreSizeChart_Previews: PreviewProvider { // static var previews: some View { // LoggerStoreSizeChart(info: try! LoggerStore.mock.info(), sizeLimit: 512 * 1024) diff --git a/Sources/PulseUI/Views/Metrics/NetworkInspectorMetricsView.swift b/Sources/PulseUI/Views/Metrics/NetworkInspectorMetricsView.swift index 34176df89..d2cfdfd7e 100644 --- a/Sources/PulseUI/Views/Metrics/NetworkInspectorMetricsView.swift +++ b/Sources/PulseUI/Views/Metrics/NetworkInspectorMetricsView.swift @@ -9,7 +9,7 @@ import Pulse // MARK: - View -@available(iOS 15, visionOS 1, macOS 13, *) +@available(iOS 16, visionOS 1, macOS 13, *) struct NetworkInspectorMetricsView: View { let viewModel: NetworkInspectorMetricsViewModel @@ -55,7 +55,7 @@ final class NetworkInspectorMetricsViewModel { // MARK: - Preview #if DEBUG -@available(iOS 15, visionOS 1, macOS 13, *) +@available(iOS 16, visionOS 1, macOS 13, *) struct NetworkInspectorMetricsView_Previews: PreviewProvider { static var previews: some View { #if os(macOS) diff --git a/Sources/PulseUI/Views/Metrics/NetworkInspectorTransactionView.swift b/Sources/PulseUI/Views/Metrics/NetworkInspectorTransactionView.swift index e82366b7d..83660e4ee 100644 --- a/Sources/PulseUI/Views/Metrics/NetworkInspectorTransactionView.swift +++ b/Sources/PulseUI/Views/Metrics/NetworkInspectorTransactionView.swift @@ -8,7 +8,7 @@ import SwiftUI import Pulse import CoreData -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkInspectorTransactionView: View { @ObservedObject var viewModel: NetworkInspectorTransactionViewModel @@ -36,7 +36,7 @@ struct NetworkInspectorTransactionView: View { NetworkRequestInfoCell(viewModel: viewModel.requestViewModel) } - @available(iOS 15, visionOS 1.0, *) + @available(iOS 16, visionOS 1, *) private func transferSizeView(size: NetworkInspectorTransferInfoViewModel) -> some View { let font = TextHelper().font(style: .init(role: .subheadline, style: .monospacedDigital, width: .condensed)) return (Text(Image(systemName: "arrow.down.circle")) + @@ -58,17 +58,17 @@ struct NetworkInspectorTransactionView: View { // MARK: - ViewModel -final class NetworkInspectorTransactionViewModel: ObservableObject, Identifiable { - let id: NSManagedObjectID - let title: String - let transaction: NetworkTransactionMetricsEntity - let statusViewModel: NetworkRequestStatusCellModel - let timingViewModel: TimingViewModel? - let requestViewModel: NetworkRequestInfoCellViewModel - let transferSizeViewModel: NetworkInspectorTransferInfoViewModel? - let details: () -> NSAttributedString - - init(transaction: NetworkTransactionMetricsEntity, task: NetworkTaskEntity) { +package final class NetworkInspectorTransactionViewModel: ObservableObject, Identifiable { + package let id: NSManagedObjectID + package let title: String + package let transaction: NetworkTransactionMetricsEntity + package let statusViewModel: NetworkRequestStatusCellModel + package let timingViewModel: TimingViewModel? + package let requestViewModel: NetworkRequestInfoCellViewModel + package let transferSizeViewModel: NetworkInspectorTransferInfoViewModel? + package let details: () -> NSAttributedString + + package init(transaction: NetworkTransactionMetricsEntity, task: NetworkTaskEntity) { self.id = transaction.objectID self.title = transaction.fetchType.title self.transaction = transaction @@ -92,7 +92,7 @@ final class NetworkInspectorTransactionViewModel: ObservableObject, Identifiable } #if DEBUG -@available(iOS 15, visionOS 1.0, *) +@available(iOS 16, visionOS 1, *) struct NetworkInspectorTransactionView_Previews: PreviewProvider { static var previews: some View { NavigationView { diff --git a/Sources/PulseUI/Views/Metrics/NetworkInspectorTransferInfoView.swift b/Sources/PulseUI/Views/Metrics/NetworkInspectorTransferInfoView.swift index 2d752a58f..d82eb1af7 100644 --- a/Sources/PulseUI/Views/Metrics/NetworkInspectorTransferInfoView.swift +++ b/Sources/PulseUI/Views/Metrics/NetworkInspectorTransferInfoView.swift @@ -7,14 +7,20 @@ import Pulse // MARK: - View -struct NetworkInspectorTransferInfoView: View { - let viewModel: NetworkInspectorTransferInfoViewModel +package struct NetworkInspectorTransferInfoView: View { + package let viewModel: NetworkInspectorTransferInfoViewModel - var isSentHidden = false - var isReceivedHidden = false + package var isSentHidden = false + package var isReceivedHidden = false + + package init(viewModel: NetworkInspectorTransferInfoViewModel, isSentHidden: Bool = false, isReceivedHidden: Bool = false) { + self.viewModel = viewModel + self.isSentHidden = isSentHidden + self.isReceivedHidden = isReceivedHidden + } #if os(watchOS) - var body: some View { + package var body: some View { HStack(alignment: .center) { if !isSentHidden { bytesSent @@ -26,7 +32,7 @@ struct NetworkInspectorTransferInfoView: View { .frame(maxWidth: .infinity) } #else - var body: some View { + package var body: some View { HStack { Spacer() bytesSent @@ -131,16 +137,16 @@ private let mockModel = NetworkInspectorTransferInfoViewModel( // MARK: - ViewModel -struct NetworkInspectorTransferInfoViewModel { - let totalBytesSent: String - let bodyBytesSent: String - let headersBytesSent: String +package struct NetworkInspectorTransferInfoViewModel { + package let totalBytesSent: String + package let bodyBytesSent: String + package let headersBytesSent: String - let totalBytesReceived: String - let bodyBytesReceived: String - let headersBytesReceived: String + package let totalBytesReceived: String + package let bodyBytesReceived: String + package let headersBytesReceived: String - init(empty: Bool) { + package init(empty: Bool) { totalBytesSent = "–" bodyBytesSent = "–" headersBytesSent = "–" @@ -149,11 +155,11 @@ struct NetworkInspectorTransferInfoViewModel { headersBytesReceived = "–" } - init(task: NetworkTaskEntity) { + package init(task: NetworkTaskEntity) { self.init(transferSize: task.totalTransferSize) } - init(transferSize: NetworkLogger.TransferSizeInfo) { + package init(transferSize: NetworkLogger.TransferSizeInfo) { totalBytesSent = formatBytes(transferSize.totalBytesSent) bodyBytesSent = formatBytes(transferSize.requestBodyBytesSent) headersBytesSent = formatBytes(transferSize.requestHeaderBytesSent) diff --git a/Sources/PulseUI/Views/Metrics/TimingViewModel+Metrics.swift b/Sources/PulseUI/Views/Metrics/TimingViewModel+Metrics.swift index 9eae296c8..945d20b4a 100644 --- a/Sources/PulseUI/Views/Metrics/TimingViewModel+Metrics.swift +++ b/Sources/PulseUI/Views/Metrics/TimingViewModel+Metrics.swift @@ -142,7 +142,7 @@ private func makeTimingRows(transaction: NetworkTransactionMetricsEntity, taskIn } extension URLSessionTaskMetrics.ResourceFetchType { - var title: String { + package var title: String { switch self { case .networkLoad: return "Network Load" case .localCache: return "Cache Lookup" diff --git a/Sources/PulseUI/Views/PDFRepresentedView.swift b/Sources/PulseUI/Views/PDFRepresentedView.swift index 2db835ff3..adbd96e3e 100644 --- a/Sources/PulseUI/Views/PDFRepresentedView.swift +++ b/Sources/PulseUI/Views/PDFRepresentedView.swift @@ -7,33 +7,41 @@ import SwiftUI #if os(iOS) || os(visionOS) import PDFKit -struct PDFKitRepresentedView: UIViewRepresentable { - let document: PDFDocument +package struct PDFKitRepresentedView: UIViewRepresentable { + package let document: PDFDocument - func makeUIView(context: Context) -> PDFView { + package init(document: PDFDocument) { + self.document = document + } + + package func makeUIView(context: Context) -> PDFView { let pdfView = PDFView() pdfView.document = document return pdfView } - func updateUIView(_ view: PDFView, context: Context) { + package func updateUIView(_ view: PDFView, context: Context) { // Do nothing } } #elseif os(macOS) import PDFKit -struct PDFKitRepresentedView: NSViewRepresentable { - let document: PDFDocument +package struct PDFKitRepresentedView: NSViewRepresentable { + package let document: PDFDocument + + package init(document: PDFDocument) { + self.document = document + } - func makeNSView(context: Context) -> PDFView { + package func makeNSView(context: Context) -> PDFView { let pdfView = PDFView() pdfView.document = document pdfView.autoScales = true return pdfView } - func updateNSView(_ view: PDFView, context: Context) { + package func updateNSView(_ view: PDFView, context: Context) { // Do nothing } } diff --git a/Sources/PulseUI/Views/PinView.swift b/Sources/PulseUI/Views/PinView.swift new file mode 100644 index 000000000..97f460b68 --- /dev/null +++ b/Sources/PulseUI/Views/PinView.swift @@ -0,0 +1,33 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). + +import SwiftUI +import Pulse + +package struct PinView: View { + private var message: LoggerMessageEntity? + @State private var isPinned = false + + package init(message: LoggerMessageEntity?) { + self.message = message + } + + package init(task: NetworkTaskEntity) { + self.init(message: task.message) + } + + package var body: some View { + if let message = message { + Image(systemName: "pin") + .font(.footnote) + .foregroundColor(.pink) + .opacity(isPinned ? 1 : 0) + .frame(width: 8, height: 8) + .onReceive(message.publisher(for: \.isPinned).removeDuplicates()) { + isPinned = $0 + } + } + } +} diff --git a/Sources/PulseUI/Views/PlaceholderView.swift b/Sources/PulseUI/Views/PlaceholderView.swift index 992869564..27d0bd384 100644 --- a/Sources/PulseUI/Views/PlaceholderView.swift +++ b/Sources/PulseUI/Views/PlaceholderView.swift @@ -4,10 +4,10 @@ import SwiftUI -struct PlaceholderView: View { - var imageName: String? - let title: String - var subtitle: String? +package struct PlaceholderView: View { + package var imageName: String? + package let title: String + package var subtitle: String? #if os(tvOS) private let iconSize: CGFloat = 150 @@ -23,7 +23,13 @@ struct PlaceholderView: View { private let maxWidth: CGFloat = 280 #endif - var body: some View { + package init(imageName: String? = nil, title: String, subtitle: String? = nil) { + self.imageName = imageName + self.title = title + self.subtitle = subtitle + } + + package var body: some View { VStack { imageName.map(Image.init(systemName:)) .font(.system(size: iconSize, weight: .light)) diff --git a/Sources/PulseUI/Views/SearchBar-macos.swift b/Sources/PulseUI/Views/SearchBar-macos.swift index 12c75ac4d..55912c209 100644 --- a/Sources/PulseUI/Views/SearchBar-macos.swift +++ b/Sources/PulseUI/Views/SearchBar-macos.swift @@ -7,12 +7,12 @@ import SwiftUI import Combine -struct SearchBar: View { +package struct SearchBar: View { private let title: String private let imageName: String @Binding private var text: String - init(title: String, + package init(title: String, imageName: String = "magnifyingglass", text: Binding) { self.title = title @@ -20,7 +20,7 @@ struct SearchBar: View { self._text = text } - var body: some View { + package var body: some View { HStack(spacing: 6) { Image(systemName: imageName) .foregroundColor(.secondary) diff --git a/Sources/PulseUI/Views/SectionHeaderView.swift b/Sources/PulseUI/Views/SectionHeaderView.swift index 316242d7a..5094b50e5 100644 --- a/Sources/PulseUI/Views/SectionHeaderView.swift +++ b/Sources/PulseUI/Views/SectionHeaderView.swift @@ -4,11 +4,16 @@ import SwiftUI import SwiftUI -struct SectionHeaderView: View { - var systemImage: String? - let title: String +package struct SectionHeaderView: View { + package var systemImage: String? + package let title: String - var body: some View { + package init(systemImage: String? = nil, title: String) { + self.systemImage = systemImage + self.title = title + } + + package var body: some View { HStack { if let systemImage = systemImage { Image(systemName: systemImage) diff --git a/Sources/PulseUI/Views/ShareStoreView.swift b/Sources/PulseUI/Views/ShareStoreView.swift index 3691f3559..aa5d549db 100644 --- a/Sources/PulseUI/Views/ShareStoreView.swift +++ b/Sources/PulseUI/Views/ShareStoreView.swift @@ -9,8 +9,8 @@ import CoreData import Pulse import Combine -@available(iOS 15, macOS 13, watchOS 9, visionOS 1.0, *) -struct ShareStoreView: View { +@available(iOS 16, macOS 13, watchOS 9, visionOS 1, *) +package struct ShareStoreView: View { /// Preselected sessions. var sessions: Set = [] var onDismiss: () -> Void @@ -21,7 +21,12 @@ struct ShareStoreView: View { @Environment(\.store) private var store: LoggerStore - var body: some View { + package init(sessions: Set = [], onDismiss: @escaping () -> Void) { + self.sessions = sessions + self.onDismiss = onDismiss + } + + package var body: some View { content .onAppear { if !sessions.isEmpty { @@ -155,7 +160,7 @@ struct ShareStoreView: View { } #if DEBUG -@available(iOS 15, macOS 13, watchOS 9, visionOS 1.0, *) +@available(iOS 16, macOS 13, watchOS 9, visionOS 1, *) struct ShareStoreView_Previews: PreviewProvider { static var previews: some View { #if os(iOS) || os(visionOS) diff --git a/Sources/PulseUI/Views/ShareView.swift b/Sources/PulseUI/Views/ShareView.swift index da5cc3f7c..0e3c83747 100644 --- a/Sources/PulseUI/Views/ShareView.swift +++ b/Sources/PulseUI/Views/ShareView.swift @@ -7,29 +7,29 @@ import SwiftUI #if os(iOS) || os(visionOS) import UIKit -struct ShareView: UIViewControllerRepresentable { +package struct ShareView: UIViewControllerRepresentable { var activityItems: [Any] var applicationActivities: [UIActivity]? private let cleanup: () -> Void private var completion: (() -> Void)? - init(activityItems: [Any]) { + package init(activityItems: [Any]) { self.activityItems = activityItems self.cleanup = {} } - init(_ items: ShareItems) { + package init(_ items: ShareItems) { self.activityItems = items.items self.cleanup = items.cleanup } - func onCompletion(_ completion: @escaping () -> Void) -> Self { + package func onCompletion(_ completion: @escaping () -> Void) -> Self { var copy = self copy.completion = completion return copy } - func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + package func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) controller.completionWithItemsHandler = { _, _, _, _ in cleanup() @@ -38,7 +38,7 @@ struct ShareView: UIViewControllerRepresentable { return controller } - func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { + package func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { } } #endif @@ -47,24 +47,24 @@ struct ShareView: UIViewControllerRepresentable { import AppKit import Pulse -struct ShareView: View { - let items: ShareItems +package struct ShareView: View { + package let items: ShareItems private var cleanup: (() -> Void)? private var completion: (() -> Void)? - init(_ items: ShareItems) { + package init(_ items: ShareItems) { self.items = items self.cleanup = items.cleanup } - func onCompletion(_ completion: @escaping () -> Void) -> Self { + package func onCompletion(_ completion: @escaping () -> Void) -> Self { var copy = self copy.completion = completion return copy } - var body: some View { + package var body: some View { VStack(alignment: .leading, spacing: 0) { let services = NSSharingService.sharingServices(forItems: items.items) ForEach(services, id: \.title) { service in @@ -103,7 +103,7 @@ struct ShareView: View { #endif #if os(macOS) -struct ShareNetworkTaskView: View { +package struct ShareNetworkTaskView: View { @ObservedObject var task: NetworkTaskEntity @AppStorage("com-github-kean-selected-task-sharing-option") private var output: Output = .plainText @@ -111,7 +111,11 @@ struct ShareNetworkTaskView: View { @State private var items: ShareItems? @Environment(\.store) private var store - var body: some View { + package init(task: NetworkTaskEntity) { + self.task = task + } + + package var body: some View { VStack(spacing: 0) { VStack(spacing: 8) { NetworkRequestStatusCell(viewModel: .init(task: task, store: store)) diff --git a/Sources/PulseUI/Views/TimingView.swift b/Sources/PulseUI/Views/TimingView.swift index 2fb8f5fbd..49758835a 100644 --- a/Sources/PulseUI/Views/TimingView.swift +++ b/Sources/PulseUI/Views/TimingView.swift @@ -6,10 +6,14 @@ import SwiftUI -struct TimingView: View { - let viewModel: TimingViewModel +package struct TimingView: View { + package let viewModel: TimingViewModel - var body: some View { + package init(viewModel: TimingViewModel) { + self.viewModel = viewModel + } + + package var body: some View { #if os(tvOS) ForEach(viewModel.sections) { item in Section { @@ -69,7 +73,7 @@ private struct TimingRowView: View { let spacing: CGFloat = 12 #endif - @Environment(\.sizeCategory) var sizeCategory + @ScaledMetric(relativeTo: .body) private var sizeMultiplier = 1.0 var body: some View { HStack(alignment: .center, spacing: spacing) { @@ -93,12 +97,12 @@ private struct TimingRowView: View { GeometryReader { proxy in let start = max(0, min(1, viewModel.start)) let length = min(1 - start, viewModel.length) - RoundedRectangle(cornerRadius: 2 * sizeCategory.scale) + RoundedRectangle(cornerRadius: 2 * sizeMultiplier) .fill(Color(viewModel.color)) .frame(width: max(2, proxy.size.width * length)) .padding(.leading, proxy.size.width * start) } - .frame(height: barHeight * sizeCategory.scale) + .frame(height: barHeight * sizeMultiplier) } @@ -125,18 +129,18 @@ private struct TimingRowView: View { } } -final class TimingViewModel { - let sections: [TimingRowSectionViewModel] +package final class TimingViewModel { + package let sections: [TimingRowSectionViewModel] - init(sections: [TimingRowSectionViewModel]) { + package init(sections: [TimingRowSectionViewModel]) { self.sections = sections } - private(set) lazy var longestTitle: String = { + private(set) package lazy var longestTitle: String = { allRows.map(\.title).max { $0.count < $1.count } ?? "" }() - private(set) lazy var longestValue: String = { + private(set) package lazy var longestValue: String = { allRows.map(\.value).max { $0.count < $1.count } ?? "" }() @@ -145,32 +149,32 @@ final class TimingViewModel { } } -final class TimingRowSectionViewModel: Identifiable { - let title: String - let items: [TimingRowViewModel] - var isHeader = false +package final class TimingRowSectionViewModel: Identifiable { + package let title: String + package let items: [TimingRowViewModel] + package var isHeader = false - var id: ObjectIdentifier { ObjectIdentifier(self) } + package var id: ObjectIdentifier { ObjectIdentifier(self) } - init(title: String, items: [TimingRowViewModel], isHeader: Bool = false) { + package init(title: String, items: [TimingRowViewModel], isHeader: Bool = false) { self.title = title self.items = items self.isHeader = isHeader } } -final class TimingRowViewModel: Identifiable { - let title: String - let value: String - let color: UXColor +package final class TimingRowViewModel: Identifiable { + package let title: String + package let value: String + package let color: UXColor // [0, 1] - let start: CGFloat + package let start: CGFloat // [0, 1] - let length: CGFloat + package let length: CGFloat - var id: ObjectIdentifier { ObjectIdentifier(self) } + package var id: ObjectIdentifier { ObjectIdentifier(self) } - init(title: String, value: String, color: UXColor, start: CGFloat, length: CGFloat) { + package init(title: String, value: String, color: UXColor, start: CGFloat, length: CGFloat) { self.title = title self.value = value self.color = color diff --git a/Sources/PulseUI/Views/WebView.swift b/Sources/PulseUI/Views/WebView.swift index 560a3452b..a99c91bf0 100644 --- a/Sources/PulseUI/Views/WebView.swift +++ b/Sources/PulseUI/Views/WebView.swift @@ -9,17 +9,22 @@ import SwiftUI import WebKit import UIKit -struct WebView: UIViewRepresentable { - let data: Data - let contentType: String +package struct WebView: UIViewRepresentable { + package let data: Data + package let contentType: String - func makeUIView(context: Context) -> WKWebView { + package init(data: Data, contentType: String) { + self.data = data + self.contentType = contentType + } + + package func makeUIView(context: Context) -> WKWebView { let webView = WKWebView(frame: .zero, configuration: .init()) webView.load(data, mimeType: contentType, characterEncodingName: "UTF8", baseURL: FileManager.default.temporaryDirectory) return webView } - func updateUIView(_ webView: WKWebView, context: Context) { + package func updateUIView(_ webView: WKWebView, context: Context) { // Do nothing } } @@ -30,17 +35,17 @@ struct WebView: UIViewRepresentable { import WebKit import AppKit -struct WebView: NSViewRepresentable { - let data: Data - let contentType: String +package struct WebView: NSViewRepresentable { + package let data: Data + package let contentType: String - func makeNSView(context: Context) -> WKWebView { + package func makeNSView(context: Context) -> WKWebView { let webView = WKWebView(frame: .zero, configuration: .init()) webView.load(data, mimeType: contentType, characterEncodingName: "UTF8", baseURL: FileManager.default.temporaryDirectory) return webView } - func updateNSView(_ nsView: WKWebView, context: Context) { + package func updateNSView(_ nsView: WKWebView, context: Context) { // Do nothing } }