mirror of
https://github.com/kean/Pulse.git
synced 2026-04-30 21:02:27 +00:00
300 lines
8.8 KiB
Swift
300 lines
8.8 KiB
Swift
// The MIT License (MIT)
|
||
//
|
||
// Copyright (c) 2020–2022 Alexander Grebenyuk (github.com/kean).
|
||
|
||
import SwiftUI
|
||
import Pulse
|
||
|
||
struct KeyValueSectionView: View {
|
||
let viewModel: KeyValueSectionViewModel
|
||
var limit: Int = Int.max
|
||
private var hideTitle = false
|
||
|
||
init(viewModel: KeyValueSectionViewModel) {
|
||
self.viewModel = viewModel
|
||
}
|
||
|
||
init(viewModel: KeyValueSectionViewModel, limit: Int) {
|
||
self.viewModel = viewModel
|
||
self.limit = limit
|
||
}
|
||
|
||
func hiddenTitle() -> KeyValueSectionView {
|
||
var copy = self
|
||
copy.hideTitle = true
|
||
return copy
|
||
}
|
||
|
||
private var actualTintColor: Color {
|
||
viewModel.items.isEmpty ? .gray : viewModel.color
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
if !hideTitle {
|
||
headerView
|
||
}
|
||
#if os(watchOS)
|
||
KeyValueListView(viewModel: viewModel, limit: limit)
|
||
.padding(.top, 6)
|
||
.border(width: 2, edges: [.top], color: actualTintColor)
|
||
.padding(.top, 2)
|
||
|
||
if let action = viewModel.action, !viewModel.items.isEmpty {
|
||
Spacer().frame(height: 8)
|
||
Button(action: action.action, label: {
|
||
Text(action.title)
|
||
Image(systemName: "chevron.right")
|
||
.foregroundColor(.separator)
|
||
.font(.caption)
|
||
})
|
||
|
||
}
|
||
#else
|
||
KeyValueListView(viewModel: viewModel, limit: limit)
|
||
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 0))
|
||
.border(width: 2, edges: [.leading], color: actualTintColor)
|
||
.padding(EdgeInsets(top: 0, leading: 1, bottom: 0, trailing: 0))
|
||
#endif
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var headerView: some View {
|
||
HStack {
|
||
Text(viewModel.title)
|
||
.font(.headline)
|
||
Spacer()
|
||
#if os(iOS)
|
||
if let action = viewModel.action, !viewModel.items.isEmpty {
|
||
makeActionButton(with: action)
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func makeActionButton(with action: ActionViewModel) -> some View {
|
||
Button(action: action.action) {
|
||
HStack(spacing: 4) {
|
||
Text(action.title)
|
||
.font(.body)
|
||
.foregroundColor(.accentColor)
|
||
Image(systemName: "chevron.right")
|
||
.foregroundColor(Color.gray)
|
||
.font(.caption)
|
||
.padding(.top, 2)
|
||
}.padding(.trailing, -6)
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct KeyValueListView: View {
|
||
let viewModel: KeyValueSectionViewModel
|
||
var limit: Int = Int.max
|
||
|
||
private var actualTintColor: Color {
|
||
viewModel.items.isEmpty ? .gray : viewModel.color
|
||
}
|
||
|
||
var items: [(String, String?)] {
|
||
var items = Array(viewModel.items.prefix(limit))
|
||
if viewModel.items.count > limit {
|
||
items.append(("And \(viewModel.items.count - limit) more", "..."))
|
||
}
|
||
return items
|
||
}
|
||
|
||
#if os(macOS)
|
||
var body: some View {
|
||
if viewModel.items.isEmpty {
|
||
HStack {
|
||
Text("Empty")
|
||
.foregroundColor(actualTintColor)
|
||
.font(.system(size: fontSize, weight: .medium))
|
||
}
|
||
} else {
|
||
Label(text: text)
|
||
.padding(.bottom, 5)
|
||
}
|
||
}
|
||
|
||
private var text: NSAttributedString {
|
||
let text = NSMutableAttributedString()
|
||
for (index, row) in items.enumerated() {
|
||
text.append(makeRow(row))
|
||
if index != items.indices.last {
|
||
text.append("\n")
|
||
}
|
||
}
|
||
return text
|
||
}
|
||
|
||
private func makeRow(_ row: (String, String?)) -> NSAttributedString {
|
||
let text = NSMutableAttributedString()
|
||
text.append(row.0 + ": ", [
|
||
.font: NSFont.systemFont(ofSize: fontSize, weight: .medium),
|
||
.foregroundColor: NSColor(actualTintColor),
|
||
.paragraphStyle: ps
|
||
])
|
||
text.append(row.1 ?? "–", [
|
||
.font: NSFont.systemFont(ofSize: fontSize),
|
||
.foregroundColor: NSColor(Color.primary),
|
||
.paragraphStyle: ps
|
||
])
|
||
return text
|
||
}
|
||
#else
|
||
var body: some View {
|
||
if viewModel.items.isEmpty {
|
||
HStack {
|
||
Text("Empty")
|
||
.foregroundColor(actualTintColor)
|
||
.font(.system(size: fontSize, weight: .medium))
|
||
}
|
||
} else {
|
||
VStack(spacing: 2) {
|
||
let rows = items.enumerated().map(KeyValueRow.init)
|
||
ForEach(rows, content: makeRow)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func makeRow(_ row: KeyValueRow) -> some View {
|
||
HStack {
|
||
let title = Text(row.item.0 + ": ")
|
||
.foregroundColor(actualTintColor)
|
||
.font(.system(size: fontSize, weight: .medium))
|
||
let value = Text(row.item.1 ?? "–")
|
||
.foregroundColor(.primary)
|
||
.font(.system(size: fontSize, weight: .regular))
|
||
(title + value)
|
||
.lineLimit(row.item.0 == "URL" ? 3 : 3)
|
||
#if os(iOS)
|
||
.backport.contextMenu(menuItems: {
|
||
makeContextMenu(for: row.item)
|
||
}, preview: {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
Text(row.item.0)
|
||
.foregroundColor(actualTintColor)
|
||
.font(.system(size: fontSize, weight: .medium))
|
||
Text(row.item.1 ?? "–")
|
||
.foregroundColor(.primary)
|
||
.font(.system(size: fontSize, weight: .regular))
|
||
.lineLimit(24)
|
||
}
|
||
.padding()
|
||
.frame(width: 340)
|
||
})
|
||
#endif
|
||
Spacer()
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if os(iOS)
|
||
@ViewBuilder
|
||
func makeContextMenu(for item: (String, String?)) -> some View {
|
||
makeCopyButton(title: "Copy Pair", value: "\(item.0): \(item.1 ?? "–")")
|
||
makeCopyButton(title: "Copy Key", value: item.0)
|
||
makeCopyButton(title: "Copy Value", value: item.1)
|
||
}
|
||
|
||
func makeCopyButton(title: String, value: String?) -> some View {
|
||
Button(action: {
|
||
UXPasteboard.general.string = value
|
||
runHapticFeedback()
|
||
}) {
|
||
Text(title)
|
||
Image(systemName: "doc.on.doc")
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
#if os(macOS)
|
||
private struct Label: NSViewRepresentable {
|
||
let text: NSAttributedString
|
||
|
||
func makeNSView(context: Context) -> NSTextField {
|
||
let label = NSTextField.label()
|
||
label.isSelectable = true
|
||
label.allowsEditingTextAttributes = true
|
||
label.lineBreakMode = .byCharWrapping
|
||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||
return label
|
||
}
|
||
|
||
func updateNSView(_ nsView: NSTextField, context: Context) {
|
||
nsView.attributedStringValue = text
|
||
}
|
||
}
|
||
|
||
private let ps: NSParagraphStyle = {
|
||
let ps = NSMutableParagraphStyle()
|
||
ps.minimumLineHeight = 20
|
||
ps.maximumLineHeight = 20
|
||
return ps
|
||
}()
|
||
#endif
|
||
|
||
private var fontSize: CGFloat {
|
||
#if os(iOS)
|
||
return 15
|
||
#elseif os(watchOS)
|
||
return 14
|
||
#elseif os(tvOS)
|
||
return 28
|
||
#else
|
||
return 12
|
||
#endif
|
||
}
|
||
|
||
struct KeyValueSectionViewModel {
|
||
let title: String
|
||
let color: Color
|
||
var action: ActionViewModel?
|
||
var items: [(String, String?)] = []
|
||
}
|
||
|
||
struct KeyValueRow: Identifiable {
|
||
let id: Int
|
||
let item: (String, String?)
|
||
|
||
var title: String { item.0 }
|
||
var details: String? { item.1 }
|
||
}
|
||
|
||
struct ActionViewModel {
|
||
let action: () -> Void
|
||
let title: String
|
||
}
|
||
|
||
#if DEBUG
|
||
struct KeyValueSectionView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
VStack(spacing: 16) {
|
||
|
||
KeyValueSectionView(viewModel: .init(
|
||
title: "Summary",
|
||
color: .red,
|
||
items: [("URL", "https://github.com/kean/Pulse/blob/master/Sources/PulseUI/Features/Console/This/Is/A/Very/Long/URL/that-does-not-fit-in-the-review/But/with?ios=16.0,feature=ContextMenuPreview,backport=yes,easy-to-preview-the-whole-thing-now=yes")])
|
||
)
|
||
KeyValueSectionView(viewModel: .init(
|
||
title: "Headers",
|
||
color: .blue,
|
||
action: .init(action: {}, title: "Show Raw"),
|
||
items: [
|
||
("Content-Length", "21851748"),
|
||
("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundaryrv8XAHQPtQcWta3k"),
|
||
("User-Agent", "Pulse%20Demo%20iOS/20 CFNetwork/1385 Darwin/22.0.0")
|
||
])
|
||
)
|
||
}
|
||
.padding()
|
||
.frame(width: 400)
|
||
.previewLayout(.sizeThatFits)
|
||
}
|
||
}
|
||
#endif
|