Remove the duplicate exercise list screen and dead reorder stubs
ExerciseListView was a near-duplicate of RoutineDetailView's exercise management, reachable only from RoutineAddEditView's "Exercises" row — drop both, leaving RoutineDetailView as the single exercise manager. OrderableItem and SortableForEach were self-declared dead stubs. Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
This commit is contained in:
@@ -1,198 +0,0 @@
|
|||||||
//
|
|
||||||
// ExerciseListView.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/18/25 at 8:38 AM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import IndieSync
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ExerciseListView: View {
|
|
||||||
@Environment(SyncEngine.self) private var sync
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
// Resolve the routine by id, not a captured entity: editing a seed's exercise from
|
|
||||||
// here clones the seed (new identity, old entity deleted), which would dangle a
|
|
||||||
// stored `Routine`. `currentRoutineID` follows that swap.
|
|
||||||
@State private var routineID: String
|
|
||||||
@Query private var routines: [Routine]
|
|
||||||
|
|
||||||
@State private var showingAddSheet: Bool = false
|
|
||||||
@State private var itemToEdit: Exercise? = nil
|
|
||||||
@State private var itemToDelete: Exercise? = nil
|
|
||||||
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
|
||||||
|
|
||||||
init(routine: Routine) {
|
|
||||||
_routineID = State(initialValue: routine.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var routine: Routine? {
|
|
||||||
let id = sync.currentRoutineID(for: routineID)
|
|
||||||
return routines.first { $0.id == id }
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if let routine {
|
|
||||||
content(for: routine)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell")
|
|
||||||
.task { dismiss() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func content(for routine: Routine) -> some View {
|
|
||||||
Form {
|
|
||||||
let sortedExercises = routine.exercisesArray
|
|
||||||
|
|
||||||
if !sortedExercises.isEmpty {
|
|
||||||
ForEach(sortedExercises) { item in
|
|
||||||
ListItem(
|
|
||||||
title: item.name,
|
|
||||||
subtitle: "\(item.sets) × \(item.reps) × \(weightUnit.format(item.weight))"
|
|
||||||
)
|
|
||||||
.swipeActions(edge: .leading) {
|
|
||||||
Button {
|
|
||||||
duplicateExercise(item)
|
|
||||||
} label: {
|
|
||||||
Label("Duplicate", systemImage: "plus.square.on.square")
|
|
||||||
}
|
|
||||||
.tint(.teal)
|
|
||||||
}
|
|
||||||
.swipeActions {
|
|
||||||
Button {
|
|
||||||
itemToDelete = item
|
|
||||||
} label: {
|
|
||||||
Label("Delete", systemImage: "trash")
|
|
||||||
}
|
|
||||||
.tint(.red)
|
|
||||||
Button {
|
|
||||||
itemToEdit = item
|
|
||||||
} label: {
|
|
||||||
Label("Edit", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onMove(perform: moveExercises)
|
|
||||||
|
|
||||||
Button {
|
|
||||||
showingAddSheet = true
|
|
||||||
} label: {
|
|
||||||
ListItem(title: "Add Exercise")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("No exercises added yet.")
|
|
||||||
Button(action: { showingAddSheet.toggle() }) {
|
|
||||||
ListItem(title: "Add Exercise")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(routine.name)
|
|
||||||
.sheet(isPresented: $showingAddSheet) {
|
|
||||||
ExercisePickerView(onExerciseSelected: { exerciseNames in
|
|
||||||
addExercises(names: exerciseNames)
|
|
||||||
}, allowMultiSelect: true,
|
|
||||||
inRoutineCounts: Dictionary(
|
|
||||||
routine.exercisesArray.map { ($0.libraryExerciseName, 1) },
|
|
||||||
uniquingKeysWith: +
|
|
||||||
))
|
|
||||||
}
|
|
||||||
.sheet(item: $itemToEdit) { item in
|
|
||||||
ExerciseAddEditView(exercise: item, routine: routine)
|
|
||||||
}
|
|
||||||
.confirmationDialog(
|
|
||||||
"Delete Exercise?",
|
|
||||||
isPresented: Binding(
|
|
||||||
get: { itemToDelete != nil },
|
|
||||||
set: { if !$0 { itemToDelete = nil } }
|
|
||||||
),
|
|
||||||
titleVisibility: .visible,
|
|
||||||
presenting: itemToDelete
|
|
||||||
) { item in
|
|
||||||
Button("Delete", role: .destructive) {
|
|
||||||
deleteExercise(item)
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
itemToDelete = nil
|
|
||||||
}
|
|
||||||
} message: { item in
|
|
||||||
Text("Remove \"\(item.name)\" from this routine?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
private func moveExercises(from source: IndexSet, to destination: Int) {
|
|
||||||
guard let routine else { return }
|
|
||||||
var exercises = routine.exercisesArray
|
|
||||||
exercises.move(fromOffsets: source, toOffset: destination)
|
|
||||||
var doc = RoutineDocument(from: routine)
|
|
||||||
doc.exercises = exercises.enumerated().map { i, ex in
|
|
||||||
var ed = ExerciseDocument(from: ex)
|
|
||||||
ed.order = i
|
|
||||||
return ed
|
|
||||||
}
|
|
||||||
doc.updatedAt = Date()
|
|
||||||
Task { await sync.save(routine: doc) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addExercises(names: [String]) {
|
|
||||||
guard let routine else { return }
|
|
||||||
var doc = RoutineDocument(from: routine)
|
|
||||||
let base = doc.exercises.count
|
|
||||||
let newDocs = names
|
|
||||||
.enumerated()
|
|
||||||
.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: defaults?.sets ?? 3, reps: defaults?.reps ?? 10, weight: 0,
|
|
||||||
loadType: (defaults?.loadType ?? .weight).rawValue,
|
|
||||||
durationSeconds: defaults?.durationSeconds ?? 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
doc.exercises.append(contentsOf: newDocs)
|
|
||||||
doc.updatedAt = Date()
|
|
||||||
Task { await sync.save(routine: doc) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deleteExercise(_ exercise: Exercise) {
|
|
||||||
guard let routine else { return }
|
|
||||||
var doc = RoutineDocument(from: routine)
|
|
||||||
doc.exercises.removeAll { $0.id == exercise.id }
|
|
||||||
for i in doc.exercises.indices {
|
|
||||||
doc.exercises[i].order = i
|
|
||||||
}
|
|
||||||
doc.updatedAt = Date()
|
|
||||||
Task { await sync.save(routine: doc) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy an exercise in place, right after itself — the interval-routine case
|
|
||||||
/// (a treadmill's "Warmup 5 min" / "Brisk Walk 10 min" / … segments all derive
|
|
||||||
/// from the same library exercise). The clone starts out an exact duplicate,
|
|
||||||
/// plan and any customized name included; renaming it is a follow-up edit.
|
|
||||||
private func duplicateExercise(_ exercise: Exercise) {
|
|
||||||
guard let routine else { return }
|
|
||||||
var doc = RoutineDocument(from: routine)
|
|
||||||
guard let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) else { return }
|
|
||||||
var copy = doc.exercises[idx]
|
|
||||||
copy.id = ULID.make()
|
|
||||||
doc.exercises.insert(copy, at: idx + 1)
|
|
||||||
for i in doc.exercises.indices {
|
|
||||||
doc.exercises[i].order = i
|
|
||||||
}
|
|
||||||
doc.updatedAt = Date()
|
|
||||||
Task { await sync.save(routine: doc) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// OrderableItem.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/18/25 at 5:19 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// No longer used — reordering is now a SyncEngine document write (onMove → doc save).
|
|
||||||
// File kept to avoid Xcode project reference errors.
|
|
||||||
@@ -160,18 +160,7 @@ struct RoutineAddEditView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let routine = routine {
|
if routine != nil {
|
||||||
Section(header: Text("Exercises")) {
|
|
||||||
NavigationLink {
|
|
||||||
ExerciseListView(routine: routine)
|
|
||||||
} label: {
|
|
||||||
ListItem(
|
|
||||||
text: "Exercises",
|
|
||||||
count: routine.exercisesArray.count
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button("Delete Routine", role: .destructive) {
|
Button("Delete Routine", role: .destructive) {
|
||||||
showingDeleteConfirmation = true
|
showingDeleteConfirmation = true
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// SortableForEach.swift
|
|
||||||
// Workouts
|
|
||||||
//
|
|
||||||
// Created by rzen on 7/18/25 at 2:04 PM.
|
|
||||||
//
|
|
||||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// No longer used — list reordering is handled with SwiftUI's native onMove modifier
|
|
||||||
// backed by SyncEngine document writes. File kept to avoid Xcode project reference errors.
|
|
||||||
Reference in New Issue
Block a user