wip
This commit is contained in:
56
Workouts/Views/Splits/DraggableSplitItem.swift
Normal file
56
Workouts/Views/Splits/DraggableSplitItem.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// DraggableSplitItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/18/25 at 2:45 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DraggableSplitItem: View {
|
||||
|
||||
var name: String
|
||||
var color: Color
|
||||
var systemImageName: String
|
||||
var exerciseCount: Int
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack(alignment: .bottom) {
|
||||
// Golden ratio rectangle (1:1.618)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [color, color.darker(by: 0.2)]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.aspectRatio(1.618, contentMode: .fit)
|
||||
.shadow(radius: 2)
|
||||
|
||||
VStack {
|
||||
// Icon in the center
|
||||
Image(systemName: systemImageName)
|
||||
.font(.system(size: 40, weight: .bold))
|
||||
.offset(y: -15)
|
||||
|
||||
// Name at the bottom inside the rectangle
|
||||
Text(name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
Text("\(exerciseCount) exercises")
|
||||
.font(.caption)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
99
Workouts/Views/Splits/SortableForEach.swift
Normal file
99
Workouts/Views/Splits/SortableForEach.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// SortableForEach.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/18/25 at 2:04 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct SortableForEach<Data, Content>: View where Data: Hashable, Content: View {
|
||||
@Binding var data: [Data]
|
||||
@Binding var allowReordering: Bool
|
||||
private let content: (Data, Bool) -> Content
|
||||
|
||||
@State private var draggedItem: Data?
|
||||
@State private var hasChangedLocation: Bool = false
|
||||
|
||||
public init(_ data: Binding<[Data]>,
|
||||
allowReordering: Binding<Bool>,
|
||||
@ViewBuilder content: @escaping (Data, Bool) -> Content) {
|
||||
_data = data
|
||||
_allowReordering = allowReordering
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ForEach(data, id: \.self) { item in
|
||||
if allowReordering {
|
||||
content(item, hasChangedLocation && draggedItem == item)
|
||||
.onDrag {
|
||||
draggedItem = item
|
||||
return NSItemProvider(object: "\(item.hashValue)" as NSString)
|
||||
}
|
||||
.onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate(
|
||||
item: item,
|
||||
data: $data,
|
||||
draggedItem: $draggedItem,
|
||||
hasChangedLocation: $hasChangedLocation))
|
||||
} else {
|
||||
content(item, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DragRelocateDelegate<ItemType>: DropDelegate where ItemType : Equatable {
|
||||
let item: ItemType
|
||||
@Binding var data: [ItemType]
|
||||
@Binding var draggedItem: ItemType?
|
||||
@Binding var hasChangedLocation: Bool
|
||||
|
||||
func dropEntered(info: DropInfo) {
|
||||
guard item != draggedItem,
|
||||
let current = draggedItem,
|
||||
let from = data.firstIndex(of: current),
|
||||
let to = data.firstIndex(of: item)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
hasChangedLocation = true
|
||||
|
||||
if data[to] != current {
|
||||
withAnimation {
|
||||
data.move(
|
||||
fromOffsets: IndexSet(integer: from),
|
||||
toOffset: (to > from) ? to + 1 : to
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dropUpdated(info: DropInfo) -> DropProposal? {
|
||||
DropProposal(operation: .move)
|
||||
}
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
// Update the order property of each item to match its position in the array
|
||||
updateItemOrders()
|
||||
|
||||
hasChangedLocation = false
|
||||
draggedItem = nil
|
||||
return true
|
||||
}
|
||||
|
||||
// Helper method to update the order property of each item
|
||||
private func updateItemOrders() {
|
||||
// Only update orders if we're working with items that have an 'order' property
|
||||
for (index, item) in data.enumerated() {
|
||||
// Use key path and dynamic member lookup to set the order if available
|
||||
if let orderableItem = item as? any OrderableItem {
|
||||
orderableItem.updateOrder(to: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct SplitExercisesListView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@ -18,6 +19,7 @@ struct SplitExercisesListView: View {
|
||||
@State private var showingAddSheet: Bool = false
|
||||
@State private var itemToEdit: SplitExerciseAssignment? = nil
|
||||
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
||||
@State private var createdWorkout: Workout? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@ -67,24 +69,59 @@ struct SplitExercisesListView: View {
|
||||
.navigationTitle("\(model.name)")
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showingAddSheet.toggle() }) {
|
||||
Image(systemName: "plus")
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Start This Split") {
|
||||
let split = model
|
||||
let workout = Workout(start: Date(), split: split)
|
||||
modelContext.insert(workout)
|
||||
if let exercises = split.exercises {
|
||||
for assignment in exercises {
|
||||
let workoutLog = WorkoutLog(
|
||||
workout: workout,
|
||||
exerciseName: assignment.exerciseName,
|
||||
date: Date(),
|
||||
order: assignment.order,
|
||||
sets: assignment.sets,
|
||||
reps: assignment.reps,
|
||||
weight: assignment.weight
|
||||
)
|
||||
modelContext.insert(workoutLog)
|
||||
}
|
||||
}
|
||||
try? modelContext.save()
|
||||
|
||||
// Set the created workout to trigger navigation
|
||||
createdWorkout = workout
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet (isPresented: $showingAddSheet) {
|
||||
ExercisePickerView { exerciseName in
|
||||
itemToEdit = SplitExerciseAssignment(
|
||||
split: model,
|
||||
exerciseName: exerciseName,
|
||||
order: 0,
|
||||
sets: 3,
|
||||
reps: 10,
|
||||
weight: 40
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $createdWorkout, destination: { workout in
|
||||
WorkoutLogView(workout: workout)
|
||||
})
|
||||
// .sheet(item: $createdWorkout) { workout in
|
||||
// NavigationStack {
|
||||
// WorkoutLogView(workout: workout)
|
||||
// }
|
||||
// }
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .navigationBarTrailing) {
|
||||
// Button(action: { showingAddSheet.toggle() }) {
|
||||
// Image(systemName: "plus")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .sheet (isPresented: $showingAddSheet) {
|
||||
// ExercisePickerView { exerciseName in
|
||||
// itemToEdit = SplitExerciseAssignment(
|
||||
// split: model,
|
||||
// exerciseName: exerciseName,
|
||||
// order: 0,
|
||||
// sets: 3,
|
||||
// reps: 10,
|
||||
// weight: 40
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
SplitExerciseAssignmentAddEditView(model: item)
|
||||
}
|
||||
|
@ -14,83 +14,49 @@ struct SplitsView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Query(sort: [
|
||||
SortDescriptor(\Split.order),
|
||||
SortDescriptor(\Split.name)
|
||||
]) private var splits: [Split]
|
||||
|
||||
@State var splits: [Split] = []
|
||||
|
||||
@State private var showingAddSheet: Bool = false
|
||||
@State private var allowSorting: Bool = true
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
|
||||
ForEach(splits) { split in
|
||||
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
|
||||
NavigationLink {
|
||||
SplitExercisesListView(model: split)
|
||||
} label: {
|
||||
VStack {
|
||||
ZStack(alignment: .bottom) {
|
||||
// Golden ratio rectangle (1:1.618)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [split.getColor(), split.getColor().darker(by: 0.2)]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.aspectRatio(1.618, contentMode: .fit)
|
||||
.shadow(radius: 2)
|
||||
|
||||
VStack {
|
||||
// Icon in the center
|
||||
Image(systemName: split.systemImage)
|
||||
.font(.system(size: 40, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.offset(y: -15)
|
||||
|
||||
// Name at the bottom inside the rectangle
|
||||
Text(split.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise count below the rectangle
|
||||
Text("\(split.exercises?.count ?? 0) exercises")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
DraggableSplitItem(
|
||||
name: split.name,
|
||||
color: Color.color(from: split.color),
|
||||
systemImageName: split.systemImage,
|
||||
exerciseCount: split.exercises?.count ?? 0
|
||||
)
|
||||
.overlay(dragging ? Color.white.opacity(0.8) : Color.clear)
|
||||
}
|
||||
}
|
||||
.onMove(perform: { indices, destination in
|
||||
var splitArray = Array(splits)
|
||||
splitArray.move(fromOffsets: indices, toOffset: destination)
|
||||
for (index, split) in splitArray.enumerated() {
|
||||
split.order = index
|
||||
}
|
||||
if let modelContext = splitArray.first?.modelContext {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Error saving after reordering: \(error)")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Splits")
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showingAddSheet.toggle() }) {
|
||||
Image(systemName: "plus")
|
||||
.onAppear {
|
||||
do {
|
||||
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
|
||||
sortBy: [
|
||||
SortDescriptor(\Split.order),
|
||||
SortDescriptor(\Split.name)
|
||||
]
|
||||
))
|
||||
} catch {
|
||||
print("ERROR: failed to load splits \(error)")
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showingAddSheet.toggle() }) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,16 +59,16 @@ struct WorkoutsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Workouts")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Start Workout") {
|
||||
showingSplitPicker = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
WorkoutEditView(workout: item)
|
||||
}
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .primaryAction) {
|
||||
// Button("Start Workout") {
|
||||
// showingSplitPicker = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .sheet(item: $itemToEdit) { item in
|
||||
// WorkoutEditView(workout: item)
|
||||
// }
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
|
Reference in New Issue
Block a user