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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user