Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2487deb
UI overhaul: bottom tab nav, calendar home, workout tracking, gold de…
TWhit229 Feb 23, 2026
1766f8d
Merge main into travis-winter-2026, resolve CoachView conflict
TWhit229 Feb 23, 2026
492d7b1
Update STYLE_GUIDE.md v2.0: match actual SwiftUI implementation
TWhit229 Feb 23, 2026
0bf0249
Fix E402 lint: add noqa for imports after sys.path manipulation
TWhit229 Feb 23, 2026
3836728
Fix F401: remove unused numpy import in run_batch.py
TWhit229 Feb 23, 2026
82daf13
Fix tab bar floating + mAI Coach button not working
TWhit229 Feb 23, 2026
60f1c10
Fix demo video: revert to original file, apply videoComposition to pl…
TWhit229 Feb 23, 2026
8aab89f
Fix floating tab bar: use configureWithOpaqueBackground for flat edge…
TWhit229 Feb 23, 2026
28a9ab6
Fix template button taps + direct UIKit tab bar override
TWhit229 Feb 23, 2026
1964440
Fix demo freeze + tab bar floating
TWhit229 Feb 23, 2026
60e6000
Custom flat tab bar + fix demo video player
TWhit229 Feb 23, 2026
bd59831
Fix contact email: twhit229@mtroyal.ca → whitnetr@oregonstate.edu
TWhit229 Feb 24, 2026
b0d7197
Add exercise picker with equipment filter + local persistence
TWhit229 Feb 24, 2026
786c2b1
Fix demo video white screen on first load
TWhit229 Feb 24, 2026
24c4ab2
Fix demo video: wait for readyToPlay before calling play()
TWhit229 Feb 27, 2026
8b21a75
feat: light/dark mode toggle, golden app icon, boot screen & session …
TWhit229 Feb 27, 2026
2fd61c1
feat: expand equipment types to 22 with custom icons + dual filter ex…
TWhit229 Feb 28, 2026
8bea6e8
fix: update TemplateExercisePickerView with dual filter (muscle group…
TWhit229 Feb 28, 2026
0f629e6
refactor: consolidate exercise pickers into single ExercisePickerView
TWhit229 Feb 28, 2026
4610eb1
feat: consolidate muscle groups to 5 + Other with custom icons
TWhit229 Feb 28, 2026
f39e1ed
feat: Superset migration, workout notes, timer fixes, UI polish
TWhit229 Mar 1, 2026
bbfa6d5
docs: Update all documentation to reflect current features
TWhit229 Mar 1, 2026
51b4006
feat: Add interactive onboarding tutorial (28 steps, 4 phases)
TWhit229 Mar 1, 2026
737e4dd
fix: Fix tutorial overlay rendering — compositingGroup inside mask
TWhit229 Mar 1, 2026
be990f7
feat: Enhanced tutorial — demo workout, real UI spotlights, tighter flow
TWhit229 Mar 1, 2026
5f66621
feat: Active workout persists in Workout tab across navigation
TWhit229 Mar 1, 2026
fabd809
fix: Tutorial spotlight alignment and bubble never goes off-screen
TWhit229 Mar 1, 2026
ee92c8d
fix: Replace anchorPreference with frame-reporting for reliable spotl…
TWhit229 Mar 1, 2026
609ee90
feat: Auto-scroll to spotlighted elements during tutorial
TWhit229 Mar 1, 2026
0470d6f
fix: Tutorial auto-scrolls to spotlight elements, reordered steps
TWhit229 Mar 1, 2026
7b1bd9e
fix: Auto-scroll on all three tabs, Navigation scrolls back to top
TWhit229 Mar 1, 2026
4afd920
fix: Settings spotlight on Audio section, Navigation step uses no spo…
TWhit229 Mar 1, 2026
a574b47
fix: Add @MainActor to DataBackupManager for CI concurrency check
TWhit229 Mar 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ __pycache__/

Pods/

# Scratch / archive
temp/
archive/
.gemini/

# PyInstaller build artifacts
Dev_tools/build/
Dev_tools/dist/
Expand Down
47 changes: 40 additions & 7 deletions App Core/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mAI_Coach iOS App

This folder contains the source code for the mAI_Coach iOS application, which provides real-time motion capture and feedback for strength training using on-device MediaPipe inference.
This folder contains the source code for the mAI_Coach iOS application, which provides real-time motion capture and feedback for strength training using on-device MediaPipe inference, plus a full-featured workout tracker with superset support.

## Prerequisites

Expand Down Expand Up @@ -39,19 +39,52 @@ open "mAICoach.xcworkspace"

## Architecture

### App Entry & Navigation

| File | Purpose |
| :--- | :--- |
| `mAICoachApp.swift` | App entry point. |
| `RootView.swift` | Navigation root, manages auth state. |
| `HomeView.swift` | Main menu with "Coach me!" and "My Workouts" buttons. |
| `CoachView.swift` | Exercise selection (Live / Demo modes). |
| `mAICoachApp.swift` | App entry point. Injects `AuthSession` and `WorkoutStore`. |
| `RootView.swift` | Navigation root, manages auth state, loads admin demo data. |
| `MainTabView.swift` | 3-tab layout: Home, Workout, Profile with frosted glass tab bar. |
| `AuthSession.swift` | Authentication manager, admin account support. |

### Views

| File | Purpose |
| :--- | :--- |
| `HomeView.swift` | Calendar with workout dots, day cards, template shortcuts. |
| `WorkoutView.swift` | Active workout tab with live session tracking. |
| `WorkoutDetailView.swift` | Full workout/template detail: exercise cards, supersets, edit mode, fill-from-history, workout notes. |
| `WorkoutBuilderView.swift` | Create new workouts with name, exercises, save-as-template. |
| `AllTemplatesView.swift` | Template management: favorites, archive, reorder. |
| `ExerciseCardView.swift` | Exercise card with sets grid (weight/reps/RPE), checkmarks, notes, mAI Coach button. |
| `SupersetBlockView.swift` | Superset group block with label, notes, inner exercise cards, add/move/delete. |
| `ExercisePickerView.swift` | Searchable exercise catalog picker grouped by muscle group. |
| `ExerciseStatsView.swift` | Per-exercise historical stats: best sets, volume trend, session count. |
| `PersonalRecordsView.swift` | All-time PR board across all exercises. |
| `ProfileView.swift` | Account card, settings, stats link. |
| `SettingsView.swift` | Coach voice, appearance, template shortcuts, PR badges, dev data toggles. |
| `ConfettiView.swift` | On-screen confetti animation for PR celebrations. |
| `BenchSessionView.swift` | Camera feed with pose overlay, HUD (rep count, form feedback), and centering guide. |

### Models & Logic

| File | Purpose |
| :--- | :--- |
| `WorkoutModel.swift` | `Workout`, `WorkoutItem`, `SupersetGroup`, `Exercise`, `ExerciseSet` types + `WorkoutStore` with CRUD, templates, PR detection, 16-week admin demo data. |
| `WorkoutStatsEngine.swift` | Stats computation: totals, PRs, exercise trends, streaks. |
| `RestTimerManager.swift` | Observable rest timer with auto-start on set completion. |
| `ExerciseCatalog.swift` | Equipment types enum, exercise catalog loader. |
| `exercise_library.json` | 100+ exercises with name, equipment, and muscle group. |

### AI Coaching

| File | Purpose |
| :--- | :--- |
| `BenchInferenceEngine.swift` | MLP inference, rep detection (wrist-Y peak), and audio coaching triggers. |
| `PoseLandmarkerService.swift` | MediaPipe pose detection, landmark smoothing, and skeleton constraints. |
| `AudioCoach.swift` | Audio feedback system with tag-based coaching clips. |
| `DemoPlayerPipeline.swift` | Pre-recorded demo video playback with frame-by-frame pose processing. |
| `SettingsView.swift` | User preferences (coach voice selection, dev data toggle). |
| `MyWorkoutsView.swift` | Workout tracking placeholder (coming soon). |

### Model

Expand Down
270 changes: 270 additions & 0 deletions App Core/Resources/Swift Code/AllTemplatesView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// AllTemplatesView.swift
// mAI Coach — Full template list with favorites management, delete/archive support.

import SwiftUI

struct AllTemplatesView: View {
@EnvironmentObject var store: WorkoutStore
@State private var editMode: EditMode = .inactive
@State private var showMaxAlert = false
@State private var showCreator = false
@State private var templateToDelete: String?
@State private var showDeleteDialog = false
@State private var searchText = ""
@State private var selectedMuscleGroups: Set<String> = []
@AppStorage(SettingsKeys.maxTemplateShortcuts) private var maxShortcuts = 4

private var isEditing: Bool { editMode == .active }

private static let muscleGroupIcons: [String: String] = [
"Chest": "muscle_chest",
"Back": "muscle_back",
"Shoulders": "muscle_shoulders",
"Arms": "muscle_arms",
"Legs": "muscle_legs",
"Core": "muscle_core",
"Other": "muscle_other",
]

/// All template names that match the current search text and muscle group filter.
private var filteredTemplateNames: [String] {
store.allTemplateNames.filter { name in
let matchesSearch = searchText.isEmpty || name.localizedCaseInsensitiveContains(searchText)
guard matchesSearch else { return false }
guard !selectedMuscleGroups.isEmpty else { return true }
// Template matches only if it contains ALL selected muscle groups
if let tpl = store.templateWorkout(named: name) {
return selectedMuscleGroups.isSubset(of: Set(tpl.tags))
}
return false
}
}

var body: some View {
VStack(spacing: 0) {
// Search bar
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.foregroundStyle(AppColors.textTertiary)
TextField("Search templates", text: $searchText)
.font(AppFonts.body())
.foregroundStyle(AppColors.textPrimary)
if !searchText.isEmpty {
Button { searchText = "" } label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(AppColors.textTertiary)
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(AppColors.bgSecondary)
.cornerRadius(10)
.padding(.horizontal, 16)
.padding(.top, 8)

// Muscle group filter pills
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedMuscleGroups.isEmpty) {
selectedMuscleGroups.removeAll()
}

ForEach(ExerciseCatalog.muscleGroups, id: \.self) { group in
let iconName = Self.muscleGroupIcons[group] ?? "muscle_other"
filterPill(label: group, icon: iconName, isSystemImage: false, isSelected: selectedMuscleGroups.contains(group)) {
if selectedMuscleGroups.contains(group) {
selectedMuscleGroups.remove(group)
} else {
selectedMuscleGroups.insert(group)
}
}
}
}
.padding(.horizontal, 16)
}
.padding(.vertical, 8)

List {
// MARK: Favorites Section
let filteredFavs = store.allTemplatesGrouped.favorites.filter { filteredTemplateNames.contains($0.name) }
if !filteredFavs.isEmpty {
Section {
ForEach(filteredFavs, id: \.name) { tpl in
templateRow(name: tpl.name, count: tpl.count, isFav: true)
}
.onMove { from, to in
store.moveFavorite(from: from, to: to)
}
} header: {
Text("Favorites")
}
}

// MARK: All Templates Section (filtered)
Section {
let grouped = Dictionary(grouping: store.workouts) { $0.name }
let allSorted = filteredTemplateNames.compactMap { name -> (name: String, count: Int)? in
guard let latest = grouped[name]?.last else { return nil }
return (name: name, count: latest.exercises.count)
}
ForEach(allSorted, id: \.name) { tpl in
templateRow(name: tpl.name, count: tpl.count, isFav: store.isFavorite(tpl.name))
}
} header: {
Text("All Templates")
}

// MARK: Add Template
Section {
Button { showCreator = true } label: {
HStack {
Image(systemName: "plus.circle.fill")
.font(.system(size: 20))
.foregroundStyle(AppColors.accentGold)
Text("Add Template")
.font(AppFonts.bodyBold())
.foregroundStyle(AppColors.accentText)
}
}
}
}
.listStyle(.insetGrouped)
}
.navigationTitle("Templates")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
HStack(spacing: 12) {
if isEditing {
NavigationLink(destination: ArchivedTemplatesView()) {
Image(systemName: "archivebox")
.font(.system(size: 16))
.foregroundStyle(AppColors.accentGold)
}
}
EditButton()
}
}
}
.environment(\.editMode, $editMode)
.alert("Favorites Limit Reached", isPresented: $showMaxAlert) {
Button("OK", role: .cancel) { }
} message: {
Text("You've reached your maximum of \(maxShortcuts) favorites. Remove one to add another.")
}
.confirmationDialog(
"Template Options",
isPresented: $showDeleteDialog,
titleVisibility: .visible
) {
Button("Archive", role: .destructive) {
if let name = templateToDelete {
store.archiveTemplate(name: name)
}
}
Button("Delete Permanently", role: .destructive) {
if let name = templateToDelete {
store.deleteTemplate(name: name)
}
}
Button("Cancel", role: .cancel) {
templateToDelete = nil
}
} message: {
if let name = templateToDelete {
Text("What would you like to do with \"\(name)\"?\n\nArchive keeps it recoverable. Delete removes it permanently.")
}
}
.sheet(isPresented: $showCreator) {
TemplateCreatorView()
}
}

