Files
Pulse/Sources/PulseUI/Features/Search/Views/ConsoleSearchResultsSectionView.swift
2024-08-04 15:05:38 -04:00

200 lines
6.0 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
#if os(iOS) || os(macOS) || os(visionOS)
import SwiftUI
import Pulse
import CoreData
import Combine
@available(iOS 15, visionOS 1.0, macOS 13, *)
struct ConsoleSearchResultView: View {
let viewModel: ConsoleSearchResultViewModel
var limit: Int = 4
var isSeparatorNeeded = false
@EnvironmentObject private var environment: ConsoleEnvironment
var body: some View {
ConsoleEntityCell(entity: viewModel.entity)
let occurrences = Array(viewModel.occurrences).filter { $0.scope.isDisplayedInResults }
ForEach(occurrences.prefix(limit)) { item in
NavigationLink(destination: ConsoleSearchResultView.makeDestination(for: item, entity: viewModel.entity).injecting(environment)) {
makeCell(for: item)
}
}
if occurrences.count > limit {
let total = occurrences.count > ConsoleSearchMatch.limit ? "\(ConsoleSearchMatch.limit)+" : "\(occurrences.count)"
#if os(macOS)
Text("Total Results: \(total)")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.leading, 16)
.padding(.bottom, 8)
#else
NavigationLink(destination: ConsoleSearchResultDetailsView(viewModel: viewModel).injecting(environment)) {
Text("Total Results: ")
.font(ConsoleConstants.fontBody) +
Text(total)
.font(ConsoleConstants.fontBody)
.foregroundColor(.secondary)
}
#endif
}
#if os(iOS) || os(visionOS)
if isSeparatorNeeded {
PlainListGroupSeparator()
}
#endif
}
@ViewBuilder
private func makeCell(for occurrence: ConsoleSearchOccurrence) -> some View {
let contents = VStack(alignment: .leading, spacing: 4) {
Text(occurrence.preview)
.lineLimit(3)
Text(occurrence.scope.title + " (\(occurrence.line):\(occurrence.range.lowerBound + 1))")
.font(ConsoleConstants.fontInfo)
.foregroundColor(.secondary)
}
#if os(macOS)
.listRowSeparator(.visible)
.padding(.leading, 16)
#endif
if #unavailable(iOS 16) {
contents.padding(.vertical, 4)
} else {
#if os(macOS)
contents.padding(.vertical, 4)
#else
contents
#endif
}
}
@ViewBuilder
static func makeDestination(for occurrence: ConsoleSearchOccurrence, entity: NSManagedObject) -> some View {
_makeDestination(for: occurrence, entity: entity)
.environment(\.textViewSearchContext, occurrence.searchContext)
}
@ViewBuilder
private static func _makeDestination(for occurrence: ConsoleSearchOccurrence, entity: NSManagedObject) -> some View {
switch LoggerEntity(entity) {
case .message(let message):
ConsoleMessageDetailsView(message: message)
case .task(let task):
_makeDestination(for: occurrence, task: task)
}
}
@ViewBuilder
private static func _makeDestination(for occurrence: ConsoleSearchOccurrence, task: NetworkTaskEntity) -> some View {
switch occurrence.scope {
case .originalRequestHeaders, .currentRequestHeaders, .responseHeaders:
EmptyView() // Reserved
case .requestBody:
NetworkInspectorRequestBodyView(viewModel: .init(task: task))
case .responseBody:
NetworkInspectorResponseBodyView(viewModel: .init(task: task))
case .url, .message, .metadata:
EmptyView()
}
}
}
@available(iOS 15, visionOS 1.0, macOS 13, *)
struct ConsoleSearchResultDetailsView: View {
let viewModel: ConsoleSearchResultViewModel
var body: some View {
List {
ConsoleSearchResultView(viewModel: viewModel, limit: Int.max)
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, 0)
.inlineNavigationTitle("Search Results")
}
}
#if os(iOS) || os(visionOS)
@available(iOS 15, visionOS 1.0, *)
struct PlainListGroupSeparator: View {
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))
.listRowSeparator(.hidden)
.frame(height: 12)
}
}
#endif
@available(iOS 15, visionOS 1.0, *)
struct PlainListSectionHeader<Content: View>: View {
var title: String?
@ViewBuilder let content: () -> Content
var body: some View {
contents
.padding(.top, 8)
.listRowBackground(Color.separator.opacity(0.2))
#if os(iOS) || os(visionOS)
.listRowSeparator(.hidden)
#endif
}
@ViewBuilder
private var contents: some View {
if let title = title {
HStack(alignment: .bottom, spacing: 0) {
Text(title)
.foregroundColor(.secondary)
.lineLimit(3)
.font(.subheadline.weight(.medium))
Spacer()
}
} else {
content()
}
}
}
@available(iOS 15, visionOS 1.0, *)
extension PlainListSectionHeader where Content == Text {
init(title: String) {
self.init(title: title, content: { Text(title) })
}
}
@available(iOS 15, visionOS 1.0, *)
struct PlainListSeeAllView: View {
let count: Int
var body: some View {
(Text("Show All").foregroundColor(.accentColor) +
Text(" (\(count))"))
.font(.subheadline)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@available(iOS 15, visionOS 1.0, *)
struct PlainListSectionHeaderSeparator: View {
let title: String
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
Text(title)
.foregroundColor(.secondary)
.font(.subheadline.weight(.medium))
Spacer()
}
}
}
#endif