Refactor UI: move Splits to Settings, redesign ExerciseView
Schema & Models: - Add notes, loadType, duration fields to WorkoutLog - Align Watch schema with iOS (use duration Date instead of separate mins/secs) - Add duration helper properties to Exercise and WorkoutLog UI Changes: - Remove Splits and Settings tabs, single Workout Logs view - Add gear button in nav bar to access Settings as sheet - Move Splits section into Settings view with inline list - Redesign ExerciseView with read-only Plan/Notes tiles and Edit buttons - Add PlanEditView and NotesEditView with Cancel/Save buttons - Auto-dismiss ExerciseView when completing last set - Navigate to ExerciseView when adding new exercise Data Flow: - Plan edits sync to both WorkoutLog and corresponding Exercise - Changes propagate up navigation chain via CoreData
This commit is contained in:
@@ -10,7 +10,10 @@
|
|||||||
"Bash(xcrun simctl install:*)",
|
"Bash(xcrun simctl install:*)",
|
||||||
"Bash(xcrun simctl launch:*)",
|
"Bash(xcrun simctl launch:*)",
|
||||||
"Bash(xcrun simctl get_app_container:*)",
|
"Bash(xcrun simctl get_app_container:*)",
|
||||||
"Bash(log show:*)"
|
"Bash(log show:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -21,6 +21,29 @@ public class Exercise: NSManagedObject, Identifiable {
|
|||||||
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||||
set { loadType = Int32(newValue.rawValue) }
|
set { loadType = Int32(newValue.rawValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Duration helpers for minutes/seconds conversion
|
||||||
|
var durationMinutes: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) / 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let seconds = durationSeconds
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationSeconds: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) % 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let minutes = durationMinutes
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Request
|
// MARK: - Fetch Request
|
||||||
|
|||||||
@@ -7,23 +7,59 @@ public class WorkoutLog: NSManagedObject, Identifiable {
|
|||||||
@NSManaged public var sets: Int32
|
@NSManaged public var sets: Int32
|
||||||
@NSManaged public var reps: Int32
|
@NSManaged public var reps: Int32
|
||||||
@NSManaged public var weight: Int32
|
@NSManaged public var weight: Int32
|
||||||
@NSManaged private var statusRaw: String?
|
|
||||||
@NSManaged public var order: Int32
|
@NSManaged public var order: Int32
|
||||||
@NSManaged public var exerciseName: String
|
@NSManaged public var exerciseName: String
|
||||||
@NSManaged public var currentStateIndex: Int32
|
@NSManaged public var currentStateIndex: Int32
|
||||||
@NSManaged public var elapsedSeconds: Int32
|
@NSManaged public var elapsedSeconds: Int32
|
||||||
@NSManaged public var completed: Bool
|
@NSManaged public var completed: Bool
|
||||||
|
@NSManaged public var loadType: Int32
|
||||||
|
@NSManaged public var duration: Date?
|
||||||
|
@NSManaged public var notes: String?
|
||||||
|
|
||||||
@NSManaged public var workout: Workout?
|
@NSManaged public var workout: Workout?
|
||||||
|
|
||||||
public var id: NSManagedObjectID { objectID }
|
public var id: NSManagedObjectID { objectID }
|
||||||
|
|
||||||
var status: WorkoutStatus? {
|
var status: WorkoutStatus {
|
||||||
get {
|
get {
|
||||||
guard let raw = statusRaw else { return nil }
|
willAccessValue(forKey: "status")
|
||||||
return WorkoutStatus(rawValue: raw)
|
let raw = primitiveValue(forKey: "status") as? String ?? "notStarted"
|
||||||
|
didAccessValue(forKey: "status")
|
||||||
|
return WorkoutStatus(rawValue: raw) ?? .notStarted
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
willChangeValue(forKey: "status")
|
||||||
|
setPrimitiveValue(newValue.rawValue, forKey: "status")
|
||||||
|
didChangeValue(forKey: "status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadTypeEnum: LoadType {
|
||||||
|
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||||
|
set { loadType = Int32(newValue.rawValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration helpers for minutes/seconds conversion
|
||||||
|
var durationMinutes: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) / 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let seconds = durationSeconds
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationSeconds: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) % 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let minutes = durationMinutes
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||||
}
|
}
|
||||||
set { statusRaw = newValue?.rawValue }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,11 @@
|
|||||||
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="currentStateIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="currentStateIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="date" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
<attribute name="date" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="duration" optional="YES" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||||
<attribute name="elapsedSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="elapsedSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="exerciseName" attributeType="String" defaultValueString=""/>
|
<attribute name="exerciseName" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="loadType" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="notes" optional="YES" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="reps" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="reps" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="sets" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="sets" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
|||||||
@@ -15,22 +15,7 @@ struct ContentView: View {
|
|||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
|
||||||
WorkoutLogsView()
|
WorkoutLogsView()
|
||||||
.tabItem {
|
|
||||||
Label("Workout Logs", systemImage: "list.bullet.clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
SplitsView()
|
|
||||||
.tabItem {
|
|
||||||
Label("Splits", systemImage: "dumbbell.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsView()
|
|
||||||
.tabItem {
|
|
||||||
Label("Settings", systemImage: "gear")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,29 @@ public class Exercise: NSManagedObject, Identifiable {
|
|||||||
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||||
set { loadType = Int32(newValue.rawValue) }
|
set { loadType = Int32(newValue.rawValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Duration helpers for minutes/seconds conversion
|
||||||
|
var durationMinutes: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) / 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let seconds = durationSeconds
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationSeconds: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) % 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let minutes = durationMinutes
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Request
|
// MARK: - Fetch Request
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public class WorkoutLog: NSManagedObject, Identifiable {
|
|||||||
@NSManaged public var currentStateIndex: Int32
|
@NSManaged public var currentStateIndex: Int32
|
||||||
@NSManaged public var elapsedSeconds: Int32
|
@NSManaged public var elapsedSeconds: Int32
|
||||||
@NSManaged public var completed: Bool
|
@NSManaged public var completed: Bool
|
||||||
|
@NSManaged public var loadType: Int32
|
||||||
|
@NSManaged public var duration: Date?
|
||||||
|
@NSManaged public var notes: String?
|
||||||
|
|
||||||
@NSManaged public var workout: Workout?
|
@NSManaged public var workout: Workout?
|
||||||
|
|
||||||
@@ -30,6 +33,34 @@ public class WorkoutLog: NSManagedObject, Identifiable {
|
|||||||
didChangeValue(forKey: "status")
|
didChangeValue(forKey: "status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loadTypeEnum: LoadType {
|
||||||
|
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||||
|
set { loadType = Int32(newValue.rawValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration helpers for minutes/seconds conversion
|
||||||
|
var durationMinutes: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) / 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let seconds = durationSeconds
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationSeconds: Int {
|
||||||
|
get {
|
||||||
|
guard let duration = duration else { return 0 }
|
||||||
|
return Int(duration.timeIntervalSince1970) % 60
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let minutes = durationMinutes
|
||||||
|
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Request
|
// MARK: - Fetch Request
|
||||||
|
|||||||
@@ -6,17 +6,82 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
import IndieAbout
|
import IndieAbout
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
|
@FetchRequest(
|
||||||
|
sortDescriptors: [
|
||||||
|
NSSortDescriptor(keyPath: \Split.order, ascending: true),
|
||||||
|
NSSortDescriptor(keyPath: \Split.name, ascending: true)
|
||||||
|
],
|
||||||
|
animation: .default
|
||||||
|
)
|
||||||
|
private var splits: FetchedResults<Split>
|
||||||
|
|
||||||
|
@State private var showingAddSplitSheet = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
// MARK: - Splits Section
|
||||||
|
Section(header: Text("Splits")) {
|
||||||
|
if splits.isEmpty {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: "dumbbell.fill")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("No Splits Yet")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Create a split to organize your workout routine.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(splits, id: \.objectID) { split in
|
||||||
|
NavigationLink {
|
||||||
|
SplitDetailView(split: split)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: split.systemImage)
|
||||||
|
.foregroundColor(Color.color(from: split.color))
|
||||||
|
.frame(width: 24)
|
||||||
|
Text(split.name)
|
||||||
|
Spacer()
|
||||||
|
Text("\(split.exercisesArray.count)")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showingAddSplitSheet = true
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text("Add Split")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Account Section
|
||||||
Section(header: Text("Account")) {
|
Section(header: Text("Account")) {
|
||||||
Text("Settings coming soon")
|
Text("Settings coming soon")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - About Section
|
||||||
Section {
|
Section {
|
||||||
IndieAbout(configuration: AppInfoConfiguration(
|
IndieAbout(configuration: AppInfoConfiguration(
|
||||||
documents: [
|
documents: [
|
||||||
@@ -28,10 +93,14 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
|
.sheet(isPresented: $showingAddSplitSheet) {
|
||||||
|
SplitAddEditView(split: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,46 +17,19 @@ struct ExerciseView: View {
|
|||||||
|
|
||||||
@ObservedObject var workoutLog: WorkoutLog
|
@ObservedObject var workoutLog: WorkoutLog
|
||||||
|
|
||||||
var allLogs: [WorkoutLog]
|
|
||||||
var currentIndex: Int = 0
|
|
||||||
|
|
||||||
@State private var progress: Int = 0
|
@State private var progress: Int = 0
|
||||||
@State private var navigateTo: WorkoutLog? = nil
|
@State private var showingPlanEdit = false
|
||||||
|
@State private var showingNotesEdit = false
|
||||||
|
|
||||||
let notStartedColor = Color.white
|
let notStartedColor = Color.white
|
||||||
let completedColor = Color.green
|
let completedColor = Color.green
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section(header: Text("Navigation")) {
|
// MARK: - Progress Section
|
||||||
HStack {
|
|
||||||
Button(action: navigateToPrevious) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "chevron.left")
|
|
||||||
Text("Previous")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(currentIndex <= 0)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
Text("\(currentIndex + 1) of \(allLogs.count)")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(action: navigateToNext) {
|
|
||||||
HStack {
|
|
||||||
Text("Next")
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(currentIndex >= allLogs.count - 1)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Progress")) {
|
Section(header: Text("Progress")) {
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 2) {
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 4) {
|
||||||
ForEach(1...Int(workoutLog.sets), id: \.self) { index in
|
ForEach(1...max(1, Int(workoutLog.sets)), id: \.self) { index in
|
||||||
ZStack {
|
ZStack {
|
||||||
let completed = index <= progress
|
let completed = index <= progress
|
||||||
let color = completed ? completedColor : notStartedColor
|
let color = completed ? completedColor : notStartedColor
|
||||||
@@ -71,95 +44,181 @@ struct ExerciseView: View {
|
|||||||
.aspectRatio(0.618, contentMode: .fit)
|
.aspectRatio(0.618, contentMode: .fit)
|
||||||
.shadow(radius: 2)
|
.shadow(radius: 2)
|
||||||
Text("\(index)")
|
Text("\(index)")
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.colorInvert()
|
.colorInvert()
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if progress == index {
|
let totalSets = Int(workoutLog.sets)
|
||||||
|
let isLastTile = index == totalSets
|
||||||
|
let wasAlreadyAtThisProgress = progress == index
|
||||||
|
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
if wasAlreadyAtThisProgress {
|
||||||
progress = 0
|
progress = 0
|
||||||
} else {
|
} else {
|
||||||
progress = index
|
progress = index
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateLogStatus()
|
updateLogStatus()
|
||||||
|
|
||||||
|
// If tapping the last tile to complete, go back to list
|
||||||
|
if isLastTile && !wasAlreadyAtThisProgress {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Plan")) {
|
// MARK: - Plan Section (Read-only with Edit button)
|
||||||
Stepper("\(workoutLog.sets) sets", value: Binding(
|
Section {
|
||||||
get: { Int(workoutLog.sets) },
|
PlanTilesView(workoutLog: workoutLog)
|
||||||
set: { workoutLog.sets = Int32($0) }
|
} header: {
|
||||||
), in: 1...10)
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
Stepper("\(workoutLog.reps) reps", value: Binding(
|
|
||||||
get: { Int(workoutLog.reps) },
|
|
||||||
set: { workoutLog.reps = Int32($0) }
|
|
||||||
), in: 1...25)
|
|
||||||
.font(.title)
|
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(workoutLog.weight) lbs")
|
Text("Plan")
|
||||||
VStack(alignment: .trailing) {
|
Spacer()
|
||||||
Stepper("", value: Binding(
|
Button("Edit") {
|
||||||
get: { Int(workoutLog.weight) },
|
showingPlanEdit = true
|
||||||
set: { workoutLog.weight = Int32($0) }
|
|
||||||
), in: 1...500)
|
|
||||||
Stepper("", value: Binding(
|
|
||||||
get: { Int(workoutLog.weight) },
|
|
||||||
set: { workoutLog.weight = Int32($0) }
|
|
||||||
), in: 1...500, step: 5)
|
|
||||||
}
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.textCase(.none)
|
||||||
}
|
}
|
||||||
.font(.title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Notes Section (Read-only with Edit button)
|
||||||
|
Section {
|
||||||
|
if let notes = workoutLog.notes, !notes.isEmpty {
|
||||||
|
Text(notes)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
} else {
|
||||||
|
Text("No notes")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.italic()
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Notes")
|
||||||
|
Spacer()
|
||||||
|
Button("Edit") {
|
||||||
|
showingNotesEdit = true
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.textCase(.none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Progress Tracking Chart
|
||||||
Section(header: Text("Progress Tracking")) {
|
Section(header: Text("Progress Tracking")) {
|
||||||
WeightProgressionChartView(exerciseName: workoutLog.exerciseName)
|
WeightProgressionChartView(exerciseName: workoutLog.exerciseName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(workoutLog.exerciseName)
|
.navigationTitle(workoutLog.exerciseName)
|
||||||
.navigationDestination(item: $navigateTo) { nextLog in
|
.sheet(isPresented: $showingPlanEdit) {
|
||||||
ExerciseView(
|
PlanEditView(workoutLog: workoutLog)
|
||||||
workoutLog: nextLog,
|
}
|
||||||
allLogs: allLogs,
|
.sheet(isPresented: $showingNotesEdit) {
|
||||||
currentIndex: allLogs.firstIndex(where: { $0.objectID == nextLog.objectID }) ?? 0
|
NotesEditView(workoutLog: workoutLog)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
progress = Int(workoutLog.currentStateIndex)
|
progress = Int(workoutLog.currentStateIndex)
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
saveChanges()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLogStatus() {
|
private func updateLogStatus() {
|
||||||
workoutLog.currentStateIndex = Int32(progress)
|
workoutLog.currentStateIndex = Int32(progress)
|
||||||
if progress >= Int(workoutLog.sets) {
|
if progress >= Int(workoutLog.sets) {
|
||||||
workoutLog.status = .completed
|
workoutLog.status = .completed
|
||||||
|
workoutLog.completed = true
|
||||||
} else if progress > 0 {
|
} else if progress > 0 {
|
||||||
workoutLog.status = .inProgress
|
workoutLog.status = .inProgress
|
||||||
|
workoutLog.completed = false
|
||||||
} else {
|
} else {
|
||||||
workoutLog.status = .notStarted
|
workoutLog.status = .notStarted
|
||||||
|
workoutLog.completed = false
|
||||||
}
|
}
|
||||||
|
updateWorkoutStatus()
|
||||||
saveChanges()
|
saveChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateWorkoutStatus() {
|
||||||
|
guard let workout = workoutLog.workout else { return }
|
||||||
|
let logs = workout.logsArray
|
||||||
|
let allCompleted = logs.allSatisfy { $0.status == .completed }
|
||||||
|
let anyInProgress = logs.contains { $0.status == .inProgress }
|
||||||
|
let allNotStarted = logs.allSatisfy { $0.status == .notStarted }
|
||||||
|
|
||||||
|
if allCompleted {
|
||||||
|
workout.status = .completed
|
||||||
|
workout.end = Date()
|
||||||
|
} else if anyInProgress || !allNotStarted {
|
||||||
|
workout.status = .inProgress
|
||||||
|
} else {
|
||||||
|
workout.status = .notStarted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func saveChanges() {
|
private func saveChanges() {
|
||||||
try? viewContext.save()
|
try? viewContext.save()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func navigateToPrevious() {
|
// MARK: - Plan Tiles View
|
||||||
guard currentIndex > 0 else { return }
|
|
||||||
let previousIndex = currentIndex - 1
|
struct PlanTilesView: View {
|
||||||
navigateTo = allLogs[previousIndex]
|
@ObservedObject var workoutLog: WorkoutLog
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if workoutLog.loadTypeEnum == .duration {
|
||||||
|
// Duration layout: Sets | Duration
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
PlanTile(label: "Sets", value: "\(workoutLog.sets)")
|
||||||
|
PlanTile(label: "Duration", value: formattedDuration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Weight layout: Sets | Reps | Weight
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
PlanTile(label: "Sets", value: "\(workoutLog.sets)")
|
||||||
|
PlanTile(label: "Reps", value: "\(workoutLog.reps)")
|
||||||
|
PlanTile(label: "Weight", value: "\(workoutLog.weight) lbs")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func navigateToNext() {
|
private var formattedDuration: String {
|
||||||
guard currentIndex < allLogs.count - 1 else { return }
|
let mins = workoutLog.durationMinutes
|
||||||
let nextIndex = currentIndex + 1
|
let secs = workoutLog.durationSeconds
|
||||||
navigateTo = allLogs[nextIndex]
|
if mins > 0 && secs > 0 {
|
||||||
|
return "\(mins)m \(secs)s"
|
||||||
|
} else if mins > 0 {
|
||||||
|
return "\(mins) min"
|
||||||
|
} else if secs > 0 {
|
||||||
|
return "\(secs) sec"
|
||||||
|
} else {
|
||||||
|
return "0 sec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlanTile: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(value)
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color(.systemGray6))
|
||||||
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
Workouts/Views/WorkoutLogs/NotesEditView.swift
Normal file
52
Workouts/Views/WorkoutLogs/NotesEditView.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// NotesEditView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct NotesEditView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@ObservedObject var workoutLog: WorkoutLog
|
||||||
|
|
||||||
|
@State private var notesText: String = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TextEditor(text: $notesText)
|
||||||
|
.frame(minHeight: 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Edit Notes")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Save") {
|
||||||
|
saveChanges()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
notesText = workoutLog.notes ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveChanges() {
|
||||||
|
workoutLog.notes = notesText
|
||||||
|
try? viewContext.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
167
Workouts/Views/WorkoutLogs/PlanEditView.swift
Normal file
167
Workouts/Views/WorkoutLogs/PlanEditView.swift
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
//
|
||||||
|
// PlanEditView.swift
|
||||||
|
// Workouts
|
||||||
|
//
|
||||||
|
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct PlanEditView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@ObservedObject var workoutLog: WorkoutLog
|
||||||
|
|
||||||
|
@State private var sets: Int = 3
|
||||||
|
@State private var reps: Int = 12
|
||||||
|
@State private var weight: Int = 0
|
||||||
|
@State private var durationMinutes: Int = 0
|
||||||
|
@State private var durationSeconds: Int = 0
|
||||||
|
@State private var selectedLoadType: LoadType = .weight
|
||||||
|
|
||||||
|
// Find the corresponding exercise in the split for syncing changes
|
||||||
|
private var correspondingExercise: Exercise? {
|
||||||
|
workoutLog.workout?.split?.exercisesArray.first { $0.name == workoutLog.exerciseName }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
// Sets and Reps side by side
|
||||||
|
Section {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
VStack {
|
||||||
|
Text("Sets")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Sets", selection: $sets) {
|
||||||
|
ForEach(1...7, id: \.self) { num in
|
||||||
|
Text("\(num)").tag(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 120)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Reps")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Reps", selection: $reps) {
|
||||||
|
ForEach(1...40, id: \.self) { num in
|
||||||
|
Text("\(num)").tag(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 120)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Type Picker
|
||||||
|
Section {
|
||||||
|
Picker("Load Type", selection: $selectedLoadType) {
|
||||||
|
Text("Weight").tag(LoadType.weight)
|
||||||
|
Text("Time").tag(LoadType.duration)
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight or Time picker based on load type
|
||||||
|
Section {
|
||||||
|
if selectedLoadType == .weight {
|
||||||
|
VStack {
|
||||||
|
Text("Weight")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Weight", selection: $weight) {
|
||||||
|
ForEach(0...300, id: \.self) { num in
|
||||||
|
Text("\(num) lbs").tag(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 150)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
VStack {
|
||||||
|
Text("Mins")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Minutes", selection: $durationMinutes) {
|
||||||
|
ForEach(0...60, id: \.self) { num in
|
||||||
|
Text("\(num)").tag(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 120)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Secs")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Seconds", selection: $durationSeconds) {
|
||||||
|
ForEach(0...59, id: \.self) { num in
|
||||||
|
Text("\(num)").tag(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(height: 120)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Edit Plan")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Save") {
|
||||||
|
saveChanges()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
sets = Int(workoutLog.sets)
|
||||||
|
reps = Int(workoutLog.reps)
|
||||||
|
weight = Int(workoutLog.weight)
|
||||||
|
durationMinutes = workoutLog.durationMinutes
|
||||||
|
durationSeconds = workoutLog.durationSeconds
|
||||||
|
selectedLoadType = workoutLog.loadTypeEnum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveChanges() {
|
||||||
|
workoutLog.sets = Int32(sets)
|
||||||
|
workoutLog.reps = Int32(reps)
|
||||||
|
workoutLog.weight = Int32(weight)
|
||||||
|
workoutLog.durationMinutes = durationMinutes
|
||||||
|
workoutLog.durationSeconds = durationSeconds
|
||||||
|
workoutLog.loadTypeEnum = selectedLoadType
|
||||||
|
|
||||||
|
// Sync to corresponding exercise
|
||||||
|
if let exercise = correspondingExercise {
|
||||||
|
exercise.sets = workoutLog.sets
|
||||||
|
exercise.reps = workoutLog.reps
|
||||||
|
exercise.weight = workoutLog.weight
|
||||||
|
exercise.loadType = workoutLog.loadType
|
||||||
|
exercise.duration = workoutLog.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
try? viewContext.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ struct WorkoutLogListView: View {
|
|||||||
|
|
||||||
@State private var showingAddSheet = false
|
@State private var showingAddSheet = false
|
||||||
@State private var itemToDelete: WorkoutLog? = nil
|
@State private var itemToDelete: WorkoutLog? = nil
|
||||||
|
@State private var newlyAddedLog: WorkoutLog? = nil
|
||||||
|
|
||||||
var sortedWorkoutLogs: [WorkoutLog] {
|
var sortedWorkoutLogs: [WorkoutLog] {
|
||||||
workout.logsArray
|
workout.logsArray
|
||||||
@@ -40,20 +41,16 @@ struct WorkoutLogListView: View {
|
|||||||
} else {
|
} else {
|
||||||
Form {
|
Form {
|
||||||
Section(header: Text("\(workout.label)")) {
|
Section(header: Text("\(workout.label)")) {
|
||||||
ForEach(Array(sortedWorkoutLogs.enumerated()), id: \.element.objectID) { index, log in
|
ForEach(sortedWorkoutLogs, id: \.objectID) { log in
|
||||||
let workoutLogStatus = log.status.checkboxStatus
|
let workoutLogStatus = log.status.checkboxStatus
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ExerciseView(
|
ExerciseView(workoutLog: log)
|
||||||
workoutLog: log,
|
|
||||||
allLogs: sortedWorkoutLogs,
|
|
||||||
currentIndex: index
|
|
||||||
)
|
|
||||||
} label: {
|
} label: {
|
||||||
CheckboxListItem(
|
CheckboxListItem(
|
||||||
status: workoutLogStatus,
|
status: workoutLogStatus,
|
||||||
title: log.exerciseName,
|
title: log.exerciseName,
|
||||||
subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
|
subtitle: subtitleForLog(log)
|
||||||
) {
|
) {
|
||||||
cycleStatus(for: log)
|
cycleStatus(for: log)
|
||||||
}
|
}
|
||||||
@@ -80,6 +77,9 @@ struct WorkoutLogListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationDestination(item: $newlyAddedLog) { log in
|
||||||
|
ExerciseView(workoutLog: log)
|
||||||
|
}
|
||||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed)")
|
.navigationTitle("\(workout.split?.name ?? Split.unnamed)")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
@@ -182,10 +182,31 @@ struct WorkoutLogListView: View {
|
|||||||
log.sets = exercise.sets
|
log.sets = exercise.sets
|
||||||
log.reps = exercise.reps
|
log.reps = exercise.reps
|
||||||
log.weight = exercise.weight
|
log.weight = exercise.weight
|
||||||
|
log.loadType = exercise.loadType
|
||||||
|
log.duration = exercise.duration
|
||||||
log.status = .notStarted
|
log.status = .notStarted
|
||||||
log.workout = workout
|
log.workout = workout
|
||||||
|
|
||||||
try? viewContext.save()
|
try? viewContext.save()
|
||||||
|
|
||||||
|
// Navigate to the new exercise view
|
||||||
|
newlyAddedLog = log
|
||||||
|
}
|
||||||
|
|
||||||
|
private func subtitleForLog(_ log: WorkoutLog) -> String {
|
||||||
|
if log.loadTypeEnum == .duration {
|
||||||
|
let mins = log.durationMinutes
|
||||||
|
let secs = log.durationSeconds
|
||||||
|
if mins > 0 && secs > 0 {
|
||||||
|
return "\(log.sets) × \(mins)m \(secs)s"
|
||||||
|
} else if mins > 0 {
|
||||||
|
return "\(log.sets) × \(mins) min"
|
||||||
|
} else {
|
||||||
|
return "\(log.sets) × \(secs) sec"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct WorkoutLogsView: View {
|
|||||||
private var workouts: FetchedResults<Workout>
|
private var workouts: FetchedResults<Workout>
|
||||||
|
|
||||||
@State private var showingSplitPicker = false
|
@State private var showingSplitPicker = false
|
||||||
|
@State private var showingSettings = false
|
||||||
@State private var itemToDelete: Workout? = nil
|
@State private var itemToDelete: Workout? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -55,12 +56,22 @@ struct WorkoutLogsView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("Workout Logs")
|
.navigationTitle("Workout Logs")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
showingSettings = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gearshape.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Start New") {
|
Button("Start New") {
|
||||||
showingSplitPicker.toggle()
|
showingSplitPicker.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingSettings) {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
.sheet(isPresented: $showingSplitPicker) {
|
.sheet(isPresented: $showingSplitPicker) {
|
||||||
SplitPickerSheet()
|
SplitPickerSheet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,11 @@
|
|||||||
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="currentStateIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="currentStateIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="date" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
<attribute name="date" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="duration" optional="YES" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||||
<attribute name="elapsedSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="elapsedSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="exerciseName" attributeType="String" defaultValueString=""/>
|
<attribute name="exerciseName" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="loadType" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="notes" optional="YES" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="reps" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="reps" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="sets" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="sets" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user