260 lines
8.1 KiB
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"
|
|
}
|
|
}
|
|
}
|