Some refactors
This commit is contained in:
@@ -6,7 +6,9 @@
|
||||
"Bash(dart run build_runner:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(dir /s /b \"C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\presentation\\\\*.dart\")",
|
||||
"Bash(flutter build:*)"
|
||||
"Bash(flutter build:*)",
|
||||
"Bash(test:*)",
|
||||
"Bash(powershell -Command \"Remove-Item ''C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\theme.dart'' -Force -ErrorAction SilentlyContinue; Remove-Item ''C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\models'' -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item ''C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\database'' -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item ''C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\providers'' -Recurse -Force -ErrorAction SilentlyContinue; Write-Output ''Done''\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'dart:io';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
// Exercises
|
||||
class Exercises extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get instructions => text().nullable()();
|
||||
TextColumn get enrichment => text().nullable()();
|
||||
TextColumn get tags => text().nullable()(); // JSON string
|
||||
TextColumn get videoUrl => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// TrainingPlans
|
||||
class TrainingPlans extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get sections => text().nullable()(); // JSON string
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// Programs
|
||||
class Programs extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get createdAt => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// ProgramWeeks
|
||||
class ProgramWeeks extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get programId =>
|
||||
text().references(Programs, #id, onDelete: KeyAction.cascade)();
|
||||
IntColumn get position => integer()();
|
||||
TextColumn get notes => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// ProgramWorkouts
|
||||
class ProgramWorkouts extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get weekId =>
|
||||
text().references(ProgramWeeks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get programId =>
|
||||
text().references(Programs, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get day => text()();
|
||||
TextColumn get type => text()(); // "exercise" | "plan"
|
||||
TextColumn get refId => text().nullable()();
|
||||
TextColumn get name => text().nullable()();
|
||||
TextColumn get description => text().nullable()();
|
||||
BoolColumn get completed => boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// AnalysisSessions
|
||||
class AnalysisSessions extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get date => text()();
|
||||
TextColumn get videoPath => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// Annotations
|
||||
class Annotations extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get sessionId =>
|
||||
text().references(AnalysisSessions, #id, onDelete: KeyAction.cascade)();
|
||||
RealColumn get startTime => real()();
|
||||
RealColumn get endTime => real()();
|
||||
TextColumn get name => text().nullable()();
|
||||
TextColumn get description => text().nullable()();
|
||||
TextColumn get color => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// ChatSessions
|
||||
class ChatSessions extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get title => text().nullable()();
|
||||
TextColumn get createdAt => text()();
|
||||
TextColumn get updatedAt => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
// ChatMessages
|
||||
class ChatMessages extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get sessionId =>
|
||||
text().references(ChatSessions, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get role => text()(); // 'user' | 'assistant'
|
||||
TextColumn get content => text()();
|
||||
TextColumn get createdAt => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Exercises,
|
||||
TrainingPlans,
|
||||
Programs,
|
||||
ProgramWeeks,
|
||||
ProgramWorkouts,
|
||||
AnalysisSessions,
|
||||
Annotations,
|
||||
ChatSessions,
|
||||
ChatMessages,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'trainhub.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,107 +0,0 @@
|
||||
class TrainingPlanModel {
|
||||
String id;
|
||||
String name;
|
||||
List<TrainingSectionModel> sections;
|
||||
|
||||
TrainingPlanModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sections,
|
||||
});
|
||||
|
||||
factory TrainingPlanModel.fromJson(Map<String, dynamic> json) {
|
||||
return TrainingPlanModel(
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
sections:
|
||||
(json['sections'] as List<dynamic>?)
|
||||
?.map((e) => TrainingSectionModel.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'sections': sections.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingSectionModel {
|
||||
String id;
|
||||
String name;
|
||||
List<TrainingExerciseModel> exercises;
|
||||
|
||||
TrainingSectionModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.exercises,
|
||||
});
|
||||
|
||||
factory TrainingSectionModel.fromJson(Map<String, dynamic> json) {
|
||||
return TrainingSectionModel(
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
exercises:
|
||||
(json['exercises'] as List<dynamic>?)
|
||||
?.map((e) => TrainingExerciseModel.fromJson(e))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'exercises': exercises.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingExerciseModel {
|
||||
String instanceId;
|
||||
String exerciseId;
|
||||
String name;
|
||||
int sets;
|
||||
int value; // Reps or Seconds
|
||||
bool isTime;
|
||||
int rest;
|
||||
|
||||
TrainingExerciseModel({
|
||||
required this.instanceId,
|
||||
required this.exerciseId,
|
||||
required this.name,
|
||||
required this.sets,
|
||||
required this.value,
|
||||
required this.isTime,
|
||||
required this.rest,
|
||||
});
|
||||
|
||||
factory TrainingExerciseModel.fromJson(Map<String, dynamic> json) {
|
||||
return TrainingExerciseModel(
|
||||
instanceId: json['instanceId'] ?? '',
|
||||
exerciseId: json['exerciseId'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
sets: json['sets'] ?? 3,
|
||||
value: json['value'] ?? 10,
|
||||
isTime: json['isTime'] ?? false,
|
||||
rest: json['rest'] ?? 60,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'instanceId': instanceId,
|
||||
'exerciseId': exerciseId,
|
||||
'name': name,
|
||||
'sets': sets,
|
||||
'value': value,
|
||||
'isTime': isTime,
|
||||
'rest': rest,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ part 'analysis_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class AnalysisController extends _$AnalysisController {
|
||||
late final AnalysisRepository _repo;
|
||||
late AnalysisRepository _repo;
|
||||
|
||||
@override
|
||||
Future<AnalysisState> build() async {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
|
||||
|
||||
class AnalysisSessionList extends StatelessWidget {
|
||||
@@ -15,26 +17,148 @@ class AnalysisSessionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sessions.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No analysis sessions yet. Tap + to create one.'),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(UIConstants.spacing24),
|
||||
itemCount: sessions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final session = sessions[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(child: Icon(Icons.video_library)),
|
||||
title: Text(session.name),
|
||||
subtitle: Text(session.date),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () => onDeleteSession(session),
|
||||
),
|
||||
return _SessionCard(
|
||||
session: session,
|
||||
onTap: () => onSessionSelected(session),
|
||||
onDelete: () => onDeleteSession(session),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SessionCard extends StatefulWidget {
|
||||
final AnalysisSessionEntity session;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _SessionCard({
|
||||
required this.session,
|
||||
required this.onTap,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SessionCard> createState() => _SessionCardState();
|
||||
}
|
||||
|
||||
class _SessionCardState extends State<_SessionCard> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
margin: const EdgeInsets.only(bottom: UIConstants.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered ? AppColors.surfaceContainerHigh : AppColors.surfaceContainer,
|
||||
borderRadius: UIConstants.cardBorderRadius,
|
||||
border: Border.all(
|
||||
color: _isHovered ? AppColors.zinc600 : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: UIConstants.cardBorderRadius,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UIConstants.cardPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.purpleMuted,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.play_circle_outline,
|
||||
color: AppColors.purple,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.session.name,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
size: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.session.date,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (widget.session.videoPath != null) ...[
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
const Icon(
|
||||
Icons.videocam_outlined,
|
||||
size: 12,
|
||||
color: AppColors.info,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Video',
|
||||
style: TextStyle(
|
||||
color: AppColors.info,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isHovered)
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 18,
|
||||
color: AppColors.destructive,
|
||||
),
|
||||
onPressed: widget.onDelete,
|
||||
tooltip: 'Delete',
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ part 'calendar_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CalendarController extends _$CalendarController {
|
||||
late final ProgramRepository _programRepo;
|
||||
late final TrainingPlanRepository _planRepo;
|
||||
late final ExerciseRepository _exerciseRepo;
|
||||
late ProgramRepository _programRepo;
|
||||
late TrainingPlanRepository _planRepo;
|
||||
late ExerciseRepository _exerciseRepo;
|
||||
|
||||
@override
|
||||
Future<CalendarState> build() async {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/presentation/calendar/calendar_controller.dart';
|
||||
import 'package:trainhub_flutter/presentation/calendar/widgets/program_selector.dart';
|
||||
import 'package:trainhub_flutter/presentation/calendar/widgets/program_week_view.dart';
|
||||
import 'package:trainhub_flutter/presentation/common/widgets/app_empty_state.dart';
|
||||
import 'package:trainhub_flutter/presentation/common/dialogs/text_input_dialog.dart';
|
||||
import 'package:trainhub_flutter/presentation/common/dialogs/confirm_dialog.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CalendarPage extends ConsumerWidget {
|
||||
@@ -14,16 +19,15 @@ class CalendarPage extends ConsumerWidget {
|
||||
final state = ref.watch(calendarControllerProvider);
|
||||
final controller = ref.read(calendarControllerProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Program & Schedule')),
|
||||
body: state.when(
|
||||
return state.when(
|
||||
data: (data) {
|
||||
if (data.programs.isEmpty) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _showCreateProgramDialog(context, controller),
|
||||
child: const Text('Create First Program'),
|
||||
),
|
||||
return AppEmptyState(
|
||||
icon: Icons.calendar_month_outlined,
|
||||
title: 'No programs yet',
|
||||
subtitle: 'Create your first training program to start scheduling workouts',
|
||||
actionLabel: 'Create Program',
|
||||
onAction: () => _showCreateProgramDialog(context, controller),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,24 +39,48 @@ class CalendarPage extends ConsumerWidget {
|
||||
onProgramSelected: (p) => controller.loadProgram(p.id),
|
||||
onCreateProgram: () =>
|
||||
_showCreateProgramDialog(context, controller),
|
||||
onDuplicateProgram: () {
|
||||
if (data.activeProgram != null) {
|
||||
controller.duplicateProgram(data.activeProgram!.id);
|
||||
}
|
||||
},
|
||||
onDeleteProgram: () async {
|
||||
if (data.activeProgram != null) {
|
||||
final confirmed = await ConfirmDialog.show(
|
||||
context,
|
||||
title: 'Delete Program?',
|
||||
message:
|
||||
'Are you sure you want to delete "${data.activeProgram!.name}"? This cannot be undone.',
|
||||
);
|
||||
if (confirmed == true) {
|
||||
controller.deleteProgram(data.activeProgram!.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: data.activeProgram == null
|
||||
? const Center(child: Text("Select a program"))
|
||||
? const Center(
|
||||
child: Text(
|
||||
'Select a program to view its schedule',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UIConstants.spacing24),
|
||||
itemCount: data.weeks.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == data.weeks.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 24.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: UIConstants.spacing24),
|
||||
child: Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: controller.addWeek,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Add Week"),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('Add Week'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -65,9 +93,8 @@ class CalendarPage extends ConsumerWidget {
|
||||
week: week,
|
||||
workouts: weekWorkouts,
|
||||
availablePlans: data.plans,
|
||||
onAddWorkout: (workout) => controller.addWorkout(
|
||||
workout,
|
||||
), // logic needs refined params
|
||||
onAddWorkout: (workout) =>
|
||||
controller.addWorkout(workout),
|
||||
onDeleteWorkout: controller.deleteWorkout,
|
||||
);
|
||||
},
|
||||
@@ -76,41 +103,27 @@ class CalendarPage extends ConsumerWidget {
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (e, s) => Center(child: Text('Error: $e')),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, s) => Center(
|
||||
child: Text(
|
||||
'Error: $e',
|
||||
style: const TextStyle(color: AppColors.destructive),
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateProgramDialog(
|
||||
Future<void> _showCreateProgramDialog(
|
||||
BuildContext context,
|
||||
CalendarController controller,
|
||||
) {
|
||||
final textController = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('New Program'),
|
||||
content: TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(labelText: 'Program Name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (textController.text.isNotEmpty) {
|
||||
controller.createProgram(textController.text);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Create'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) async {
|
||||
final name = await TextInputDialog.show(
|
||||
context,
|
||||
title: 'New Program',
|
||||
hintText: 'e.g. 12 Week Strength',
|
||||
);
|
||||
if (name != null && name.isNotEmpty) {
|
||||
controller.createProgram(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ part 'chat_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatController extends _$ChatController {
|
||||
late final ChatRepository _repo;
|
||||
late ChatRepository _repo;
|
||||
|
||||
@override
|
||||
Future<ChatState> build() async {
|
||||
|
||||
@@ -13,7 +13,7 @@ part 'plan_editor_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class PlanEditorController extends _$PlanEditorController {
|
||||
late final TrainingPlanRepository _planRepo;
|
||||
late TrainingPlanRepository _planRepo;
|
||||
|
||||
@override
|
||||
Future<PlanEditorState> build(String planId) async {
|
||||
|
||||
@@ -10,8 +10,8 @@ part 'trainings_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class TrainingsController extends _$TrainingsController {
|
||||
late final TrainingPlanRepository _planRepo;
|
||||
late final ExerciseRepository _exerciseRepo;
|
||||
late TrainingPlanRepository _planRepo;
|
||||
late ExerciseRepository _exerciseRepo;
|
||||
|
||||
@override
|
||||
Future<TrainingsState> build() async {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
|
||||
|
||||
class ActivityCard extends StatelessWidget {
|
||||
@@ -8,65 +10,127 @@ class ActivityCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isRest = activity.type == 'rest';
|
||||
final isRest = activity.isRest;
|
||||
final accentColor = isRest ? AppColors.info : AppColors.accent;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer.withValues(alpha: 0.6),
|
||||
borderRadius: UIConstants.cardBorderRadius,
|
||||
border: Border.all(
|
||||
color: accentColor.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Activity type badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
isRest ? 'REST' : 'WORK',
|
||||
style: TextStyle(
|
||||
color: accentColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
// Activity name
|
||||
Text(
|
||||
activity.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!isRest) ...[
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
"${activity.sectionName} • Set ${activity.setIndex}/${activity.totalSets}",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
'${activity.sectionName ?? ''} \u00B7 Set ${activity.setIndex}/${activity.totalSets}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (activity.originalExercise != null)
|
||||
if (activity.originalExercise != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildInfo(
|
||||
context,
|
||||
"Sets",
|
||||
"${activity.originalExercise!.sets}",
|
||||
_InfoChip(
|
||||
label: 'Sets',
|
||||
value: '${activity.originalExercise!.sets}',
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_buildInfo(
|
||||
context,
|
||||
activity.originalExercise!.isTime ? "Secs" : "Reps",
|
||||
"${activity.originalExercise!.value}",
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
_InfoChip(
|
||||
label: activity.originalExercise!.isTime ? 'Secs' : 'Reps',
|
||||
value: '${activity.originalExercise!.value}',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
] else
|
||||
Text(
|
||||
"Resting...",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UIConstants.spacing8),
|
||||
child: Text(
|
||||
'Take a break',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoChip extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoChip({required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc800.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfo(BuildContext context, String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(value, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
|
||||
class SessionControls extends StatelessWidget {
|
||||
final bool isRunning;
|
||||
@@ -20,35 +22,109 @@ class SessionControls extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFinished) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Finish Workout'),
|
||||
);
|
||||
}
|
||||
if (isFinished) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: UIConstants.spacing24),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
border: Border.all(color: AppColors.border.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
onPressed: onPrevious,
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
iconSize: 32,
|
||||
_ControlButton(
|
||||
icon: Icons.skip_previous_rounded,
|
||||
onTap: onPrevious,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
IconButton.filled(
|
||||
onPressed: isRunning ? onPause : onPlay,
|
||||
icon: Icon(isRunning ? Icons.pause : Icons.play_arrow),
|
||||
iconSize: 48,
|
||||
style: IconButton.styleFrom(padding: const EdgeInsets.all(16)),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
IconButton.filledTonal(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(Icons.skip_next),
|
||||
iconSize: 32,
|
||||
const SizedBox(width: UIConstants.spacing24),
|
||||
// Play/Pause - larger main button
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc50,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.zinc50.withValues(alpha: 0.15),
|
||||
blurRadius: 16,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: isRunning ? onPause : onPlay,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Icon(
|
||||
isRunning ? Icons.pause_rounded : Icons.play_arrow_rounded,
|
||||
color: AppColors.zinc950,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing24),
|
||||
_ControlButton(
|
||||
icon: Icons.skip_next_rounded,
|
||||
onTap: onNext,
|
||||
size: 28,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ControlButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final double size;
|
||||
|
||||
const _ControlButton({
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.size = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ControlButton> createState() => _ControlButtonState();
|
||||
}
|
||||
|
||||
class _ControlButtonState extends State<_ControlButton> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered ? AppColors.zinc700 : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: _isHovered ? AppColors.textPrimary : AppColors.textSecondary,
|
||||
size: widget.size,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
|
||||
class SessionProgressBar extends StatelessWidget {
|
||||
final double progress;
|
||||
@@ -7,10 +8,23 @@ class SessionProgressBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
return Container(
|
||||
height: 4,
|
||||
color: AppColors.zinc800,
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: progress.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.accent,
|
||||
AppColors.accent.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:trainhub_flutter/database/database.dart';
|
||||
|
||||
class AnalysisProvider extends ChangeNotifier {
|
||||
final AppDatabase database;
|
||||
|
||||
List<AnalysisSession> _sessions = [];
|
||||
List<Annotation> _currentAnnotations = [];
|
||||
AnalysisSession? _activeSession;
|
||||
|
||||
List<AnalysisSession> get sessions => _sessions;
|
||||
List<Annotation> get currentAnnotations => _currentAnnotations;
|
||||
AnalysisSession? get activeSession => _activeSession;
|
||||
|
||||
AnalysisProvider(this.database) {
|
||||
_loadSessions();
|
||||
}
|
||||
|
||||
Future<void> _loadSessions() async {
|
||||
_sessions = await database.select(database.analysisSessions).get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> createSession(String name, String videoPath) async {
|
||||
final id = DateTime.now().toIso8601String();
|
||||
final session = AnalysisSessionsCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
videoPath: Value(videoPath),
|
||||
date: DateTime.now().toIso8601String(),
|
||||
);
|
||||
await database.into(database.analysisSessions).insert(session);
|
||||
await _loadSessions();
|
||||
await loadSession(id);
|
||||
}
|
||||
|
||||
Future<void> deleteSession(String id) async {
|
||||
await (database.delete(
|
||||
database.analysisSessions,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
if (_activeSession?.id == id) {
|
||||
_activeSession = null;
|
||||
_currentAnnotations = [];
|
||||
}
|
||||
await _loadSessions();
|
||||
}
|
||||
|
||||
Future<void> loadSession(String id) async {
|
||||
final session = await (database.select(
|
||||
database.analysisSessions,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (session != null) {
|
||||
_activeSession = session;
|
||||
await _loadAnnotations(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAnnotations(String sessionId) async {
|
||||
_currentAnnotations = await (database.select(
|
||||
database.annotations,
|
||||
)..where((t) => t.sessionId.equals(sessionId))).get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addAnnotation({
|
||||
required String name,
|
||||
required String description,
|
||||
required double startTime,
|
||||
required double endTime,
|
||||
required String color,
|
||||
}) async {
|
||||
if (_activeSession == null) return;
|
||||
|
||||
final id = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.annotations)
|
||||
.insert(
|
||||
AnnotationsCompanion.insert(
|
||||
id: id,
|
||||
sessionId: _activeSession!.id,
|
||||
name: Value(name),
|
||||
description: Value(description),
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
color: Value(color),
|
||||
),
|
||||
);
|
||||
await _loadAnnotations(_activeSession!.id);
|
||||
}
|
||||
|
||||
Future<void> updateAnnotation(Annotation annotation) async {
|
||||
await (database.update(
|
||||
database.annotations,
|
||||
)..where((t) => t.id.equals(annotation.id))).write(
|
||||
AnnotationsCompanion(
|
||||
name: Value(annotation.name),
|
||||
description: Value(annotation.description),
|
||||
startTime: Value(annotation.startTime),
|
||||
endTime: Value(annotation.endTime),
|
||||
color: Value(annotation.color),
|
||||
),
|
||||
);
|
||||
if (_activeSession != null) {
|
||||
await _loadAnnotations(_activeSession!.id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAnnotation(String id) async {
|
||||
await (database.delete(
|
||||
database.annotations,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
if (_activeSession != null) {
|
||||
await _loadAnnotations(_activeSession!.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:trainhub_flutter/database/database.dart';
|
||||
|
||||
class ChatProvider extends ChangeNotifier {
|
||||
final AppDatabase database;
|
||||
|
||||
List<ChatSession> _sessions = [];
|
||||
List<ChatMessage> _currentMessages = [];
|
||||
ChatSession? _activeSession;
|
||||
|
||||
List<ChatSession> get sessions => _sessions;
|
||||
List<ChatMessage> get currentMessages => _currentMessages;
|
||||
ChatSession? get activeSession => _activeSession;
|
||||
|
||||
bool _isTyping = false;
|
||||
bool get isTyping => _isTyping;
|
||||
|
||||
ChatProvider(this.database) {
|
||||
_loadSessions();
|
||||
}
|
||||
|
||||
Future<void> _loadSessions() async {
|
||||
_sessions =
|
||||
await (database.select(database.chatSessions)..orderBy([
|
||||
(t) => OrderingTerm(
|
||||
expression: t.createdAt,
|
||||
mode: OrderingMode.desc,
|
||||
),
|
||||
]))
|
||||
.get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> createSession() async {
|
||||
final id = DateTime.now().toIso8601String();
|
||||
final session = ChatSessionsCompanion.insert(
|
||||
id: id,
|
||||
title: const Value('New Chat'),
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
updatedAt: DateTime.now().toIso8601String(),
|
||||
);
|
||||
await database.into(database.chatSessions).insert(session);
|
||||
await _loadSessions();
|
||||
await loadSession(id);
|
||||
}
|
||||
|
||||
Future<void> deleteSession(String id) async {
|
||||
await (database.delete(
|
||||
database.chatSessions,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
if (_activeSession?.id == id) {
|
||||
_activeSession = null;
|
||||
_currentMessages = [];
|
||||
}
|
||||
await _loadSessions();
|
||||
}
|
||||
|
||||
Future<void> loadSession(String id) async {
|
||||
final session = await (database.select(
|
||||
database.chatSessions,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (session != null) {
|
||||
_activeSession = session;
|
||||
await _loadMessages(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMessages(String sessionId) async {
|
||||
_currentMessages =
|
||||
await (database.select(database.chatMessages)
|
||||
..where((t) => t.sessionId.equals(sessionId))
|
||||
..orderBy([
|
||||
(t) => OrderingTerm(
|
||||
expression: t.createdAt,
|
||||
mode: OrderingMode.asc,
|
||||
),
|
||||
]))
|
||||
.get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String content) async {
|
||||
if (_activeSession == null) {
|
||||
await createSession();
|
||||
}
|
||||
final sessionId = _activeSession!.id;
|
||||
|
||||
// User Message
|
||||
final userMsgId = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.chatMessages)
|
||||
.insert(
|
||||
ChatMessagesCompanion.insert(
|
||||
id: userMsgId,
|
||||
sessionId: sessionId,
|
||||
role: 'user',
|
||||
content: content,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
),
|
||||
);
|
||||
await _loadMessages(sessionId);
|
||||
|
||||
// AI Response (Mock)
|
||||
_isTyping = true;
|
||||
notifyListeners();
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate latency
|
||||
|
||||
final aiMsgId = DateTime.now().toIso8601String();
|
||||
final response = _getMockResponse(content);
|
||||
|
||||
await database
|
||||
.into(database.chatMessages)
|
||||
.insert(
|
||||
ChatMessagesCompanion.insert(
|
||||
id: aiMsgId,
|
||||
sessionId: sessionId,
|
||||
role: 'assistant',
|
||||
content: response,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
),
|
||||
);
|
||||
|
||||
// Update session title if it's the first message
|
||||
if (_currentMessages.length <= 2) {
|
||||
final newTitle = content.length > 30
|
||||
? '${content.substring(0, 30)}...'
|
||||
: content;
|
||||
await (database.update(database.chatSessions)
|
||||
..where((t) => t.id.equals(sessionId)))
|
||||
.write(ChatSessionsCompanion(title: Value(newTitle)));
|
||||
await _loadSessions();
|
||||
}
|
||||
|
||||
_isTyping = false;
|
||||
await _loadMessages(sessionId);
|
||||
}
|
||||
|
||||
String _getMockResponse(String input) {
|
||||
input = input.toLowerCase();
|
||||
if (input.contains('plan') || input.contains('program')) {
|
||||
return "I can help you design a training plan! What are your goals? Strength, hypertrophy, or endurance?";
|
||||
} else if (input.contains('squat') || input.contains('bench')) {
|
||||
return "Compound movements are great. Remember to maintain proper form. For squats, keep your chest up and knees tracking over toes.";
|
||||
} else if (input.contains('nutrition') || input.contains('eat')) {
|
||||
return "Nutrition is key. Aim for 1.6-2.2g of protein per kg of bodyweight if you're training hard.";
|
||||
}
|
||||
return "I'm your AI training assistant. How can I help you today?";
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:trainhub_flutter/database/database.dart';
|
||||
|
||||
class ProgramProvider extends ChangeNotifier {
|
||||
final AppDatabase database;
|
||||
|
||||
List<Program> _programs = [];
|
||||
Program? _activeProgram;
|
||||
List<ProgramWeek> _activeWeeks = [];
|
||||
List<ProgramWorkout> _activeWorkouts = [];
|
||||
|
||||
List<Program> get programs => _programs;
|
||||
Program? get activeProgram => _activeProgram;
|
||||
List<ProgramWeek> get activeWeeks => _activeWeeks;
|
||||
List<ProgramWorkout> get activeWorkouts => _activeWorkouts;
|
||||
|
||||
ProgramProvider(this.database) {
|
||||
_loadPrograms();
|
||||
}
|
||||
|
||||
Future<void> _loadPrograms() async {
|
||||
_programs =
|
||||
await (database.select(database.programs)..orderBy([
|
||||
(t) => OrderingTerm(
|
||||
expression: t.createdAt,
|
||||
mode: OrderingMode.desc,
|
||||
),
|
||||
]))
|
||||
.get();
|
||||
|
||||
// Auto-select most recent if none selected, or re-verify active one
|
||||
if (_activeProgram == null && _programs.isNotEmpty) {
|
||||
await loadProgram(_programs.first.id);
|
||||
} else if (_activeProgram != null) {
|
||||
// Refresh active program data
|
||||
await loadProgram(_activeProgram!.id);
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadProgram(String id) async {
|
||||
final program = await (database.select(
|
||||
database.programs,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
if (program != null) {
|
||||
_activeProgram = program;
|
||||
await _loadProgramDetails(id);
|
||||
} else {
|
||||
_activeProgram = null;
|
||||
_activeWeeks = [];
|
||||
_activeWorkouts = [];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadProgramDetails(String programId) async {
|
||||
_activeWeeks =
|
||||
await (database.select(database.programWeeks)
|
||||
..where((t) => t.programId.equals(programId))
|
||||
..orderBy([(t) => OrderingTerm(expression: t.position)]))
|
||||
.get();
|
||||
|
||||
_activeWorkouts = await (database.select(
|
||||
database.programWorkouts,
|
||||
)..where((t) => t.programId.equals(programId))).get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// --- Program Actions ---
|
||||
|
||||
Future<void> createProgram(String name) async {
|
||||
final id = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.programs)
|
||||
.insert(
|
||||
ProgramsCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
),
|
||||
);
|
||||
await _loadPrograms();
|
||||
await loadProgram(id);
|
||||
}
|
||||
|
||||
Future<void> deleteProgram(String id) async {
|
||||
await (database.delete(
|
||||
database.programs,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
if (_activeProgram?.id == id) {
|
||||
_activeProgram = null;
|
||||
}
|
||||
await _loadPrograms();
|
||||
}
|
||||
|
||||
Future<void> duplicateProgram(String sourceId) async {
|
||||
final sourceProgram = await (database.select(
|
||||
database.programs,
|
||||
)..where((t) => t.id.equals(sourceId))).getSingle();
|
||||
|
||||
final newId = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.programs)
|
||||
.insert(
|
||||
ProgramsCompanion.insert(
|
||||
id: newId,
|
||||
name: '${sourceProgram.name} (Copy)',
|
||||
createdAt: DateTime.now().toIso8601String(),
|
||||
),
|
||||
);
|
||||
|
||||
// Duplicate Weeks and Workouts
|
||||
// Note: implementing deep copy logic
|
||||
final weeks = await (database.select(
|
||||
database.programWeeks,
|
||||
)..where((t) => t.programId.equals(sourceId))).get();
|
||||
final workouts = await (database.select(
|
||||
database.programWorkouts,
|
||||
)..where((t) => t.programId.equals(sourceId))).get();
|
||||
|
||||
for (var week in weeks) {
|
||||
final newWeekId = '${newId}_${week.position}'; // Simple ID gen
|
||||
await database
|
||||
.into(database.programWeeks)
|
||||
.insert(
|
||||
ProgramWeeksCompanion.insert(
|
||||
id: newWeekId,
|
||||
programId: newId,
|
||||
position: week.position,
|
||||
notes: Value(week.notes),
|
||||
),
|
||||
);
|
||||
|
||||
final weekWorkouts = workouts.where((w) => w.weekId == week.id);
|
||||
for (var workout in weekWorkouts) {
|
||||
final newWorkoutId =
|
||||
DateTime.now().toIso8601String() + workout.id; // ensure uniqueness
|
||||
await database
|
||||
.into(database.programWorkouts)
|
||||
.insert(
|
||||
ProgramWorkoutsCompanion.insert(
|
||||
id: newWorkoutId,
|
||||
weekId: newWeekId,
|
||||
programId: newId,
|
||||
day: workout.day,
|
||||
type: workout.type,
|
||||
refId: Value(workout.refId),
|
||||
name: Value(workout.name),
|
||||
description: Value(workout.description),
|
||||
completed: const Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _loadPrograms();
|
||||
await loadProgram(newId);
|
||||
}
|
||||
|
||||
// --- Week Actions ---
|
||||
|
||||
Future<void> addWeek() async {
|
||||
if (_activeProgram == null) return;
|
||||
final nextPosition = _activeWeeks.isEmpty
|
||||
? 1
|
||||
: _activeWeeks.last.position + 1;
|
||||
final id = DateTime.now().toIso8601String();
|
||||
|
||||
await database
|
||||
.into(database.programWeeks)
|
||||
.insert(
|
||||
ProgramWeeksCompanion.insert(
|
||||
id: id,
|
||||
programId: _activeProgram!.id,
|
||||
position: nextPosition,
|
||||
),
|
||||
);
|
||||
await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
Future<void> deleteWeek(String id) async {
|
||||
if (_activeProgram == null) return;
|
||||
await (database.delete(
|
||||
database.programWeeks,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
Future<void> updateWeekNote(String weekId, String note) async {
|
||||
await (database.update(database.programWeeks)
|
||||
..where((t) => t.id.equals(weekId)))
|
||||
.write(ProgramWeeksCompanion(notes: Value(note)));
|
||||
if (_activeProgram != null) await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
// --- Workout Actions ---
|
||||
|
||||
Future<void> addWorkout(ProgramWorkoutsCompanion workout) async {
|
||||
await database.into(database.programWorkouts).insert(workout);
|
||||
if (_activeProgram != null) await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
Future<void> updateWorkout(ProgramWorkout workout) async {
|
||||
await (database.update(
|
||||
database.programWorkouts,
|
||||
)..where((t) => t.id.equals(workout.id))).write(
|
||||
ProgramWorkoutsCompanion(
|
||||
day: Value(workout.day),
|
||||
type: Value(workout.type),
|
||||
refId: Value(workout.refId),
|
||||
name: Value(workout.name),
|
||||
description: Value(workout.description),
|
||||
completed: Value(workout.completed),
|
||||
weekId: Value(workout.weekId),
|
||||
),
|
||||
);
|
||||
if (_activeProgram != null) await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
Future<void> deleteWorkout(String id) async {
|
||||
await (database.delete(
|
||||
database.programWorkouts,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
if (_activeProgram != null) await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
|
||||
Future<void> toggleWorkoutComplete(String id, bool currentStatus) async {
|
||||
await (database.update(database.programWorkouts)
|
||||
..where((t) => t.id.equals(id)))
|
||||
.write(ProgramWorkoutsCompanion(completed: Value(!currentStatus)));
|
||||
if (_activeProgram != null) await _loadProgramDetails(_activeProgram!.id);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:trainhub_flutter/database/database.dart';
|
||||
import 'package:trainhub_flutter/models/training_models.dart';
|
||||
|
||||
class TrainingsProvider extends ChangeNotifier {
|
||||
final AppDatabase database;
|
||||
|
||||
List<TrainingPlanModel> _plans = [];
|
||||
List<Exercise> _exercises = [];
|
||||
|
||||
List<TrainingPlanModel> get plans => _plans;
|
||||
List<Exercise> get exercises => _exercises;
|
||||
|
||||
TrainingsProvider(this.database) {
|
||||
_loadData();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
final dbPlans = await database.select(database.trainingPlans).get();
|
||||
_plans = dbPlans.map((p) {
|
||||
final sectionsJson = p.sections != null && p.sections!.isNotEmpty
|
||||
? jsonDecode(p.sections!)
|
||||
: [];
|
||||
// Manual mapping or just pass to model
|
||||
return TrainingPlanModel.fromJson({
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'sections': sectionsJson,
|
||||
});
|
||||
}).toList();
|
||||
|
||||
_exercises = await database.select(database.exercises).get();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<TrainingPlanModel> createPlan(String name) async {
|
||||
final id = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.trainingPlans)
|
||||
.insert(
|
||||
TrainingPlansCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
sections: const Value('[]'),
|
||||
),
|
||||
);
|
||||
await _loadData();
|
||||
return _plans.firstWhere((p) => p.id == id);
|
||||
}
|
||||
|
||||
Future<void> updatePlan(TrainingPlanModel plan) async {
|
||||
await (database.update(
|
||||
database.trainingPlans,
|
||||
)..where((t) => t.id.equals(plan.id))).write(
|
||||
TrainingPlansCompanion(
|
||||
name: Value(plan.name),
|
||||
sections: Value(
|
||||
jsonEncode(plan.sections.map((s) => s.toJson()).toList()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
Future<void> deletePlan(String id) async {
|
||||
await (database.delete(
|
||||
database.trainingPlans,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
Future<Exercise> addExercise(
|
||||
String name,
|
||||
String instructions,
|
||||
String tags,
|
||||
String videoUrl,
|
||||
) async {
|
||||
final id = DateTime.now().toIso8601String();
|
||||
await database
|
||||
.into(database.exercises)
|
||||
.insert(
|
||||
ExercisesCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
instructions: Value(instructions),
|
||||
tags: Value(tags), // Storing as JSON string
|
||||
videoUrl: Value(videoUrl),
|
||||
),
|
||||
);
|
||||
await _loadData();
|
||||
return _exercises.firstWhere((e) => e.id == id);
|
||||
}
|
||||
|
||||
Future<void> updateExercise(Exercise exercise) async {
|
||||
await (database.update(
|
||||
database.exercises,
|
||||
)..where((t) => t.id.equals(exercise.id))).write(
|
||||
ExercisesCompanion(
|
||||
name: Value(exercise.name),
|
||||
instructions: Value(exercise.instructions),
|
||||
tags: Value(exercise.tags),
|
||||
videoUrl: Value(exercise.videoUrl),
|
||||
),
|
||||
);
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
Future<void> deleteExercise(String id) async {
|
||||
await (database.delete(
|
||||
database.exercises,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
await _loadData();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class HTheme {
|
||||
static final darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: Color(0xFFFAFAFA), // Zinc 50
|
||||
onPrimary: Color(0xFF09090B), // Zinc 950
|
||||
secondary: Color(0xFFE4E4E7), // Zinc 200
|
||||
onSecondary: Color(0xFF09090B),
|
||||
surface: Color(0xFF09090B), // Zinc 950
|
||||
onSurface: Color(0xFFFAFAFA), // Zinc 50
|
||||
surfaceContainer: Color(0xFF18181B), // Zinc 900
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF09090B), // Zinc 950
|
||||
cardTheme: CardThemeData(
|
||||
color: const Color(0xFF18181B), // Zinc 900
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: const Color(0xFF27272A), width: 1), // Zinc 800
|
||||
),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: Color(0xFF27272A), // Zinc 800
|
||||
thickness: 1,
|
||||
),
|
||||
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme).apply(
|
||||
bodyColor: const Color(0xFFFAFAFA),
|
||||
displayColor: const Color(0xFFFAFAFA),
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: Color(0xFFA1A1AA), // Zinc 400
|
||||
),
|
||||
);
|
||||
}
|
||||
24
pubspec.lock
24
pubspec.lock
@@ -437,14 +437,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -637,14 +629,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.4"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -757,14 +741,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -49,9 +49,6 @@ dependencies:
|
||||
window_manager: ^0.3.9
|
||||
uuid: ^4.5.1
|
||||
|
||||
# Keep temporarily during migration
|
||||
provider: ^6.1.1
|
||||
go_router: ^12.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user