import IndieSync import SwiftUI import SwiftData /// Create or edit a routine's identity: name, icon, color, and activity type. Mac- /// native sheet (grouped Form, fixed width, Cancel/Save). The workout-run settings /// the iOS editor also carries (rest, auto-advance, target heart rate) are omitted — /// the Mac never runs a workout — but any values already set on the routine are /// preserved on save, since we rebuild from `RoutineDocument(from:)` and touch only /// these four fields. struct MacRoutineEditorSheet: View { @Environment(SyncEngine.self) private var syncEngine @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss // Resolve by id, not a captured entity: a clone-on-edit swaps a seed's identity // mid-edit. `routineID` is nil in create mode. Mirrors iOS `RoutineAddEditView`. @State private var routineID: String? @Query private var routines: [Routine] @State private var name: String @State private var color: String @State private var systemImage: String @State private var activityType: WorkoutActivityType var isEditing: Bool { routineID != nil } init(routine: Routine?) { _routineID = State(initialValue: routine?.id) _name = State(initialValue: routine?.name ?? "") _color = State(initialValue: routine?.color ?? "indigo") _systemImage = State(initialValue: routine?.systemImage ?? "dumbbell.fill") _activityType = State(initialValue: routine?.activityTypeEnum ?? .traditionalStrength) } private var routine: Routine? { guard let routineID else { return nil } let id = syncEngine.currentRoutineID(for: routineID) return routines.first { $0.id == id } } var body: some View { NavigationStack { Form { Section("Name") { TextField("Name", text: $name) if let routineID, SeedLibrary.isSeed(id: routineID) { Text("Saving changes to a starter routine creates your own copy; the starter stays available in the gallery.") .font(.caption) .foregroundStyle(.secondary) } } Section("Icon") { IconGridPicker(selection: $systemImage, tint: Color.color(from: color)) } Section("Color") { ColorSwatchPicker(selection: $color) } Section { Picker("Activity Type", selection: $activityType) { ForEach(WorkoutActivityType.allCases, id: \.self) { type in Label(type.displayName, systemImage: type.systemImage).tag(type) } } } header: { Text("Activity Type") } footer: { Text("Determines how this workout is categorized in Apple Health and how it credits your Activity rings.") .font(.caption) .foregroundStyle(.secondary) } } .formStyle(.grouped) .navigationTitle(isEditing ? "Edit Routine" : "New Routine") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { save(); dismiss() } .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } } } .frame(width: 460, height: 520) } private func save() { if isEditing { guard let routine else { return } var doc = RoutineDocument(from: routine) doc.name = name doc.color = color doc.systemImage = systemImage doc.activityType = activityType.rawValue doc.updatedAt = Date() Task { await syncEngine.save(routine: doc) } } else { let existing = (try? modelContext.fetch(FetchDescriptor())) ?? [] let now = Date() let doc = RoutineDocument( schemaVersion: RoutineDocument.currentSchemaVersion, id: ULID.make(), name: name, color: color, systemImage: systemImage, order: existing.count, createdAt: now, updatedAt: now, exercises: [], activityType: activityType.rawValue ) Task { await syncEngine.save(routine: doc) } } } } /// A grid of the curated `availableIcons`, single-select, with the chosen one ringed. private struct IconGridPicker: View { @Binding var selection: String let tint: Color private let columns = [GridItem(.adaptive(minimum: 44), spacing: 8)] var body: some View { LazyVGrid(columns: columns, spacing: 8) { ForEach(availableIcons, id: \.self) { icon in Button { selection = icon } label: { Image(systemName: icon) .font(.title3) .foregroundStyle(icon == selection ? tint : .secondary) .frame(width: 40, height: 40) .background( (icon == selection ? tint.opacity(0.15) : Color.secondary.opacity(0.08)), in: RoundedRectangle(cornerRadius: 8) ) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(tint, lineWidth: icon == selection ? 2 : 0) ) } .buttonStyle(.plain) } } .padding(.vertical, 4) } } /// A row of the canonical `availableColors` as tappable swatches, single-select. private struct ColorSwatchPicker: View { @Binding var selection: String private let columns = [GridItem(.adaptive(minimum: 36), spacing: 10)] var body: some View { LazyVGrid(columns: columns, spacing: 10) { ForEach(availableColors, id: \.self) { name in Button { selection = name } label: { Circle() .fill(Color.color(from: name)) .frame(width: 28, height: 28) .overlay( Circle() .strokeBorder(.primary, lineWidth: name == selection ? 2 : 0) .padding(-2) ) } .buttonStyle(.plain) .help(name.capitalized) } } .padding(.vertical, 4) } }