Add library exercises mid-workout with plan defaults
Pick any exercise from the full library while a workout is running, not just the ones in its split. The new exercise's plan is seeded from the most recent log of that exercise, else the library's authored Defaults line, else a plain 3x10. Adds a searchable two-section picker sheet and a **Defaults:** bullet to all 47 library reference pages. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -7,6 +7,7 @@ supported.
|
||||
- **Type:** Machine-based trunk flexion
|
||||
- **Targets:** Rectus abdominis, deep core
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ hips and glutes.
|
||||
- **Type:** Machine-based hip abduction
|
||||
- **Targets:** Gluteus medius, gluteus minimus, outer thighs
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ inner thighs.
|
||||
- **Type:** Machine-based hip adduction
|
||||
- **Targets:** Adductors, inner thighs
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the pad so only the forearms move.
|
||||
- **Type:** Machine-based elbow flexion (isolation)
|
||||
- **Targets:** Biceps brachii, brachialis
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the trunk back in line with the legs.
|
||||
- **Type:** Machine-based back extension (hip hinge)
|
||||
- **Targets:** Spinal erectors, glutes, hamstrings
|
||||
- **Prescription:** 3 × 10–15
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ heels, stand back up.
|
||||
- **Type:** Free-weight compound squat
|
||||
- **Targets:** Quadriceps, glutes, adductors, trunk
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ and press it back to straight arms.
|
||||
- **Type:** Free-weight horizontal press
|
||||
- **Targets:** Chest, front delts, triceps
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ twist.
|
||||
- **Type:** Anti-rotation
|
||||
- **Targets:** Spinal erectors, glutes, deep core, shoulder stability
|
||||
- **Prescription:** Warm-up 8 per side; main circuit 3 × 8–10 per side, slow
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ sit down between the heels. The pattern every loaded squat is built on.
|
||||
- **Type:** Bodyweight squat
|
||||
- **Targets:** Quadriceps, glutes, mobility
|
||||
- **Prescription:** 3 × 15–20, or 10 slow reps as a warm-up
|
||||
- **Defaults:** 3 × 15 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ a hard squeeze at the top.
|
||||
- **Type:** Machine-based calf raise
|
||||
- **Targets:** Soleus, gastrocnemius
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Gentle spinal mobility drill that wakes up the whole trunk before core work.
|
||||
- **Type:** Spinal mobility
|
||||
- **Targets:** Thoracic and lumbar spine, deep core awareness
|
||||
- **Prescription:** 10 slow reps (one rep = cat + cow)
|
||||
- **Defaults:** 3 × 10 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ forward from the chest.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based horizontal press
|
||||
- **Targets:** Pectorals, anterior deltoids, triceps
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ lower. Short range, all abs.
|
||||
- **Type:** Spinal flexion (upper)
|
||||
- **Targets:** Rectus abdominis
|
||||
- **Prescription:** 3 × 15–20, slow
|
||||
- **Defaults:** 3 × 15 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Anti-extension drill — the limbs move while the trunk stays perfectly still.
|
||||
- **Type:** Anti-extension
|
||||
- **Targets:** Deep core (transverse abdominis), hip flexor control
|
||||
- **Prescription:** 8 per side, slow and controlled
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the hips through.
|
||||
- **Type:** Free-weight compound hinge
|
||||
- **Targets:** Glutes, hamstrings, spinal erectors, grip
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ elbow depth, then press back to straight arms.
|
||||
- **Type:** Bodyweight elbow extension (chest/triceps press)
|
||||
- **Targets:** Triceps, chest, front delts
|
||||
- **Prescription:** 3 × 6–12
|
||||
- **Defaults:** 3 × 6 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the bar, one weight in each hand.
|
||||
- **Type:** Free-weight horizontal press (dumbbells)
|
||||
- **Targets:** Chest, front delts, triceps
|
||||
- **Prescription:** 3 × 8–12
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ dumbbells from a full hang to the shoulders.
|
||||
- **Type:** Free-weight elbow flexion (dumbbells)
|
||||
- **Targets:** Biceps, forearms
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ hugging arc, elbows softly fixed.
|
||||
- **Type:** Free-weight chest fly (dumbbells)
|
||||
- **Targets:** Chest, front delts
|
||||
- **Prescription:** 3 × 10–15, light and controlled
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from a dead hang to the ribs.
|
||||
- **Type:** Free-weight horizontal pull (dumbbell)
|
||||
- **Targets:** Lats, mid-back, biceps
|
||||
- **Prescription:** 3 × 8–12 per side
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ press from the shoulders to lockout overhead.
|
||||
- **Type:** Free-weight vertical press (dumbbells)
|
||||
- **Targets:** Shoulders, triceps, upper chest
|
||||
- **Prescription:** 3 × 8–12
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the nose. Small weight, big shoulders-health payoff.
|
||||
- **Type:** Machine-based cable pull (rear delts)
|
||||
- **Targets:** Rear delts, mid-back, rotator cuff
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ body is a straight line from shoulders to knees.
|
||||
- **Type:** Hip extension (floor)
|
||||
- **Targets:** Glutes, hamstrings
|
||||
- **Prescription:** 3 × 12–15, squeeze at the top
|
||||
- **Defaults:** 3 × 12 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ between the heels. The friendliest way to load a squat.
|
||||
- **Type:** Free-weight squat (dumbbell)
|
||||
- **Targets:** Quadriceps, glutes, trunk
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ drive the hips up to a straight line.
|
||||
- **Type:** Free-weight hip extension
|
||||
- **Targets:** Glutes, hamstrings
|
||||
- **Prescription:** 4 × 8–10
|
||||
- **Defaults:** 4 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ The gold standard for deep core tension — a shallow, rigid banana shape.
|
||||
- **Type:** Anti-extension (isometric hold)
|
||||
- **Targets:** Rectus abdominis, deep core, hip flexors
|
||||
- **Prescription:** 3 × 20–40 s holds
|
||||
- **Defaults:** 3 × 20 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ chest.
|
||||
- **Type:** Free-weight incline press
|
||||
- **Targets:** Upper chest, front delts, triceps
|
||||
- **Prescription:** 3 × 8–10
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ down from overhead.
|
||||
- **Type:** Machine-based vertical pull
|
||||
- **Targets:** Latissimus dorsi, mid-back, rear delts, biceps
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the arms are level with the shoulders.
|
||||
- **Type:** Free-weight lateral raise (dumbbells)
|
||||
- **Targets:** Side delts
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the thighs in place.
|
||||
- **Type:** Machine-based isolation (knee flexion)
|
||||
- **Targets:** Hamstrings (biceps femoris, semitendinosus, semimembranosus)
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ stay pinned to the seat.
|
||||
- **Type:** Machine-based isolation (knee extension)
|
||||
- **Targets:** Quadriceps (rectus femoris, vastus lateralis/medialis)
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ supported.
|
||||
- **Type:** Machine-based compound push
|
||||
- **Targets:** Quadriceps, glutes, hamstrings
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Lower-ab builder — the slow lowering is the exercise.
|
||||
- **Type:** Anti-extension (dynamic)
|
||||
- **Targets:** Lower rectus abdominis, hip flexors, deep core
|
||||
- **Prescription:** 3 × 8–15 reps, slow negatives
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ back up through the front heel. Bodyweight first, dumbbells in hand later.
|
||||
- **Type:** Split-stance squat (bodyweight or dumbbells)
|
||||
- **Targets:** Quadriceps, glutes, balance
|
||||
- **Prescription:** 3 × 8–10 per side
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the machine's fixed arc.
|
||||
- **Type:** Machine-based chest fly
|
||||
- **Targets:** Chest, front delts
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ The baseline anti-extension hold — a rigid line from head to heels.
|
||||
- **Type:** Anti-extension (isometric hold)
|
||||
- **Targets:** Rectus abdominis, deep core, glutes, shoulders
|
||||
- **Prescription:** 3 × 30–60 s holds
|
||||
- **Defaults:** 3 × 30 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ lower all the way back down.
|
||||
- **Type:** Bodyweight vertical pull
|
||||
- **Targets:** Lats, biceps, mid-back, grip
|
||||
- **Prescription:** 3 × as many strict reps as possible
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ press back up without the hips sagging.
|
||||
- **Type:** Bodyweight horizontal press
|
||||
- **Targets:** Chest, front delts, triceps, trunk
|
||||
- **Prescription:** 3 × 8–15
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ wide and back, hitting what pressing never touches.
|
||||
- **Type:** Machine-based reverse fly
|
||||
- **Targets:** Rear delts, mid-back
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Curl the pelvis toward the ribs — lower-ab flexion without neck strain.
|
||||
- **Type:** Spinal flexion (lower)
|
||||
- **Targets:** Lower rectus abdominis, deep core
|
||||
- **Prescription:** 3 × 12–15 reps, controlled negative
|
||||
- **Defaults:** 3 × 12 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the hamstrings stop you, then stand back up. Barbell or dumbbells.
|
||||
- **Type:** Free-weight hinge (barbell or dumbbells)
|
||||
- **Targets:** Hamstrings, glutes, spinal erectors
|
||||
- **Prescription:** 3 × 8–10
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ obliques.
|
||||
- **Type:** Machine-based trunk rotation
|
||||
- **Targets:** Obliques, deep core
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ straight back against the chest pad.
|
||||
- **Type:** Machine-based horizontal pull
|
||||
- **Targets:** Mid-back, latissimus dorsi, rear delts, biceps
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ against the pad.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based vertical press
|
||||
- **Targets:** Deltoids, triceps, upper trapezius
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Anti-lateral-flexion hold — resist the hips dropping toward the floor.
|
||||
- **Type:** Anti-lateral flexion (isometric hold)
|
||||
- **Targets:** Obliques, quadratus lumborum, glute medius, shoulder stability
|
||||
- **Prescription:** 3 × 20–45 s per side
|
||||
- **Defaults:** 3 × 20 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ chest to full extension.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based elbow extension
|
||||
- **Targets:** Triceps brachii
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ chest height down to straight arms.
|
||||
- **Type:** Machine-based cable elbow extension
|
||||
- **Targets:** Triceps
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ struct ExerciseInfo {
|
||||
/// The comma-separated `**Targets:**` values, split for chip rendering.
|
||||
var targets: [String]
|
||||
var sections: [Section]
|
||||
/// Parsed `**Defaults:**` bullet — the library's suggested starting plan for an
|
||||
/// exercise added outside a split (no prior log, no split entry to copy from).
|
||||
/// `nil` when the bullet is absent (not yet authored) or unparseable.
|
||||
var defaults: Defaults?
|
||||
|
||||
/// Whether the authored `Type:` identifies this as a machine exercise — the
|
||||
/// library convention is that every machine entry's type starts "Machine-based".
|
||||
@@ -34,6 +38,14 @@ struct ExerciseInfo {
|
||||
var items: [Item]
|
||||
}
|
||||
|
||||
/// The library's suggested starting plan, parsed from the `**Defaults:**` bullet.
|
||||
struct Defaults {
|
||||
var sets: Int
|
||||
var reps: Int
|
||||
var loadType: LoadType
|
||||
var durationSeconds: Int
|
||||
}
|
||||
|
||||
/// One block within a section, preserving the authored list flavor: numbered
|
||||
/// steps render with their ordinal, bullets with a dot, paragraphs plain.
|
||||
enum Item {
|
||||
@@ -58,6 +70,7 @@ extension ExerciseInfo {
|
||||
var category: String?
|
||||
var type: String?
|
||||
var targets: [String] = []
|
||||
var defaults: Defaults?
|
||||
var sections: [Section] = []
|
||||
|
||||
var currentSection: Section?
|
||||
@@ -118,6 +131,10 @@ extension ExerciseInfo {
|
||||
targets = value.components(separatedBy: ",")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
} else if currentSection == nil,
|
||||
let value = metadataValue(line, key: "Defaults") {
|
||||
flushItem()
|
||||
defaults = parseDefaults(value)
|
||||
} else if let range = line.range(of: #"^\d+\.\s+"#, options: .regularExpression) {
|
||||
flushItem()
|
||||
currentItem = .step(String(line[range.upperBound...]))
|
||||
@@ -136,9 +153,31 @@ extension ExerciseInfo {
|
||||
category: category,
|
||||
type: type,
|
||||
targets: targets,
|
||||
sections: sections
|
||||
sections: sections,
|
||||
defaults: defaults
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse a `**Defaults:**` value — "4 × 10 weighted", "3 × 12 bodyweight", or
|
||||
/// "3 × 30 s" — into sets/reps/loadType. The trailing unit picks the `LoadType`
|
||||
/// and which of reps/duration the middle number fills; anything else (missing
|
||||
/// bullet, unrecognized unit) yields `nil` rather than guessing.
|
||||
private static func parseDefaults(_ value: String) -> Defaults? {
|
||||
let parts = value.components(separatedBy: " × ")
|
||||
guard parts.count == 2, let sets = Int(parts[0]) else { return nil }
|
||||
let rest = parts[1].components(separatedBy: " ")
|
||||
guard rest.count == 2, let n = Int(rest[0]) else { return nil }
|
||||
switch rest[1] {
|
||||
case "weighted":
|
||||
return Defaults(sets: sets, reps: n, loadType: .weight, durationSeconds: 0)
|
||||
case "bodyweight":
|
||||
return Defaults(sets: sets, reps: n, loadType: .none, durationSeconds: 0)
|
||||
case "s":
|
||||
return Defaults(sets: sets, reps: 0, loadType: .duration, durationSeconds: n)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and parses the bundled `<Exercise Name>.info.md` exported next to the
|
||||
|
||||
@@ -7,6 +7,7 @@ supported.
|
||||
- **Type:** Machine-based trunk flexion
|
||||
- **Targets:** Rectus abdominis, deep core
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ hips and glutes.
|
||||
- **Type:** Machine-based hip abduction
|
||||
- **Targets:** Gluteus medius, gluteus minimus, outer thighs
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ inner thighs.
|
||||
- **Type:** Machine-based hip adduction
|
||||
- **Targets:** Adductors, inner thighs
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the pad so only the forearms move.
|
||||
- **Type:** Machine-based elbow flexion (isolation)
|
||||
- **Targets:** Biceps brachii, brachialis
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the trunk back in line with the legs.
|
||||
- **Type:** Machine-based back extension (hip hinge)
|
||||
- **Targets:** Spinal erectors, glutes, hamstrings
|
||||
- **Prescription:** 3 × 10–15
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ heels, stand back up.
|
||||
- **Type:** Free-weight compound squat
|
||||
- **Targets:** Quadriceps, glutes, adductors, trunk
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ and press it back to straight arms.
|
||||
- **Type:** Free-weight horizontal press
|
||||
- **Targets:** Chest, front delts, triceps
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ twist.
|
||||
- **Type:** Anti-rotation
|
||||
- **Targets:** Spinal erectors, glutes, deep core, shoulder stability
|
||||
- **Prescription:** Warm-up 8 per side; main circuit 3 × 8–10 per side, slow
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ sit down between the heels. The pattern every loaded squat is built on.
|
||||
- **Type:** Bodyweight squat
|
||||
- **Targets:** Quadriceps, glutes, mobility
|
||||
- **Prescription:** 3 × 15–20, or 10 slow reps as a warm-up
|
||||
- **Defaults:** 3 × 15 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ a hard squeeze at the top.
|
||||
- **Type:** Machine-based calf raise
|
||||
- **Targets:** Soleus, gastrocnemius
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Gentle spinal mobility drill that wakes up the whole trunk before core work.
|
||||
- **Type:** Spinal mobility
|
||||
- **Targets:** Thoracic and lumbar spine, deep core awareness
|
||||
- **Prescription:** 10 slow reps (one rep = cat + cow)
|
||||
- **Defaults:** 3 × 10 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ forward from the chest.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based horizontal press
|
||||
- **Targets:** Pectorals, anterior deltoids, triceps
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ lower. Short range, all abs.
|
||||
- **Type:** Spinal flexion (upper)
|
||||
- **Targets:** Rectus abdominis
|
||||
- **Prescription:** 3 × 15–20, slow
|
||||
- **Defaults:** 3 × 15 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Anti-extension drill — the limbs move while the trunk stays perfectly still.
|
||||
- **Type:** Anti-extension
|
||||
- **Targets:** Deep core (transverse abdominis), hip flexor control
|
||||
- **Prescription:** 8 per side, slow and controlled
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the hips through.
|
||||
- **Type:** Free-weight compound hinge
|
||||
- **Targets:** Glutes, hamstrings, spinal erectors, grip
|
||||
- **Prescription:** 4 × 6–8
|
||||
- **Defaults:** 4 × 6 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ elbow depth, then press back to straight arms.
|
||||
- **Type:** Bodyweight elbow extension (chest/triceps press)
|
||||
- **Targets:** Triceps, chest, front delts
|
||||
- **Prescription:** 3 × 6–12
|
||||
- **Defaults:** 3 × 6 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the bar, one weight in each hand.
|
||||
- **Type:** Free-weight horizontal press (dumbbells)
|
||||
- **Targets:** Chest, front delts, triceps
|
||||
- **Prescription:** 3 × 8–12
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ dumbbells from a full hang to the shoulders.
|
||||
- **Type:** Free-weight elbow flexion (dumbbells)
|
||||
- **Targets:** Biceps, forearms
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ hugging arc, elbows softly fixed.
|
||||
- **Type:** Free-weight chest fly (dumbbells)
|
||||
- **Targets:** Chest, front delts
|
||||
- **Prescription:** 3 × 10–15, light and controlled
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from a dead hang to the ribs.
|
||||
- **Type:** Free-weight horizontal pull (dumbbell)
|
||||
- **Targets:** Lats, mid-back, biceps
|
||||
- **Prescription:** 3 × 8–12 per side
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ press from the shoulders to lockout overhead.
|
||||
- **Type:** Free-weight vertical press (dumbbells)
|
||||
- **Targets:** Shoulders, triceps, upper chest
|
||||
- **Prescription:** 3 × 8–12
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the nose. Small weight, big shoulders-health payoff.
|
||||
- **Type:** Machine-based cable pull (rear delts)
|
||||
- **Targets:** Rear delts, mid-back, rotator cuff
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ body is a straight line from shoulders to knees.
|
||||
- **Type:** Hip extension (floor)
|
||||
- **Targets:** Glutes, hamstrings
|
||||
- **Prescription:** 3 × 12–15, squeeze at the top
|
||||
- **Defaults:** 3 × 12 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ between the heels. The friendliest way to load a squat.
|
||||
- **Type:** Free-weight squat (dumbbell)
|
||||
- **Targets:** Quadriceps, glutes, trunk
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ drive the hips up to a straight line.
|
||||
- **Type:** Free-weight hip extension
|
||||
- **Targets:** Glutes, hamstrings
|
||||
- **Prescription:** 4 × 8–10
|
||||
- **Defaults:** 4 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ The gold standard for deep core tension — a shallow, rigid banana shape.
|
||||
- **Type:** Anti-extension (isometric hold)
|
||||
- **Targets:** Rectus abdominis, deep core, hip flexors
|
||||
- **Prescription:** 3 × 20–40 s holds
|
||||
- **Defaults:** 3 × 20 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ chest.
|
||||
- **Type:** Free-weight incline press
|
||||
- **Targets:** Upper chest, front delts, triceps
|
||||
- **Prescription:** 3 × 8–10
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ down from overhead.
|
||||
- **Type:** Machine-based vertical pull
|
||||
- **Targets:** Latissimus dorsi, mid-back, rear delts, biceps
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the arms are level with the shoulders.
|
||||
- **Type:** Free-weight lateral raise (dumbbells)
|
||||
- **Targets:** Side delts
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the thighs in place.
|
||||
- **Type:** Machine-based isolation (knee flexion)
|
||||
- **Targets:** Hamstrings (biceps femoris, semitendinosus, semimembranosus)
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ stay pinned to the seat.
|
||||
- **Type:** Machine-based isolation (knee extension)
|
||||
- **Targets:** Quadriceps (rectus femoris, vastus lateralis/medialis)
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ supported.
|
||||
- **Type:** Machine-based compound push
|
||||
- **Targets:** Quadriceps, glutes, hamstrings
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Lower-ab builder — the slow lowering is the exercise.
|
||||
- **Type:** Anti-extension (dynamic)
|
||||
- **Targets:** Lower rectus abdominis, hip flexors, deep core
|
||||
- **Prescription:** 3 × 8–15 reps, slow negatives
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ back up through the front heel. Bodyweight first, dumbbells in hand later.
|
||||
- **Type:** Split-stance squat (bodyweight or dumbbells)
|
||||
- **Targets:** Quadriceps, glutes, balance
|
||||
- **Prescription:** 3 × 8–10 per side
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the machine's fixed arc.
|
||||
- **Type:** Machine-based chest fly
|
||||
- **Targets:** Chest, front delts
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ The baseline anti-extension hold — a rigid line from head to heels.
|
||||
- **Type:** Anti-extension (isometric hold)
|
||||
- **Targets:** Rectus abdominis, deep core, glutes, shoulders
|
||||
- **Prescription:** 3 × 30–60 s holds
|
||||
- **Defaults:** 3 × 30 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ lower all the way back down.
|
||||
- **Type:** Bodyweight vertical pull
|
||||
- **Targets:** Lats, biceps, mid-back, grip
|
||||
- **Prescription:** 3 × as many strict reps as possible
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ press back up without the hips sagging.
|
||||
- **Type:** Bodyweight horizontal press
|
||||
- **Targets:** Chest, front delts, triceps, trunk
|
||||
- **Prescription:** 3 × 8–15
|
||||
- **Defaults:** 3 × 8 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ wide and back, hitting what pressing never touches.
|
||||
- **Type:** Machine-based reverse fly
|
||||
- **Targets:** Rear delts, mid-back
|
||||
- **Prescription:** 3 × 12–15, light
|
||||
- **Defaults:** 3 × 12 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Curl the pelvis toward the ribs — lower-ab flexion without neck strain.
|
||||
- **Type:** Spinal flexion (lower)
|
||||
- **Targets:** Lower rectus abdominis, deep core
|
||||
- **Prescription:** 3 × 12–15 reps, controlled negative
|
||||
- **Defaults:** 3 × 12 bodyweight
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ the hamstrings stop you, then stand back up. Barbell or dumbbells.
|
||||
- **Type:** Free-weight hinge (barbell or dumbbells)
|
||||
- **Targets:** Hamstrings, glutes, spinal erectors
|
||||
- **Prescription:** 3 × 8–10
|
||||
- **Defaults:** 3 × 8 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ obliques.
|
||||
- **Type:** Machine-based trunk rotation
|
||||
- **Targets:** Obliques, deep core
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ straight back against the chest pad.
|
||||
- **Type:** Machine-based horizontal pull
|
||||
- **Targets:** Mid-back, latissimus dorsi, rear delts, biceps
|
||||
- **Prescription:** 4 × 10
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ against the pad.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based vertical press
|
||||
- **Targets:** Deltoids, triceps, upper trapezius
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Anti-lateral-flexion hold — resist the hips dropping toward the floor.
|
||||
- **Type:** Anti-lateral flexion (isometric hold)
|
||||
- **Targets:** Obliques, quadratus lumborum, glute medius, shoulder stability
|
||||
- **Prescription:** 3 × 20–45 s per side
|
||||
- **Defaults:** 3 × 20 s
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ chest to full extension.
|
||||
- **Category:** Main circuit
|
||||
- **Type:** Machine-based elbow extension
|
||||
- **Targets:** Triceps brachii
|
||||
- **Defaults:** 4 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ chest height down to straight arms.
|
||||
- **Type:** Machine-based cable elbow extension
|
||||
- **Targets:** Triceps
|
||||
- **Prescription:** 3 × 10–12
|
||||
- **Defaults:** 3 × 10 weighted
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -254,12 +254,16 @@ struct ExerciseListView: View {
|
||||
let newDocs = names
|
||||
.filter { !existingNames.contains($0) }
|
||||
.enumerated()
|
||||
.map { i, exName in
|
||||
ExerciseDocument(
|
||||
.map { i, exName -> ExerciseDocument in
|
||||
// Seed from the library's authored `**Defaults:**` when available,
|
||||
// falling back to a plain 3×10 weighted guess; weight always starts
|
||||
// at 0 (there's no prior lift to seed it from).
|
||||
let defaults = ExerciseInfoLibrary.info(for: exName)?.defaults
|
||||
return ExerciseDocument(
|
||||
id: ULID.make(), name: exName, order: base + i,
|
||||
sets: 3, reps: 10, weight: 40,
|
||||
loadType: LoadType.weight.rawValue,
|
||||
durationSeconds: 0
|
||||
sets: defaults?.sets ?? 3, reps: defaults?.reps ?? 10, weight: 0,
|
||||
loadType: (defaults?.loadType ?? .weight).rawValue,
|
||||
durationSeconds: defaults?.durationSeconds ?? 0
|
||||
)
|
||||
}
|
||||
doc.exercises.append(contentsOf: newDocs)
|
||||
|
||||
@@ -175,12 +175,16 @@ struct SplitDetailView: View {
|
||||
let newDocs = names
|
||||
.filter { !existingNames.contains($0) }
|
||||
.enumerated()
|
||||
.map { i, exName in
|
||||
ExerciseDocument(
|
||||
.map { i, exName -> ExerciseDocument in
|
||||
// Seed from the library's authored `**Defaults:**` when available,
|
||||
// falling back to a plain 3×10 weighted guess; weight always starts
|
||||
// at 0 (there's no prior lift to seed it from).
|
||||
let defaults = ExerciseInfoLibrary.info(for: exName)?.defaults
|
||||
return ExerciseDocument(
|
||||
id: ULID.make(), name: exName, order: base + i,
|
||||
sets: 3, reps: 10, weight: 40,
|
||||
loadType: LoadType.weight.rawValue,
|
||||
durationSeconds: 0
|
||||
sets: defaults?.sets ?? 3, reps: defaults?.reps ?? 10, weight: 0,
|
||||
loadType: (defaults?.loadType ?? .weight).rawValue,
|
||||
durationSeconds: defaults?.durationSeconds ?? 0
|
||||
)
|
||||
}
|
||||
doc.exercises.append(contentsOf: newDocs)
|
||||
|
||||
@@ -182,8 +182,11 @@ struct WorkoutLogListView: View {
|
||||
SplitExercisePickerSheet(
|
||||
split: split,
|
||||
existingExerciseNames: Set(sortedLogs.map { $0.exerciseName })
|
||||
) { exercise in
|
||||
addExerciseFromSplit(exercise)
|
||||
) { selection in
|
||||
switch selection {
|
||||
case .fromSplit(let exercise): addExerciseFromSplit(exercise)
|
||||
case .fromLibrary(let name): addExerciseFromLibrary(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
@@ -305,6 +308,21 @@ struct WorkoutLogListView: View {
|
||||
}
|
||||
|
||||
private func addExerciseFromSplit(_ exercise: Exercise) {
|
||||
appendExercise(ExerciseDocument(from: exercise))
|
||||
}
|
||||
|
||||
/// Add an exercise that isn't part of this workout's split, picked from the full
|
||||
/// exercise library. Seeds a plan from, in priority order: the most recent logged
|
||||
/// set of this exercise (any workout), the library's authored `**Defaults:**`, or
|
||||
/// a plain 3×10 fallback when neither exists.
|
||||
private func addExerciseFromLibrary(_ name: String) {
|
||||
appendExercise(libraryPlan(for: name))
|
||||
}
|
||||
|
||||
/// Append a freshly-planned exercise to the working doc, persist, and push
|
||||
/// straight into its progress flow. Shared tail of `addExerciseFromSplit` and
|
||||
/// `addExerciseFromLibrary`.
|
||||
private func appendExercise(_ plan: ExerciseDocument) {
|
||||
let now = Date()
|
||||
|
||||
// Reuse the workout's start time only when it's the very first exercise.
|
||||
@@ -313,7 +331,7 @@ struct WorkoutLogListView: View {
|
||||
}
|
||||
doc.end = nil
|
||||
|
||||
let newLog = WorkoutLogDocument(planFrom: ExerciseDocument(from: exercise), order: doc.logs.count, date: now)
|
||||
let newLog = WorkoutLogDocument(planFrom: plan, order: doc.logs.count, date: now)
|
||||
doc.logs.append(newLog)
|
||||
save()
|
||||
|
||||
@@ -321,6 +339,46 @@ struct WorkoutLogListView: View {
|
||||
addedLog = LogRoute(id: newLog.id)
|
||||
}
|
||||
|
||||
/// Plan defaults for a library exercise with no split entry: copy the most recent
|
||||
/// logged set of this exercise (sets/reps/weight/loadType/duration and machine
|
||||
/// settings) if one exists anywhere; otherwise the library's authored defaults
|
||||
/// (weight always starts at 0 — there's no prior lift to copy); otherwise a plain
|
||||
/// 3×10 bodyweight-style fallback.
|
||||
private func libraryPlan(for name: String) -> ExerciseDocument {
|
||||
if let recent = mostRecentLog(named: name) {
|
||||
return ExerciseDocument(
|
||||
id: ULID.make(), name: name, order: doc.logs.count,
|
||||
sets: recent.sets, reps: recent.reps, weight: recent.weight,
|
||||
loadType: recent.loadType, durationSeconds: recent.durationTotalSeconds,
|
||||
machineSettings: recent.machineSettings
|
||||
)
|
||||
}
|
||||
let info = ExerciseInfoLibrary.info(for: name)
|
||||
if let defaults = info?.defaults {
|
||||
return ExerciseDocument(
|
||||
id: ULID.make(), name: name, order: doc.logs.count,
|
||||
sets: defaults.sets, reps: defaults.reps, weight: 0,
|
||||
loadType: defaults.loadType.rawValue, durationSeconds: defaults.durationSeconds,
|
||||
machineSettings: info?.isMachineBased == true ? [] : nil
|
||||
)
|
||||
}
|
||||
return ExerciseDocument(
|
||||
id: ULID.make(), name: name, order: doc.logs.count,
|
||||
sets: 3, reps: 10, weight: 0, loadType: LoadType.none.rawValue, durationSeconds: 0
|
||||
)
|
||||
}
|
||||
|
||||
/// Most recent `WorkoutLog` cache entity logged under `name`, across all
|
||||
/// workouts — the best available signal for "what did I actually do last time."
|
||||
private func mostRecentLog(named name: String) -> WorkoutLog? {
|
||||
var descriptor = FetchDescriptor<WorkoutLog>(
|
||||
predicate: #Predicate { $0.exerciseName == name },
|
||||
sortBy: [SortDescriptor(\.date, order: .reverse)]
|
||||
)
|
||||
descriptor.fetchLimit = 1
|
||||
return try? modelContext.fetch(descriptor).first
|
||||
}
|
||||
|
||||
/// Apply edited machine settings from the sheet: update the log's plan-time
|
||||
/// snapshot (persisted with the workout), then write the same settings back to
|
||||
/// the originating split's exercise as the durable default. Sequenced in one
|
||||
@@ -490,57 +548,129 @@ struct SplitExercisePickerSheet: View {
|
||||
|
||||
let split: Split?
|
||||
let existingExerciseNames: Set<String>
|
||||
let onExerciseSelected: (Exercise) -> Void
|
||||
let onSelected: (Selection) -> Void
|
||||
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
||||
@State private var searchText = ""
|
||||
/// Parsed once per sheet presentation (`onAppear`), not per row render — the
|
||||
/// library section's name list is stable for the sheet's lifetime.
|
||||
@State private var libraryInfoByName: [String: ExerciseInfo] = [:]
|
||||
|
||||
private var availableExercises: [Exercise] {
|
||||
/// A pick from either source: the split path hands back the live `Exercise`
|
||||
/// entity (as today), the library path just the exercise name.
|
||||
enum Selection {
|
||||
case fromSplit(Exercise)
|
||||
case fromLibrary(String)
|
||||
}
|
||||
|
||||
/// Section 1 — this workout's split, minus exercises already added.
|
||||
private var splitExercises: [Exercise] {
|
||||
guard let split else { return [] }
|
||||
return split.exercisesArray.filter { !existingExerciseNames.contains($0.name) }
|
||||
}
|
||||
|
||||
/// Section 2 — every library exercise not already in the workout and not
|
||||
/// already offered in section 1.
|
||||
private var libraryExerciseNames: [String] {
|
||||
let shown = Set(splitExercises.map(\.name))
|
||||
return ExerciseMotionLibrary.exerciseNames.filter {
|
||||
!existingExerciseNames.contains($0) && !shown.contains($0)
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredSplitExercises: [Exercise] {
|
||||
guard !searchText.isEmpty else { return splitExercises }
|
||||
return splitExercises.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
private var filteredLibraryNames: [String] {
|
||||
guard !searchText.isEmpty else { return libraryExerciseNames }
|
||||
return libraryExerciseNames.filter { $0.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
/// Whether there's anything left to add at all — independent of the current
|
||||
/// search, so a no-results search doesn't get confused with a truly empty sheet.
|
||||
private var hasAnyExercises: Bool {
|
||||
!splitExercises.isEmpty || !libraryExerciseNames.isEmpty
|
||||
}
|
||||
|
||||
/// "3 × 12" / "3 × 30 s" — the library's suggested plan for a row's caption.
|
||||
private func defaultsCaption(_ defaults: ExerciseInfo.Defaults) -> String {
|
||||
switch defaults.loadType {
|
||||
case .duration: "\(defaults.sets) × \(defaults.durationSeconds) s"
|
||||
case .weight, .none: "\(defaults.sets) × \(defaults.reps)"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if !availableExercises.isEmpty {
|
||||
if !hasAnyExercises {
|
||||
ContentUnavailableView(
|
||||
"All Exercises Added",
|
||||
systemImage: "checkmark.circle",
|
||||
description: Text("Every exercise is already part of this workout.")
|
||||
)
|
||||
} else if filteredSplitExercises.isEmpty && filteredLibraryNames.isEmpty {
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
} else {
|
||||
List {
|
||||
ForEach(availableExercises) { exercise in
|
||||
Button {
|
||||
onExerciseSelected(exercise)
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.foregroundColor(.primary)
|
||||
Text(exercise.planSummary(weightUnit: weightUnit))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
if !filteredSplitExercises.isEmpty, let split {
|
||||
Section("From \(split.name)") {
|
||||
ForEach(filteredSplitExercises) { exercise in
|
||||
Button {
|
||||
onSelected(.fromSplit(exercise))
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.foregroundColor(.primary)
|
||||
Text(exercise.planSummary(weightUnit: weightUnit))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !filteredLibraryNames.isEmpty {
|
||||
Section("All Exercises") {
|
||||
ForEach(filteredLibraryNames, id: \.self) { name in
|
||||
Button {
|
||||
onSelected(.fromLibrary(name))
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(name)
|
||||
.foregroundColor(.primary)
|
||||
if let defaults = libraryInfoByName[name]?.defaults {
|
||||
Text(defaultsCaption(defaults))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "plus.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if split == nil {
|
||||
ContentUnavailableView(
|
||||
"No Split Selected",
|
||||
systemImage: "dumbbell",
|
||||
description: Text("This workout has no associated split.")
|
||||
)
|
||||
} else if split?.exercisesArray.isEmpty == true {
|
||||
ContentUnavailableView(
|
||||
"No Exercises in Split",
|
||||
systemImage: "dumbbell",
|
||||
description: Text("Add exercises to your split first.")
|
||||
)
|
||||
} else {
|
||||
ContentUnavailableView(
|
||||
"All Exercises Added",
|
||||
systemImage: "checkmark.circle",
|
||||
description: Text("You've added all exercises from this split.")
|
||||
)
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.onAppear {
|
||||
guard libraryInfoByName.isEmpty else { return }
|
||||
for name in libraryExerciseNames {
|
||||
libraryInfoByName[name] = ExerciseInfoLibrary.info(for: name)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Exercise")
|
||||
|
||||
Reference in New Issue
Block a user