Files
Pulse/Sources/PulseUI/Features/Console/List/ConsoleListContentView.swift
2023-04-29 10:41:34 -04:00

159 lines
4.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The MIT License (MIT)
//
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
import CoreData
import Pulse
import Combine
import SwiftUI
struct ConsoleListContentView: View {
@ObservedObject var viewModel: ConsoleListViewModel
#if os(macOS)
let proxy: ScrollViewProxy
@SceneStorage("com-github-kean-pulse-is-now-enabled") private var isNowEnabled = true
#endif
var body: some View {
#if os(iOS)
if #available(iOS 15, *) {
if !viewModel.pins.isEmpty, !viewModel.isShowingFocusedEntities {
ConsoleListPinsSectionView(viewModel: viewModel)
if !viewModel.entities.isEmpty {
PlainListGroupSeparator()
}
}
}
#endif
#if os(iOS) || os(macOS)
if #available(iOS 15, *), let sections = viewModel.sections, !sections.isEmpty {
ForEach(sections, id: \.name) {
ConsoleListGroupedSectionView(section: $0, viewModel: viewModel)
}
} else {
plainView
#if os(macOS)
.apply(registerNowMode)
#endif
}
#else
plainView
#endif
}
@ViewBuilder
private var plainView: some View {
if viewModel.entities.isEmpty {
Text("Empty")
.font(.subheadline)
.foregroundColor(.secondary)
} else {
ForEach(viewModel.visibleEntities, id: \.objectID) { entity in
ConsoleEntityCell(entity: entity)
.id(entity.objectID)
.onAppear { viewModel.onAppearCell(with: entity.objectID) }
.onDisappear { viewModel.onDisappearCell(with: entity.objectID) }
}
}
#if os(macOS)
bottomAnchorView
#else
footerView
#endif
}
@ViewBuilder
private var footerView: some View {
if #available(iOS 15, *), let session = viewModel.previousSession, !viewModel.isShowingFocusedEntities {
Button(action: { viewModel.buttonShowPreviousSessionTapped(for: session) }) {
Text("Show Previous Session")
.font(.subheadline)
.foregroundColor(.blue)
Spacer()
Text(session.formattedDate(isCompact: false))
.font(.subheadline)
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
#if os(iOS)
.listRowSeparator(.hidden, edges: .bottom)
#endif
}
}
#if os(macOS)
private func registerNowMode<T: View>(for list: T) -> some View {
list.onChange(of: viewModel.entities) { entities in
guard isNowEnabled else { return }
withAnimation {
proxy.scrollTo(BottomViewID(), anchor: .top)
}
// This is a workaround that fixes a scrolling issue when more
// than one row is added at the time.
DispatchQueue.main.async {
proxy.scrollTo(BottomViewID(), anchor: .top)
}
}
.onChange(of: isNowEnabled) {
guard $0 else { return }
proxy.scrollTo(BottomViewID(), anchor: .top)
}
}
// This view is used to keep scroll to the bottom and keep track of the
// scroll position (near bottom or not).
private var bottomAnchorView: some View {
HStack { EmptyView() }
.frame(height: 1)
.id(BottomViewID())
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.onAppear {
nowModeChange?.cancel()
}
.onDisappear {
// The scrolling with ScrollViewProxy is unreliable, and this cell
// occasionally disappears.
delayNowModeChange {
guard viewModel.isViewVisible else { return }
isNowEnabled = false
}
}
}
#endif
}
#if os(macOS)
private var nowModeChange: DispatchWorkItem?
private func delayNowModeChange(_ closure: @escaping () -> Void) {
nowModeChange?.cancel()
let item = DispatchWorkItem(block: closure)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(64), execute: item)
nowModeChange = item
}
struct BottomViewID: Hashable, Identifiable {
var id: BottomViewID { self}
}
#endif
#if os(iOS) || os(macOS)
struct ConsoleStaticList: View {
let entities: [NSManagedObject]
var body: some View {
List {
ForEach(entities, id: \.objectID, content: ConsoleEntityCell.init)
}
.listStyle(.plain)
#if os(iOS)
.environment(\.defaultMinListRowHeight, 8)
#endif
}
}
#endif