The Library tab's routines pane gains a "Browse Starter Routines" entry opening a gallery of every bundled seed — kept or deleted — each showing Added / name-taken / restorable state, a read-only plan preview, and a one-tap Add that lifts the seed's delete veto. A "Re-add All Missing Starters" fallback replaces the all-or-nothing Settings button, which is removed. The delete confirmation now also warns how many scheduled days would be left behind on the Today board. Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
143 lines
5.1 KiB
Swift
143 lines
5.1 KiB
Swift
//
|
|
// StarterPreviewView.swift
|
|
// Workouts
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
/// Read-only preview of one bundled starter routine — its icon, activity type, and
|
|
/// full exercise plan, exactly as it would land in the library — with the action that
|
|
/// actually installs it. Pushed from `StarterGalleryView` for a seed that isn't
|
|
/// already a live routine (or, read-only, for one blocked by a name collision).
|
|
struct StarterPreviewView: View {
|
|
@Environment(SyncEngine.self) private var sync
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
|
|
private var routines: [Routine]
|
|
|
|
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
|
|
|
let seed: SeedLibrary.Seed
|
|
|
|
@State private var isAdding = false
|
|
|
|
private var state: StarterSeedState { seed.state(among: routines) }
|
|
|
|
private var activityType: WorkoutActivityType {
|
|
seed.doc.activityType.flatMap(WorkoutActivityType.init(rawValue:)) ?? .traditionalStrength
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
header
|
|
Divider()
|
|
exerciseList
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.safeAreaInset(edge: .bottom) { actionBar }
|
|
.navigationTitle(seed.doc.name)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
// MARK: - Header
|
|
|
|
private var header: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: seed.doc.systemImage)
|
|
.font(.largeTitle)
|
|
.foregroundStyle(Color.color(from: seed.doc.color))
|
|
.frame(width: 56, height: 56)
|
|
.background(Color.color(from: seed.doc.color).opacity(0.12), in: RoundedRectangle(cornerRadius: 14))
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(seed.doc.name)
|
|
.font(.title2.weight(.semibold))
|
|
Label(activityType.displayName, systemImage: activityType.systemImage)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Exercises
|
|
|
|
private var exerciseList: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
Text("Exercises")
|
|
.font(.headline)
|
|
ForEach(seed.doc.exercises.sorted(by: { $0.order < $1.order })) { exercise in
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(exercise.name)
|
|
Text(exercise.planSummary(weightUnit: weightUnit))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Action
|
|
|
|
/// The prominent bottom action, reflecting the same three states the gallery row
|
|
/// does: already added (a link to the live routine), blocked by a name collision
|
|
/// (disabled, with the reason), or free to restore (active).
|
|
@ViewBuilder
|
|
private var actionBar: some View {
|
|
switch state {
|
|
case .added(let routine):
|
|
NavigationLink {
|
|
RoutineDetailView(routine: routine)
|
|
} label: {
|
|
Label("Added — View Routine", systemImage: "checkmark.circle.fill")
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.green)
|
|
.padding()
|
|
.background(.thinMaterial)
|
|
case .nameTaken:
|
|
VStack(spacing: 6) {
|
|
Text("Add to My Routines")
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 10)
|
|
.background(Color.secondary.opacity(0.15), in: RoundedRectangle(cornerRadius: 10))
|
|
.foregroundStyle(.secondary)
|
|
Text("A routine named \"\(seed.doc.name)\" already exists.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.background(.thinMaterial)
|
|
case .restorable:
|
|
Button {
|
|
Task {
|
|
isAdding = true
|
|
let added = await sync.restoreSeed(id: seed.id)
|
|
isAdding = false
|
|
// The @Query reflects the new routine the instant the cache mirror
|
|
// lands, so popping back to the gallery (which now shows this seed
|
|
// as Added) is enough — no local flag needed to fake that state here.
|
|
if added { dismiss() }
|
|
}
|
|
} label: {
|
|
HStack {
|
|
if isAdding { ProgressView().tint(.white) }
|
|
Text(isAdding ? "Adding…" : "Add to My Routines")
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(isAdding || sync.iCloudStatus != .available)
|
|
.padding()
|
|
.background(.thinMaterial)
|
|
}
|
|
}
|
|
}
|