EIS-BLE-S3/cue-ios/CueIOS/Views/SessionView.swift

260 lines
8.1 KiB
Swift

import SwiftUI
struct Session: Identifiable {
let id = UUID()
var name: String
var notes: String
var created: Date
var measurements: [SessionMeasurement]
}
struct SessionMeasurement: Identifiable {
let id = UUID()
var type: Tab
var timestamp: Date
var pointCount: Int
var summary: String
}
@Observable
final class SessionStore {
var sessions: [Session] = []
var selectedSession: Session?
func createSession(name: String, notes: String) {
let session = Session(
name: name,
notes: notes,
created: Date(),
measurements: []
)
sessions.insert(session, at: 0)
selectedSession = session
}
func deleteSession(_ session: Session) {
sessions.removeAll { $0.id == session.id }
if selectedSession?.id == session.id {
selectedSession = nil
}
}
}
struct SessionView: View {
@State var store = SessionStore()
@State private var showingNewSession = false
@State private var newName = ""
@State private var newNotes = ""
var body: some View {
GeometryReader { geo in
if geo.size.width > 700 {
wideLayout
} else {
compactLayout
}
}
.sheet(isPresented: $showingNewSession) {
newSessionSheet
}
}
// MARK: - Wide layout (iPad)
private var wideLayout: some View {
HStack(spacing: 0) {
sessionList
.frame(width: 300)
Divider()
if let session = store.selectedSession {
sessionDetail(session)
} else {
Text("Select or create a session")
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
// MARK: - Compact layout (iPhone)
private var compactLayout: some View {
NavigationStack {
sessionList
.navigationTitle("Sessions")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showingNewSession = true }) {
Image(systemName: "plus")
}
}
}
}
}
// MARK: - Session list
private var sessionList: some View {
VStack(spacing: 0) {
HStack {
Text("Sessions")
.font(.headline)
Spacer()
Button(action: { showingNewSession = true }) {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
}
}
.padding()
if store.sessions.isEmpty {
VStack(spacing: 8) {
Text("No sessions")
.foregroundStyle(.secondary)
Text("Create a session to organize measurements")
.font(.caption)
.foregroundStyle(Color(white: 0.4))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
List(selection: Binding(
get: { store.selectedSession?.id },
set: { id in store.selectedSession = store.sessions.first { $0.id == id } }
)) {
ForEach(store.sessions) { session in
sessionRow(session)
.tag(session.id)
}
.onDelete { indices in
for idx in indices {
store.deleteSession(store.sessions[idx])
}
}
}
.listStyle(.plain)
}
}
}
private func sessionRow(_ session: Session) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(session.name)
.font(.subheadline.weight(.medium))
HStack {
Text(session.created, style: .date)
Text(session.created, style: .time)
}
.font(.caption)
.foregroundStyle(.secondary)
if !session.notes.isEmpty {
Text(session.notes)
.font(.caption)
.foregroundStyle(.tertiary)
.lineLimit(1)
}
if !session.measurements.isEmpty {
Text("\(session.measurements.count) measurement\(session.measurements.count == 1 ? "" : "s")")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
// MARK: - Session detail
private func sessionDetail(_ session: Session) -> some View {
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 4) {
Text(session.name)
.font(.title2.bold())
HStack {
Text(session.created, style: .date)
Text(session.created, style: .time)
}
.font(.subheadline)
.foregroundStyle(.secondary)
if !session.notes.isEmpty {
Text(session.notes)
.font(.body)
.foregroundStyle(.secondary)
}
}
.padding()
Divider()
if session.measurements.isEmpty {
VStack(spacing: 8) {
Text("No measurements in this session")
.foregroundStyle(.secondary)
Text("Run a measurement to add data")
.font(.caption)
.foregroundStyle(Color(white: 0.4))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
List(session.measurements) { meas in
HStack {
Label(meas.type.rawValue, systemImage: measurementIcon(meas.type))
Spacer()
VStack(alignment: .trailing) {
Text("\(meas.pointCount) pts")
.font(.caption.monospacedDigit())
Text(meas.timestamp, style: .time)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
.listStyle(.plain)
}
}
}
// MARK: - New session sheet
private var newSessionSheet: some View {
NavigationStack {
Form {
Section("Session Info") {
TextField("Name", text: $newName)
TextField("Notes (optional)", text: $newNotes, axis: .vertical)
.lineLimit(3...6)
}
}
.navigationTitle("New Session")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
showingNewSession = false
newName = ""
newNotes = ""
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Create") {
store.createSession(name: newName, notes: newNotes)
showingNewSession = false
newName = ""
newNotes = ""
}
.disabled(newName.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
}
.presentationDetents([.medium])
}
private func measurementIcon(_ tab: Tab) -> String {
switch tab {
case .eis: "waveform.path.ecg"
case .lsv: "chart.xyaxis.line"
case .amp: "bolt.fill"
case .chlorine: "drop.fill"
case .ph: "scalemass"
case .sessions: "folder"
}
}
}