mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
193 lines
5.7 KiB
Swift
193 lines
5.7 KiB
Swift
// 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])] = []
|
||
|
||
#if os(iOS)
|
||
@Environment(\.editMode) private var editMode
|
||
#endif
|
||
@Environment(\.store) private var store
|
||
|
||
var body: some View {
|
||
if sessions.isEmpty {
|
||
Text("No Recorded Session")
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
.foregroundColor(.secondary)
|
||
} else {
|
||
content
|
||
.onAppear { refreshGroups() }
|
||
.onChange(of: sessions.count) { _ in refreshGroups() }
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var content: some View {
|
||
#if os(macOS)
|
||
VStack {
|
||
list
|
||
HStack {
|
||
SearchBar(title: "Filter", imageName: "line.3.horizontal.decrease.circle", text: $filterTerm)
|
||
.frame(maxWidth: 200)
|
||
Spacer()
|
||
}.padding(8)
|
||
}
|
||
#else
|
||
list
|
||
#endif
|
||
}
|
||
|
||
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 {
|
||
ForEach(groupedSessions, id: \.0) { group in
|
||
Section(header: makeHeader(for: group.0, sessions: group.1)) {
|
||
ForEach(group.1, id: \.id, content: makeCell)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#if os(iOS)
|
||
.listStyle(.plain)
|
||
.backport.searchable(text: $filterTerm)
|
||
#else
|
||
.listStyle(.sidebar)
|
||
#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 os(iOS)
|
||
if editMode?.wrappedValue.isEditing ?? false {
|
||
Spacer()
|
||
|
||
let ids = Set(sessions.map(\.id))
|
||
let isAllSelected = selection.intersection(ids).count == ids.count
|
||
Button(isAllSelected ? "Deselect All" : "Select All") {
|
||
if isAllSelected {
|
||
selection.subtract(ids)
|
||
} else {
|
||
selection.formUnion(ids)
|
||
}
|
||
}.font(.subheadline)
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func makeCell(for session: LoggerSessionEntity) -> some View {
|
||
ConsoleSessionCell(session: session, isCompact: filterTerm.isEmpty)
|
||
.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.searchTags.contains(where: {
|
||
$0.firstRange(of: filterTerm, options: [.caseInsensitive]) != nil
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
struct ConsoleSessionCell: View {
|
||
let session: LoggerSessionEntity
|
||
var isCompact = true
|
||
|
||
@Environment(\.store) private var store
|
||
#if os(iOS)
|
||
@Environment(\.editMode) private var editMode
|
||
#endif
|
||
|
||
var body: some View {
|
||
HStack(alignment: .lastTextBaseline) {
|
||
Text(session.formattedDate(isCompact: isCompact))
|
||
.fontWeight(store.session.id == session.id ? .medium : .regular)
|
||
.lineLimit(1)
|
||
.layoutPriority(1)
|
||
details
|
||
}
|
||
.tag(session.id)
|
||
#if os(iOS)
|
||
.listRowBackground((editMode?.wrappedValue.isEditing ?? false) ? Color.clear : nil)
|
||
#endif
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var details: some View {
|
||
#if !os(watchOS)
|
||
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
|
||
}
|
||
#endif
|
||
}
|
||
}
|