Replaces the app-local copies of the extracted storage core with the IndieSync 0.1.0 package (pinned from the new tag): ICloudFileManager -> DocumentFileStore + TombstoneStore, ICloudFileMonitor -> MetadataObserver (batched events with the content-date churn gate), and the shared ULID / DocumentCoder / Tombstone / VersionedDocument pieces. SyncEngine stays app-specific and is rewired onto the package types; documents rename currentSchema -> currentSchemaVersion to adopt the package protocol. ULID.make() remains as a shim minting the string form the app keys on. Behavior-preserving: identical JSON bytes (same coder config), same stub wire format (kind encodes as the same strings), same soft-delete ordering, same 30-day prune. WorkoutDocument keeps its local-calendar month bucketing rather than adopting TimeBucketedLayout's UTC paths -- changing derivation would strand existing files. The watch target links the package too (ULID + DocumentCoder via Shared); iOS, watchOS, and widget targets all build.
169 lines
6.2 KiB
Swift
169 lines
6.2 KiB
Swift
//
|
||
// SplitDetailView.swift
|
||
// Workouts
|
||
//
|
||
// Created by rzen on 7/25/25 at 3:27 PM.
|
||
//
|
||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||
//
|
||
|
||
import IndieSync
|
||
import SwiftUI
|
||
import SwiftData
|
||
|
||
struct SplitDetailView: View {
|
||
@Environment(SyncEngine.self) private var sync
|
||
@Environment(AppServices.self) private var services
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
var split: Split
|
||
|
||
@State private var showingExerciseAddSheet: Bool = false
|
||
@State private var showingSplitEditSheet: Bool = false
|
||
@State private var itemToEdit: Exercise? = nil
|
||
@State private var itemToDelete: Exercise? = nil
|
||
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section(header: Text("What is a Split?")) {
|
||
Text("A \"split\" is simply how you divide (or \"split up\") your weekly training across different days. Instead of working every muscle group every session, you assign certain muscle groups, movement patterns, or training emphases to specific days.")
|
||
.font(.caption)
|
||
}
|
||
|
||
Section(header: Text("Exercises")) {
|
||
let sortedExercises = split.exercisesArray
|
||
|
||
if !sortedExercises.isEmpty {
|
||
ForEach(sortedExercises) { item in
|
||
ListItem(
|
||
title: item.name,
|
||
subtitle: "\(item.sets) × \(item.reps) × \(weightUnit.format(item.weight))"
|
||
)
|
||
.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 {
|
||
showingExerciseAddSheet = true
|
||
} label: {
|
||
ListItem(systemName: "plus.circle", title: "Add Exercise")
|
||
}
|
||
} else {
|
||
Text("No exercises added yet.")
|
||
Button(action: { showingExerciseAddSheet.toggle() }) {
|
||
ListItem(title: "Add Exercise")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle(split.name)
|
||
.toolbar {
|
||
ToolbarItem(placement: .primaryAction) {
|
||
Button {
|
||
showingSplitEditSheet = true
|
||
} label: {
|
||
Image(systemName: "pencil")
|
||
}
|
||
}
|
||
}
|
||
.sheet(isPresented: $showingExerciseAddSheet) {
|
||
ExercisePickerView(onExerciseSelected: { exerciseNames in
|
||
addExercises(names: exerciseNames)
|
||
}, allowMultiSelect: true)
|
||
}
|
||
.sheet(isPresented: $showingSplitEditSheet) {
|
||
SplitAddEditView(split: split) {
|
||
dismiss()
|
||
}
|
||
}
|
||
.sheet(item: $itemToEdit) { item in
|
||
ExerciseAddEditView(exercise: item, split: split)
|
||
}
|
||
.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 split?")
|
||
}
|
||
// Editing this split (or any of its exercises, all reached from here) parks any
|
||
// active watch run sourced from it — matched by splitID — so the watch can't keep
|
||
// performing an exercise whose plan we're reconfiguring.
|
||
.onAppear { services.watchBridge.setEditingSplit(split.id) }
|
||
.onDisappear { services.watchBridge.setEditingSplit(nil) }
|
||
}
|
||
|
||
private func moveExercises(from source: IndexSet, to destination: Int) {
|
||
var exercises = split.exercisesArray
|
||
exercises.move(fromOffsets: source, toOffset: destination)
|
||
var doc = SplitDocument(from: split)
|
||
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(split: doc) }
|
||
}
|
||
|
||
private func addExercises(names: [String]) {
|
||
var doc = SplitDocument(from: split)
|
||
let existingNames = Set(doc.exercises.map { $0.name })
|
||
let base = doc.exercises.count
|
||
let newDocs = names
|
||
.filter { !existingNames.contains($0) }
|
||
.enumerated()
|
||
.map { i, exName in
|
||
ExerciseDocument(
|
||
id: ULID.make(), name: exName, order: base + i,
|
||
sets: 3, reps: 10, weight: 40,
|
||
loadType: LoadType.weight.rawValue,
|
||
durationSeconds: 0, weightLastUpdated: nil, weightReminderWeeks: 2
|
||
)
|
||
}
|
||
doc.exercises.append(contentsOf: newDocs)
|
||
doc.updatedAt = Date()
|
||
Task { await sync.save(split: doc) }
|
||
|
||
// If a single exercise was added, open the edit sheet once the cache refreshes.
|
||
// We rely on the observer to populate it — no direct entity reference needed.
|
||
}
|
||
|
||
private func deleteExercise(_ exercise: Exercise) {
|
||
var doc = SplitDocument(from: split)
|
||
doc.exercises.removeAll { $0.id == exercise.id }
|
||
// Re-number orders after removal
|
||
for i in doc.exercises.indices {
|
||
doc.exercises[i].order = i
|
||
}
|
||
doc.updatedAt = Date()
|
||
Task { await sync.save(split: doc) }
|
||
}
|
||
}
|