Files
Pulse/Sources/PulseUI/Helpers/ShareItems.swift
2024-09-02 14:33:26 -04:00

170 lines
5.8 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
import Pulse
import CoreData
public enum ShareStoreOutput: String, RawRepresentable, Codable, CaseIterable {
case store, text, html, har
var fileExtension: String {
switch self {
case .store: return "pulse"
case .text: return "txt"
case .html: return "html"
case .har: return "har"
}
}
var interfaceTitle: String {
switch self {
case .store: "Pulse"
case .text: "Plain Text"
case .html: "HTML"
case .har: "HAR"
}
}
}
public struct ShareItems: Identifiable {
public let id = UUID()
public let items: [Any]
public let size: Int64?
public let cleanup: () -> Void
init(_ items: [Any], size: Int64? = nil, cleanup: @escaping () -> Void = { }) {
self.items = items
self.size = size
self.cleanup = cleanup
}
}
enum ShareService {
private static var task: ShareStoreTask?
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 {
continuation.resume(returning: value)
} else {
continuation.resume(throwing: CancellationError())
}
}.start()
}
}
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 {
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 {
let string = sanitized(string, as: output)
switch output {
case .plainText:
let string = TextUtilities.plainText(from: string)
return ShareItems([string])
case .html:
let html = (try? TextUtilities.html(from: string)) ?? Data()
let directory = TemporaryDirectory()
let fileURL = directory.write(data: html, extension: "html")
return ShareItems([fileURL], size: Int64(html.count), cleanup: directory.remove)
case .pdf:
#if os(iOS) || os(visionOS)
let pdf = (try? TextUtilities.pdf(from: string)) ?? Data()
let directory = TemporaryDirectory()
let fileURL = directory.write(data: pdf, extension: "pdf")
return ShareItems([fileURL], size: Int64(pdf.count), cleanup: directory.remove)
#else
return ShareItems(["Sharing as PDF is not supported on this platform"])
#endif
case .har:
let har = TextUtilities.har(from: string)
let directory = TemporaryDirectory()
let fileURL = directory.write(data: har, extension: "har")
return ShareItems([fileURL], size: Int64(har.count), cleanup: directory.remove)
}
}
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 {
ranges.append(range)
}
}
let output = NSMutableAttributedString(attributedString: string)
for range in ranges.reversed() {
output.deleteCharacters(in: range)
}
if shareOutput == .plainText {
var ranges: [NSRange] = []
string.enumerateAttribute(.subheadline, in: NSRange(location: 0, length: string.length)) { value, range, _ in
if (value as? Bool) == true {
ranges.append(range)
}
}
for range in ranges.reversed() {
output.insert(NSAttributedString(string: "–––––––––––––––––––––––––––––––––––––––\n"), at: range.upperBound)
output.insert(NSAttributedString(string: ""), at: range.location)
}
}
return output
}
}
public enum ShareOutput {
case plainText
case html
case pdf
case har
var title: String {
switch self {
case .plainText: return "Text"
case .html: return "HTML"
case .pdf: return "PDF"
case .har: return "HAR"
}
}
}
struct TemporaryDirectory {
let url: URL
init() {
url = FileManager.default.temporaryDirectory
.appendingPathComponent("com.github.kean.logger", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
func remove() {
try? FileManager.default.removeItem(at: url)
}
}
extension TemporaryDirectory {
func write(data: Data, extension fileExtension: String) -> URL {
let date = makeCurrentDate()
let fileURL = url.appendingPathComponent("logs-\(date).\(fileExtension)", isDirectory: false)
try? data.write(to: fileURL)
return fileURL
}
}
func makeCurrentDate() -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.dateFormat = "yyyy-MM-dd-HH-mm"
return formatter.string(from: Date())
}