// MARK: - Filter Pill
private func filterPill(label: String, icon: String, isSystemImage: Bool, isSelected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 4) {
if isSystemImage {
Image(systemName: icon)
.font(.system(size: 12))
} else {
Image(icon)
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 14, height: 14)
}
Text(label)
.font(AppFonts.footnote())
.fontWeight(.medium)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(isSelected ? AppColors.accentGold : AppColors.bgSecondary)
.foregroundStyle(isSelected ? .black : AppColors.textSecondary)
.cornerRadius(16)
}
.buttonStyle(.plain)
}

// MARK: - Template Row
@ViewBuilder
private func templateRow(name: String, count: Int, isFav: Bool) -> some View {
HStack(spacing: 12) {
// Red minus delete button in edit mode
if isEditing {
Button {
templateToDelete = name
showDeleteDialog = true
} label: {
Image(systemName: "minus.circle.fill")
.font(.system(size: 22))
.foregroundStyle(.red)
}
.buttonStyle(.plain)
}

// Nav link wrapping the content
NavigationLink(destination: TemplateDetailView(templateName: name)) {
HStack(spacing: 12) {
// Star toggle
Button {
if isFav {
_ = store.toggleFavorite(name)
} else {
if !store.toggleFavorite(name) {
showMaxAlert = true
}
}
} label: {
Image(systemName: isFav ? "star.fill" : "star")
.font(.system(size: 18))
.foregroundStyle(isFav ? AppColors.accentGold : AppColors.textTertiary)
}
.buttonStyle(.plain)

// Template icon
RoundedRectangle(cornerRadius: 10)
.fill(AppColors.accentMuted)
.frame(width: 36, height: 36)
.overlay(
Image(systemName: "figure.strengthtraining.traditional")
.font(.system(size: 16))
.foregroundStyle(AppColors.accentGold)
)

// Name + count
VStack(alignment: .leading, spacing: 2) {
Text(name)
.font(AppFonts.bodyBold())
.foregroundStyle(AppColors.textPrimary)
Text("\(count) exercises")
.font(AppFonts.footnote())
.foregroundStyle(AppColors.textSecondary)
}
}
}
}
.padding(.vertical, 4)
}
}
Loading