Move SessionsView to a separate file

This commit is contained in:
Alex Grebenyuk
2023-04-15 20:39:14 -04:00
parent 93ebee14a4
commit 7fd39d259b
8 changed files with 484 additions and 304 deletions
+16 -4
View File
@@ -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
+1 -1
View File
@@ -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()