Add App Store screenshot harness + listing metadata

DEBUG-only screenshot support (seeded sample data via ScreenshotSeed, per-screen
launch args, screenshot roots for both apps) so iPhone + Apple Watch App Store
shots can be captured deterministically from the simulator — all excluded from
release builds. Also seed ExerciseView's set-grid progress in init so it renders
correctly on the first frame. Adds Scripts/metadata/ as the listing source of
truth (copy, URLs, review notes, and the captured screenshots).

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
This commit is contained in:
2026-06-19 19:23:54 -04:00
parent 1d856b98d0
commit 84d45a6d41
26 changed files with 313 additions and 16 deletions
@@ -35,17 +35,22 @@ struct ExerciseProgressView: View {
@State private var showingCancelConfirm = false
@State private var didRestorePage = false
init(doc: Binding<WorkoutDocument>, logID: String, onChange: @escaping () -> Void) {
/// Forces the starting page (used only by the DEBUG screenshot host to land on a
/// rest page). Always nil in normal use.
private let debugInitialPage: Int?
init(doc: Binding<WorkoutDocument>, logID: String, onChange: @escaping () -> Void, debugInitialPage: Int? = nil) {
self._doc = doc
self.logID = logID
self.onChange = onChange
self.debugInitialPage = debugInitialPage
let log = doc.wrappedValue.logs.first { $0.id == logID }
let sets = max(1, log?.sets ?? 1)
_setCount = State(initialValue: sets)
// Resume on the first unfinished set's work page (clamped to the last set).
let completed = min(max(0, log?.currentStateIndex ?? 0), sets - 1)
_currentPage = State(initialValue: completed * 2)
_currentPage = State(initialValue: debugInitialPage ?? (completed * 2))
}
private var log: WorkoutLogDocument? {
@@ -96,11 +101,14 @@ struct ExerciseProgressView: View {
}
.onAppear {
// Jump to the first unfinished set. A paged TabView can settle on page 0 on
// first layout, so re-assert once more after this run loop.
// first layout, so re-assert once more after this run loop. (The screenshot
// host pins an explicit page, so skip the resume jump there.)
guard !didRestorePage else { return }
didRestorePage = true
jumpToResumePage()
Task { @MainActor in jumpToResumePage() }
if debugInitialPage == nil {
jumpToResumePage()
Task { @MainActor in jumpToResumePage() }
}
}
}
@@ -15,6 +15,9 @@ final class WatchAppServices {
let container = WorkoutsModelContainer.make()
self.container = container
self.bridge = WatchConnectivityBridge(container: container)
#if DEBUG
if ScreenshotSeed.isActive { ScreenshotSeed.populate(container.mainContext) }
#endif
}
func activate() {
@@ -0,0 +1,60 @@
#if DEBUG
import SwiftUI
import SwiftData
/// DEBUG-only root for App Store screenshot capture on the watch. Renders one
/// fully-formed screen (chosen by `--screen`) against the seeded cache so captures are
/// deterministic. Never compiled into release.
struct WatchScreenshotRoot: View {
let services: WatchAppServices
private var activeWorkout: Workout? {
let context = services.container.mainContext
var descriptor = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.start, order: .reverse)])
descriptor.fetchLimit = 25
let workouts = (try? context.fetch(descriptor)) ?? []
return workouts.first { $0.status == .inProgress } ?? workouts.first
}
var body: some View {
content
.environment(services.bridge)
.modelContainer(services.container)
}
@ViewBuilder
private var content: some View {
if let workout = activeWorkout {
switch ScreenshotSeed.screen(default: "list") {
case "work":
ProgressHost(workout: workout, exerciseName: "Lat Pulldown", page: nil)
case "rest":
ProgressHost(workout: workout, exerciseName: "Lat Pulldown", page: 1)
default:
NavigationStack { WorkoutLogListView(workout: workout) }
}
} else {
Color.black
}
}
}
/// Hosts the progress view with a working-copy binding (and an optional pinned page so
/// we can capture a rest phase, which the normal resume logic never lands on).
private struct ProgressHost: View {
@State private var doc: WorkoutDocument
private let logID: String
private let page: Int?
init(workout: Workout, exerciseName: String, page: Int?) {
let document = WorkoutDocument(from: workout)
_doc = State(initialValue: document)
logID = document.logs.first { $0.exerciseName == exerciseName }?.id ?? document.logs.first?.id ?? ""
self.page = page
}
var body: some View {
ExerciseProgressView(doc: $doc, logID: logID, onChange: {}, debugInitialPage: page)
}
}
#endif
+17 -5
View File
@@ -15,11 +15,23 @@ struct WorkoutsWatchApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(services.bridge)
.environment(appDelegate.sessionManager)
.modelContainer(services.container)
.task { services.activate() }
#if DEBUG
if ScreenshotSeed.isActive {
WatchScreenshotRoot(services: services)
} else {
root
}
#else
root
#endif
}
}
private var root: some View {
ContentView()
.environment(services.bridge)
.environment(appDelegate.sessionManager)
.modelContainer(services.container)
.task { services.activate() }
}
}