mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
Move SessionsView to a separate file
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
0C7A0E00297C51CE00B4B69D /* ConsoleListOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7A0DFF297C51CE00B4B69D /* ConsoleListOptions.swift */; };
|
||||
0C7A0E02297CE71400B4B69D /* FormattersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7A0E01297CE71400B4B69D /* FormattersTests.swift */; };
|
||||
0C840D4A29E07BE900362214 /* SelectableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C840D4929E07BE900362214 /* SelectableButton.swift */; };
|
||||
0C840D4C29E0A09B00362214 /* ConsoleSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C840D4B29E0A09B00362214 /* ConsoleSessionsView.swift */; };
|
||||
0C840D4C29E0A09B00362214 /* SessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C840D4B29E0A09B00362214 /* SessionsView.swift */; };
|
||||
0C94239429B4D73E007F2784 /* ConsoleSearchOccurrenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C94239329B4D73E007F2784 /* ConsoleSearchOccurrenceTests.swift */; };
|
||||
0C9F04F92884F34A0035239F /* Pulse_Demo_macOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9F04F82884F34A0035239F /* Pulse_Demo_macOSApp.swift */; };
|
||||
0C9F04FD2884F34A0035239F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C9F04FC2884F34A0035239F /* Assets.xcassets */; };
|
||||
@@ -276,6 +276,7 @@
|
||||
0CF5E5FA2997DED7009FF7AE /* ConsoleTableViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF5E5F82997DE86009FF7AE /* ConsoleTableViewModelTests.swift */; };
|
||||
0CF5E60929981ADF009FF7AE /* ShareStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF5E60729981ADC009FF7AE /* ShareStoreViewModel.swift */; };
|
||||
0CFD1CDB29E2E8FD003872EF /* LoggerStore+Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFD1CDA29E2E8FD003872EF /* LoggerStore+Remote.swift */; };
|
||||
0CFF79DD29EB7A8E00BE767B /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFF79DB29EB7A8B00BE767B /* SessionListView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -532,7 +533,7 @@
|
||||
0C7A0DFF297C51CE00B4B69D /* ConsoleListOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleListOptions.swift; sourceTree = "<group>"; };
|
||||
0C7A0E01297CE71400B4B69D /* FormattersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattersTests.swift; sourceTree = "<group>"; };
|
||||
0C840D4929E07BE900362214 /* SelectableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableButton.swift; sourceTree = "<group>"; };
|
||||
0C840D4B29E0A09B00362214 /* ConsoleSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleSessionsView.swift; sourceTree = "<group>"; };
|
||||
0C840D4B29E0A09B00362214 /* SessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsView.swift; sourceTree = "<group>"; };
|
||||
0C94239329B4D73E007F2784 /* ConsoleSearchOccurrenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleSearchOccurrenceTests.swift; sourceTree = "<group>"; };
|
||||
0C9F04F62884F34A0035239F /* Pulse Demo macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Pulse Demo macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0C9F04F82884F34A0035239F /* Pulse_Demo_macOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pulse_Demo_macOSApp.swift; sourceTree = "<group>"; };
|
||||
@@ -728,6 +729,7 @@
|
||||
0CF5E5F82997DE86009FF7AE /* ConsoleTableViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleTableViewModelTests.swift; sourceTree = "<group>"; };
|
||||
0CF5E60729981ADC009FF7AE /* ShareStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareStoreViewModel.swift; sourceTree = "<group>"; };
|
||||
0CFD1CDA29E2E8FD003872EF /* LoggerStore+Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoggerStore+Remote.swift"; sourceTree = "<group>"; };
|
||||
0CFF79DB29EB7A8B00BE767B /* SessionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionListView.swift; sourceTree = "<group>"; };
|
||||
49E82A8526D107A00070244F /* AlamofireIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireIntegration.swift; sourceTree = "<group>"; };
|
||||
49E82A8826D1083D0070244F /* MoyaIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoyaIntegration.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -1283,6 +1285,7 @@
|
||||
0CF0D5DD296F189500EED9D4 /* Inspector */,
|
||||
0CF0D5B3296F189500EED9D4 /* Filters */,
|
||||
0CF0D5DE296F189500EED9D4 /* Metrics */,
|
||||
0CFF79DA29EB7A8000BE767B /* Sessions */,
|
||||
0CF0D5CE296F189500EED9D4 /* Settings */,
|
||||
0CF0D5D6296F189500EED9D4 /* MessageDetails */,
|
||||
);
|
||||
@@ -1429,7 +1432,6 @@
|
||||
0CE45E0B2977813400249C5D /* ConsoleToolbarView.swift */,
|
||||
0C7A0DFC297C382300B4B69D /* ConsoleShareButton.swift */,
|
||||
0CB345A5298DEEA6009428B2 /* ConsoleInspectorsView.swift */,
|
||||
0C840D4B29E0A09B00362214 /* ConsoleSessionsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -1473,6 +1475,15 @@
|
||||
path = PulseUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CFF79DA29EB7A8000BE767B /* Sessions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C840D4B29E0A09B00362214 /* SessionsView.swift */,
|
||||
0CFF79DB29EB7A8B00BE767B /* SessionListView.swift */,
|
||||
);
|
||||
path = Sessions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -1988,6 +1999,7 @@
|
||||
0CF0D66F296F189600EED9D4 /* NetworkInspectorView-watchos.swift in Sources */,
|
||||
0CF0D671296F189600EED9D4 /* NetworkInspectorResponseBodyView.swift in Sources */,
|
||||
0CF0D65B296F189600EED9D4 /* SettingsView-macos.swift in Sources */,
|
||||
0CFF79DD29EB7A8E00BE767B /* SessionListView.swift in Sources */,
|
||||
0CF0D626296F189600EED9D4 /* FileViewer.swift in Sources */,
|
||||
0C40C09A296F77F1009ECF16 /* ConsoleSearchToggleCell.swift in Sources */,
|
||||
0C63A380297A02D200F6A6A5 /* StringSearchOptions.swift in Sources */,
|
||||
@@ -2096,7 +2108,7 @@
|
||||
0C40C098296F777D009ECF16 /* ConsoleSearchResponseSourceCell.swift in Sources */,
|
||||
0C3002B32986CEF30055F6C2 /* LoggerStoreIndex.swift in Sources */,
|
||||
0CF0D689296F189600EED9D4 /* MainViewController.swift in Sources */,
|
||||
0C840D4C29E0A09B00362214 /* ConsoleSessionsView.swift in Sources */,
|
||||
0C840D4C29E0A09B00362214 /* SessionsView.swift in Sources */,
|
||||
0CA32DE729D5E028001712E8 /* NSAttributedString+Extensions.swift in Sources */,
|
||||
0CF0D60F296F189600EED9D4 /* UIKit+Extensions.swift in Sources */,
|
||||
0CF0D610296F189600EED9D4 /* SpinnerView.swift in Sources */,
|
||||
|
||||
@@ -63,7 +63,7 @@ struct ConsoleInspectorsView: View {
|
||||
}
|
||||
case .sessions:
|
||||
if #available(macOS 13, *) {
|
||||
ConsoleSessionsView()
|
||||
SessionsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ extension ConsoleRouterView {
|
||||
@ViewBuilder
|
||||
private var destinationSessions: some View {
|
||||
NavigationView {
|
||||
ConsoleSessionsView()
|
||||
SessionsView()
|
||||
.navigationTitle("Sessions")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
import Pulse
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
#warning("preselect session on macOS too")
|
||||
#warning("add sharing on macOS too")
|
||||
@available(macOS 13, *)
|
||||
struct ConsoleSessionsView: View {
|
||||
@State private var selection: Set<UUID> = []
|
||||
@State private var sharedSessions: SharedSessions?
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
@EnvironmentObject private var consoleViewModel: ConsoleViewModel
|
||||
@Environment(\.store) private var store
|
||||
|
||||
var body: some View {
|
||||
if let version = Version(store.version), version < Version(3, 6, 0) {
|
||||
PlaceholderView(imageName: "questionmark.app", title: "Unsupported", subtitle: "This feature requires a store created by Pulse version 3.6.0 or higher").padding()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
list
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchBar(title: "Filter", imageName: "line.3.horizontal.decrease.circle", text: $filterTerm)
|
||||
.frame(maxWidth: 220)
|
||||
}.padding(8)
|
||||
}
|
||||
#else
|
||||
list.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(editMode.isEditing ? "Done" : "Edit") {
|
||||
withAnimation {
|
||||
editMode = editMode.isEditing ? .inactive : .active
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
if editMode == .active {
|
||||
bottomBar
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $sharedSessions) { sessions in
|
||||
NavigationView {
|
||||
ShareStoreView(sessions: sessions.ids, onDismiss: { sharedSessions = nil })
|
||||
}.backport.presentationDetents([.medium])
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
ConsoleSessionListView(selection: $selection, sharedSessions: $sharedSessions)
|
||||
#if os(iOS)
|
||||
.environment(\.editMode, $editMode)
|
||||
.onChange(of: selection) {
|
||||
guard !editMode.isEditing, !$0.isEmpty else { return }
|
||||
showInConsole(sessions: $0)
|
||||
}
|
||||
#else
|
||||
.onChange(of: selection) {
|
||||
guard consoleViewModel.searchCriteriaViewModel.criteria.shared.sessions.selection != $0 else { return }
|
||||
consoleViewModel.searchCriteriaViewModel.select(sessions: $0)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var bottomBar: some View {
|
||||
HStack {
|
||||
Button.destructive(action: {
|
||||
store.removeSessions(withIDs: selection)
|
||||
}, label: { Image(systemName: "trash") })
|
||||
.disabled(selection.isEmpty || selection == [store.session.id])
|
||||
|
||||
Spacer()
|
||||
|
||||
// It should ideally be done using stringsdict, but Pulse
|
||||
// doesn't support localization.
|
||||
if selection.count % 10 == 1 {
|
||||
Text("\(selection.count) Session Selected")
|
||||
} else {
|
||||
Text("\(selection.count) Sessions Selected")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: { sharedSessions = SharedSessions(ids: selection) }, label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
})
|
||||
.disabled(selection.isEmpty)
|
||||
|
||||
Menu(content: {
|
||||
Button("Show in Console") {
|
||||
showInConsole(sessions: selection)
|
||||
}.disabled(selection.isEmpty)
|
||||
}, label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func showInConsole(sessions: Set<UUID>) {
|
||||
consoleViewModel.searchCriteriaViewModel.select(sessions: sessions)
|
||||
consoleViewModel.router.isShowingSessions = false
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
@ViewBuilder
|
||||
private func contextMenu(for selection: Set<UUID>) -> some View {
|
||||
if !store.isArchive {
|
||||
Button(role: .destructive, action: {
|
||||
store.removeSessions(withIDs: selection)
|
||||
}, label: { Text("Remove") })
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private struct ConsoleSessionListView: View {
|
||||
@Binding var selection: Set<UUID>
|
||||
@Binding var sharedSessions: SharedSessions?
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \LoggerSessionEntity.createdAt, ascending: false)])
|
||||
private var sessions: FetchedResults<LoggerSessionEntity>
|
||||
|
||||
@State private var filterTerm = ""
|
||||
@State private var groupedSessions: [(Date, [LoggerSessionEntity])] = []
|
||||
|
||||
@Environment(\.editMode) private var editMode
|
||||
@Environment(\.store) private var store
|
||||
|
||||
var body: some View {
|
||||
if sessions.isEmpty {
|
||||
Text("No Recorded Session")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
list
|
||||
.onAppear { refreshGroups() }
|
||||
.onChange(of: sessions.count) { _ in refreshGroups() }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshGroups() {
|
||||
let calendar = Calendar.current
|
||||
let groups = Dictionary(grouping: sessions) {
|
||||
let components = calendar.dateComponents([.day, .year, .month], from: $0.createdAt)
|
||||
return calendar.date(from: components) ?? $0.createdAt
|
||||
}
|
||||
self.groupedSessions = Array(groups.sorted(by: { $0.key > $1.key }))
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
List(selection: $selection) {
|
||||
if !filterTerm.isEmpty {
|
||||
ForEach(getFilteredSessions(), id: \.id, content: makeCell)
|
||||
} else {
|
||||
#if os(iOS)
|
||||
ForEach(groupedSessions, id: \.0) { group in
|
||||
Section(header: makeHeader(for: group.0, sessions: group.1)) {
|
||||
ForEach(group.1, id: \.id, content: makeCell)
|
||||
}
|
||||
}
|
||||
#else
|
||||
ForEach(sessions, id: \.id, content: makeCell)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.plain)
|
||||
.backport.searchable(text: $filterTerm)
|
||||
#else
|
||||
.listStyle(.inset)
|
||||
.backport.hideListContentBackground()
|
||||
.contextMenu(forSelectionType: UUID.self, menu: contextMenu)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func makeHeader(for startDate: Date, sessions: [LoggerSessionEntity]) -> some View {
|
||||
HStack {
|
||||
(Text(sectionTitleFormatter.string(from: startDate)) +
|
||||
Text(" (\(sessions.count))").foregroundColor(.secondary.opacity(0.5)))
|
||||
.font(.headline)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
if editMode?.wrappedValue.isEditing ?? false {
|
||||
Spacer()
|
||||
Button("Select All") {
|
||||
selection.formUnion(Set(sessions.map(\.id)))
|
||||
}.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCell(for session: LoggerSessionEntity) -> some View {
|
||||
ConsoleSessionCell(session: session)
|
||||
.backport.swipeActions {
|
||||
Button(action: {
|
||||
if session.id != store.session.id {
|
||||
store.removeSessions(withIDs: [session.id])
|
||||
}
|
||||
}, label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}).backport.tint(Color.red)
|
||||
|
||||
Button(action: { sharedSessions = SharedSessions(ids: [session.id]) }) {
|
||||
Label("Share", systemImage: "square.and.arrow.up.fill")
|
||||
}.backport.tint(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFilteredSessions() -> [LoggerSessionEntity] {
|
||||
sessions.filter { $0.formattedDate(isCompact: false).localizedCaseInsensitiveContains(filterTerm) }
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleSessionCell: View {
|
||||
let session: LoggerSessionEntity
|
||||
var isCompact = true
|
||||
|
||||
@Environment(\.store) private var store
|
||||
@Environment(\.editMode) private var editMode
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Text(session.formattedDate(isCompact: isCompact))
|
||||
.fontWeight(store.session.id == session.id ? .medium : .regular)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
Spacer()
|
||||
if let version = session.fullVersion {
|
||||
Text(version)
|
||||
.lineLimit(1)
|
||||
.frame(minWidth: 40)
|
||||
#if os(macOS)
|
||||
.foregroundColor(Color(UXColor.tertiaryLabelColor))
|
||||
#else
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.listRowBackground((editMode?.wrappedValue.isEditing ?? false) ? Color.clear : nil)
|
||||
.tag(session.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SharedSessions: Hashable, Identifiable {
|
||||
var id: SharedSessions { self }
|
||||
let ids: Set<UUID>
|
||||
}
|
||||
|
||||
private let sectionTitleFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .long
|
||||
formatter.timeStyle = .none
|
||||
formatter.doesRelativeDateFormatting = true
|
||||
return formatter
|
||||
}()
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 15.0, macOS 13, *)
|
||||
struct Previews_ConsoleSessionsView_Previews: PreviewProvider {
|
||||
static let viewModel = ConsoleViewModel(store: .mock)
|
||||
|
||||
static var previews: some View {
|
||||
#if os(iOS)
|
||||
NavigationView {
|
||||
ConsoleSessionsView()
|
||||
.background(ConsoleRouterView(viewModel: viewModel))
|
||||
.injectingEnvironment(viewModel)
|
||||
}
|
||||
#else
|
||||
ConsoleSessionsView()
|
||||
.injectingEnvironment(viewModel)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
import Pulse
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
struct SessionListView: View {
|
||||
@Binding var selection: Set<UUID>
|
||||
@Binding var sharedSessions: SelectedSessionsIDs?
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \LoggerSessionEntity.createdAt, ascending: false)])
|
||||
private var sessions: FetchedResults<LoggerSessionEntity>
|
||||
|
||||
@State private var filterTerm = ""
|
||||
@State private var groupedSessions: [(Date, [LoggerSessionEntity])] = []
|
||||
|
||||
@Environment(\.editMode) private var editMode
|
||||
@Environment(\.store) private var store
|
||||
|
||||
var body: some View {
|
||||
if sessions.isEmpty {
|
||||
Text("No Recorded Session")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
list
|
||||
.onAppear { refreshGroups() }
|
||||
.onChange(of: sessions.count) { _ in refreshGroups() }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshGroups() {
|
||||
let calendar = Calendar.current
|
||||
let groups = Dictionary(grouping: sessions) {
|
||||
let components = calendar.dateComponents([.day, .year, .month], from: $0.createdAt)
|
||||
return calendar.date(from: components) ?? $0.createdAt
|
||||
}
|
||||
self.groupedSessions = Array(groups.sorted(by: { $0.key > $1.key }))
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
List(selection: $selection) {
|
||||
if !filterTerm.isEmpty {
|
||||
ForEach(getFilteredSessions(), id: \.id, content: makeCell)
|
||||
} else {
|
||||
#if os(iOS)
|
||||
ForEach(groupedSessions, id: \.0) { group in
|
||||
Section(header: makeHeader(for: group.0, sessions: group.1)) {
|
||||
ForEach(group.1, id: \.id, content: makeCell)
|
||||
}
|
||||
}
|
||||
#else
|
||||
ForEach(sessions, id: \.id, content: makeCell)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.plain)
|
||||
.backport.searchable(text: $filterTerm)
|
||||
#else
|
||||
.listStyle(.inset)
|
||||
.backport.hideListContentBackground()
|
||||
.contextMenu(forSelectionType: UUID.self, menu: contextMenu)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func makeHeader(for startDate: Date, sessions: [LoggerSessionEntity]) -> some View {
|
||||
HStack {
|
||||
(Text(sectionTitleFormatter.string(from: startDate)) +
|
||||
Text(" (\(sessions.count))").foregroundColor(.secondary.opacity(0.5)))
|
||||
.font(.headline)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
if editMode?.wrappedValue.isEditing ?? false {
|
||||
Spacer()
|
||||
Button("Select All") {
|
||||
selection.formUnion(Set(sessions.map(\.id)))
|
||||
}.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCell(for session: LoggerSessionEntity) -> some View {
|
||||
ConsoleSessionCell(session: session)
|
||||
.backport.swipeActions {
|
||||
Button(action: {
|
||||
if session.id != store.session.id {
|
||||
store.removeSessions(withIDs: [session.id])
|
||||
}
|
||||
}, label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}).backport.tint(Color.red)
|
||||
|
||||
Button(action: { sharedSessions = .init(ids: [session.id]) }) {
|
||||
Label("Share", systemImage: "square.and.arrow.up.fill")
|
||||
}.backport.tint(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFilteredSessions() -> [LoggerSessionEntity] {
|
||||
sessions.filter { $0.formattedDate(isCompact: false).localizedCaseInsensitiveContains(filterTerm) }
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleSessionCell: View {
|
||||
let session: LoggerSessionEntity
|
||||
var isCompact = true
|
||||
|
||||
@Environment(\.store) private var store
|
||||
@Environment(\.editMode) private var editMode
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Text(session.formattedDate(isCompact: isCompact))
|
||||
.fontWeight(store.session.id == session.id ? .medium : .regular)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
Spacer()
|
||||
if let version = session.fullVersion {
|
||||
Text(version)
|
||||
.lineLimit(1)
|
||||
.frame(minWidth: 40)
|
||||
#if os(macOS)
|
||||
.foregroundColor(Color(UXColor.tertiaryLabelColor))
|
||||
#else
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.listRowBackground((editMode?.wrappedValue.isEditing ?? false) ? Color.clear : nil)
|
||||
.tag(session.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectedSessionsIDs: Hashable, Identifiable {
|
||||
var id: SelectedSessionsIDs { self }
|
||||
let ids: Set<UUID>
|
||||
}
|
||||
|
||||
private let sectionTitleFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .long
|
||||
formatter.timeStyle = .none
|
||||
formatter.doesRelativeDateFormatting = true
|
||||
return formatter
|
||||
}()
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
import Pulse
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
struct SessionListView: View {
|
||||
@Binding var selection: Set<UUID>
|
||||
@Binding var sharedSessions: SelectedSessionsIDs?
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \LoggerSessionEntity.createdAt, ascending: false)])
|
||||
private var sessions: FetchedResults<LoggerSessionEntity>
|
||||
|
||||
@State private var filterTerm = ""
|
||||
@State private var groupedSessions: [(Date, [LoggerSessionEntity])] = []
|
||||
|
||||
@Environment(\.editMode) private var editMode
|
||||
@Environment(\.store) private var store
|
||||
|
||||
var body: some View {
|
||||
if sessions.isEmpty {
|
||||
Text("No Recorded Session")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
list
|
||||
.onAppear { refreshGroups() }
|
||||
.onChange(of: sessions.count) { _ in refreshGroups() }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshGroups() {
|
||||
let calendar = Calendar.current
|
||||
let groups = Dictionary(grouping: sessions) {
|
||||
let components = calendar.dateComponents([.day, .year, .month], from: $0.createdAt)
|
||||
return calendar.date(from: components) ?? $0.createdAt
|
||||
}
|
||||
self.groupedSessions = Array(groups.sorted(by: { $0.key > $1.key }))
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
List(selection: $selection) {
|
||||
if !filterTerm.isEmpty {
|
||||
ForEach(getFilteredSessions(), id: \.id, content: makeCell)
|
||||
} else {
|
||||
#if os(iOS)
|
||||
ForEach(groupedSessions, id: \.0) { group in
|
||||
Section(header: makeHeader(for: group.0, sessions: group.1)) {
|
||||
ForEach(group.1, id: \.id, content: makeCell)
|
||||
}
|
||||
}
|
||||
#else
|
||||
ForEach(sessions, id: \.id, content: makeCell)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.listStyle(.plain)
|
||||
.backport.searchable(text: $filterTerm)
|
||||
#else
|
||||
.listStyle(.inset)
|
||||
.backport.hideListContentBackground()
|
||||
.contextMenu(forSelectionType: UUID.self, menu: contextMenu)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func makeHeader(for startDate: Date, sessions: [LoggerSessionEntity]) -> some View {
|
||||
HStack {
|
||||
(Text(sectionTitleFormatter.string(from: startDate)) +
|
||||
Text(" (\(sessions.count))").foregroundColor(.secondary.opacity(0.5)))
|
||||
.font(.headline)
|
||||
.padding(.vertical, 6)
|
||||
|
||||
if editMode?.wrappedValue.isEditing ?? false {
|
||||
Spacer()
|
||||
Button("Select All") {
|
||||
selection.formUnion(Set(sessions.map(\.id)))
|
||||
}.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCell(for session: LoggerSessionEntity) -> some View {
|
||||
ConsoleSessionCell(session: session)
|
||||
.backport.swipeActions {
|
||||
Button(action: {
|
||||
if session.id != store.session.id {
|
||||
store.removeSessions(withIDs: [session.id])
|
||||
}
|
||||
}, label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}).backport.tint(Color.red)
|
||||
|
||||
Button(action: { sharedSessions = .init(ids: [session.id]) }) {
|
||||
Label("Share", systemImage: "square.and.arrow.up.fill")
|
||||
}.backport.tint(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFilteredSessions() -> [LoggerSessionEntity] {
|
||||
sessions.filter { $0.formattedDate(isCompact: false).localizedCaseInsensitiveContains(filterTerm) }
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleSessionCell: View {
|
||||
let session: LoggerSessionEntity
|
||||
var isCompact = true
|
||||
|
||||
@Environment(\.store) private var store
|
||||
@Environment(\.editMode) private var editMode
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Text(session.formattedDate(isCompact: isCompact))
|
||||
.fontWeight(store.session.id == session.id ? .medium : .regular)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
Spacer()
|
||||
if let version = session.fullVersion {
|
||||
Text(version)
|
||||
.lineLimit(1)
|
||||
.frame(minWidth: 40)
|
||||
#if os(macOS)
|
||||
.foregroundColor(Color(UXColor.tertiaryLabelColor))
|
||||
#else
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.listRowBackground((editMode?.wrappedValue.isEditing ?? false) ? Color.clear : nil)
|
||||
.tag(session.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectedSessionsIDs: Hashable, Identifiable {
|
||||
var id: SelectedSessionsIDs { self }
|
||||
let ids: Set<UUID>
|
||||
}
|
||||
|
||||
private let sectionTitleFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .long
|
||||
formatter.timeStyle = .none
|
||||
formatter.doesRelativeDateFormatting = true
|
||||
return formatter
|
||||
}()
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,155 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2020–2023 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
import Pulse
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
|
||||
#warning("preselect session on macOS too")
|
||||
#warning("add sharing on macOS too")
|
||||
@available(macOS 13, *)
|
||||
struct SessionsView: View {
|
||||
@State private var selection: Set<UUID> = []
|
||||
@State private var sharedSessions: SelectedSessionsIDs?
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
@EnvironmentObject private var consoleViewModel: ConsoleViewModel
|
||||
@Environment(\.store) private var store
|
||||
|
||||
var body: some View {
|
||||
if let version = Version(store.version), version < Version(3, 6, 0) {
|
||||
PlaceholderView(imageName: "questionmark.app", title: "Unsupported", subtitle: "This feature requires a store created by Pulse version 3.6.0 or higher").padding()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
list
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchBar(title: "Filter", imageName: "line.3.horizontal.decrease.circle", text: $filterTerm)
|
||||
.frame(maxWidth: 220)
|
||||
}.padding(8)
|
||||
}
|
||||
#else
|
||||
list.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(editMode.isEditing ? "Done" : "Edit") {
|
||||
withAnimation {
|
||||
editMode = editMode.isEditing ? .inactive : .active
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
if editMode == .active {
|
||||
bottomBar
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $sharedSessions) { sessions in
|
||||
NavigationView {
|
||||
ShareStoreView(sessions: sessions.ids, onDismiss: { sharedSessions = nil })
|
||||
}.backport.presentationDetents([.medium])
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
SessionListView(selection: $selection, sharedSessions: $sharedSessions)
|
||||
#if os(iOS)
|
||||
.environment(\.editMode, $editMode)
|
||||
.onChange(of: selection) {
|
||||
guard !editMode.isEditing, !$0.isEmpty else { return }
|
||||
showInConsole(sessions: $0)
|
||||
}
|
||||
#else
|
||||
.onChange(of: selection) {
|
||||
guard consoleViewModel.searchCriteriaViewModel.criteria.shared.sessions.selection != $0 else { return }
|
||||
consoleViewModel.searchCriteriaViewModel.select(sessions: $0)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var bottomBar: some View {
|
||||
HStack {
|
||||
Button.destructive(action: {
|
||||
store.removeSessions(withIDs: selection)
|
||||
}, label: { Image(systemName: "trash") })
|
||||
.disabled(selection.isEmpty || selection == [store.session.id])
|
||||
|
||||
Spacer()
|
||||
|
||||
// It should ideally be done using stringsdict, but Pulse
|
||||
// doesn't support localization.
|
||||
if selection.count % 10 == 1 {
|
||||
Text("\(selection.count) Session Selected")
|
||||
} else {
|
||||
Text("\(selection.count) Sessions Selected")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: { sharedSessions = SelectedSessionsIDs(ids: selection) }, label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
})
|
||||
.disabled(selection.isEmpty)
|
||||
|
||||
Menu(content: {
|
||||
Button("Show in Console") {
|
||||
showInConsole(sessions: selection)
|
||||
}.disabled(selection.isEmpty)
|
||||
}, label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func showInConsole(sessions: Set<UUID>) {
|
||||
consoleViewModel.searchCriteriaViewModel.select(sessions: sessions)
|
||||
consoleViewModel.router.isShowingSessions = false
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
@ViewBuilder
|
||||
private func contextMenu(for selection: Set<UUID>) -> some View {
|
||||
if !store.isArchive {
|
||||
Button(role: .destructive, action: {
|
||||
store.removeSessions(withIDs: selection)
|
||||
}, label: { Text("Remove") })
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 15.0, macOS 13, *)
|
||||
struct Previews_SessionsView_Previews: PreviewProvider {
|
||||
static let viewModel = ConsoleViewModel(store: .mock)
|
||||
|
||||
static var previews: some View {
|
||||
#if os(iOS)
|
||||
NavigationView {
|
||||
SessionsView()
|
||||
.background(ConsoleRouterView(viewModel: viewModel))
|
||||
.injectingEnvironment(viewModel)
|
||||
}
|
||||
#else
|
||||
SessionsView()
|
||||
.injectingEnvironment(viewModel)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -54,7 +54,7 @@ struct ShareStoreView: View {
|
||||
#warning("select sessions fix AND pass current selection")
|
||||
private var sectionSharingOptions: some View {
|
||||
Section {
|
||||
NavigationLink(destination: ConsoleSessionsView(isPicker: true)) {
|
||||
NavigationLink(destination: SessionsView()) {
|
||||
HStack {
|
||||
Text("Sessions")
|
||||
Spacer()
|
||||
|
||||
Reference in New Issue
Block a user