Files
workouts/Workouts/Views/Library/StarterGalleryView.swift
T

135 lines
5.1 KiB
Swift

//
// StarterGalleryView.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
/// Every bundled starter routine — including ones the user has deleted — with a
/// per-seed Add/Restore action and a bulk "re-add everything missing" fallback. Unlike
/// `RoutinesLibraryView` (which only ever shows *live* routines), this reads straight
/// from `SeedLibrary.seeds`, the bundle-derived source of truth, so a deleted starter
/// still has a row here. Pushed from that view's "Browse Starter Routines" entry.
struct StarterGalleryView: View {
@Environment(SyncEngine.self) private var sync
@Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
private var routines: [Routine]
@State private var isRestoringAll = false
@State private var restoreAllMessage: String?
var body: some View {
List {
Section {
ForEach(SeedLibrary.seeds, id: \.id) { seed in
row(for: seed)
}
} footer: {
Text("Every routine Workouts ships with, whether or not you've kept it. Adding one never touches a routine you've created or edited.")
}
Section {
Button {
Task {
isRestoringAll = true
restoreAllMessage = nil
let n = await sync.restoreSeeds()
isRestoringAll = false
restoreAllMessage = n == 0
? "All starter routines are already present."
: "Restored \(n) starter routine\(n == 1 ? "" : "s")."
}
} label: {
HStack {
Label("Re-add All Missing Starters", systemImage: "arrow.counterclockwise")
if isRestoringAll {
Spacer()
ProgressView()
}
}
}
.disabled(isRestoringAll || sync.iCloudStatus != .available)
} footer: {
if let restoreAllMessage {
Text(restoreAllMessage)
}
}
}
.listStyle(.insetGrouped)
.navigationTitle("Starter Routines")
.navigationBarTitleDisplayMode(.inline)
}
// MARK: - Row
/// One seed's row: always navigable (to the live detail once added, otherwise the
/// read-only preview), with a state-appropriate trailing accessory — a checkmark
/// once added, an inline Add button while restorable (a `.borderless` button style
/// keeps its tap from also firing the row's `NavigationLink`), or nothing extra
/// when blocked (the preview screen explains why).
@ViewBuilder
private func row(for seed: SeedLibrary.Seed) -> some View {
switch seed.state(among: routines) {
case .added(let routine):
NavigationLink {
RoutineDetailView(routine: routine)
} label: {
HStack {
content(for: seed.doc)
Spacer()
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
}
}
case .nameTaken:
NavigationLink {
StarterPreviewView(seed: seed)
} label: {
content(for: seed.doc, caption: "A routine named \"\(seed.doc.name)\" already exists")
}
case .restorable:
NavigationLink {
StarterPreviewView(seed: seed)
} label: {
HStack {
content(for: seed.doc)
Spacer()
Button("Add") {
Task { _ = await sync.restoreSeed(id: seed.id) }
}
.buttonStyle(.borderless)
.disabled(sync.iCloudStatus != .available)
}
}
}
}
/// The row's icon badge + name + caption — the same visual language as
/// `RoutinesLibraryView`'s `RoutineRow`, but built from the seed's `RoutineDocument`
/// rather than a live `Routine` (a not-yet-added seed has no entity to read).
/// `caption` overrides the default exercise count when the row needs to explain
/// itself instead.
private func content(for doc: RoutineDocument, caption: String? = nil) -> some View {
HStack(spacing: 12) {
Image(systemName: doc.systemImage)
.font(.title3)
.foregroundStyle(Color.color(from: doc.color))
.frame(width: 36, height: 36)
.background(Color.color(from: doc.color).opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 2) {
Text(doc.name)
.foregroundStyle(.primary)
Text(caption ?? "\(doc.exercises.count) exercises")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 2)
}
}