Some refactors

This commit is contained in:
Kazimierz Ciołek
2026-02-19 14:16:03 +01:00
parent 782986a632
commit f943e89430
21 changed files with 490 additions and 7378 deletions

View File

@@ -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''\")"
]
}
}

View File

@@ -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

View File

@@ -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,
};
}
}

View File

@@ -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 {

View File

@@ -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,
),
],
),
),
),
),
),
);
}
}

View File

@@ -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 {

View File

@@ -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,103 +19,111 @@ 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(
data: (data) {
if (data.programs.isEmpty) {
return Center(
child: ElevatedButton(
onPressed: () => _showCreateProgramDialog(context, controller),
child: const Text('Create First Program'),
),
);
}
return Column(
children: [
ProgramSelector(
programs: data.programs,
activeProgram: data.activeProgram,
onProgramSelected: (p) => controller.loadProgram(p.id),
onCreateProgram: () =>
_showCreateProgramDialog(context, controller),
),
Expanded(
child: data.activeProgram == null
? const Center(child: Text("Select a program"))
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: data.weeks.length + 1,
itemBuilder: (context, index) {
if (index == data.weeks.length) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 24.0,
),
child: Center(
child: OutlinedButton.icon(
onPressed: controller.addWeek,
icon: const Icon(Icons.add),
label: const Text("Add Week"),
),
),
);
}
final week = data.weeks[index];
final weekWorkouts = data.workouts
.where((w) => w.weekId == week.id)
.toList();
return ProgramWeekView(
week: week,
workouts: weekWorkouts,
availablePlans: data.plans,
onAddWorkout: (workout) => controller.addWorkout(
workout,
), // logic needs refined params
onDeleteWorkout: controller.deleteWorkout,
);
},
),
),
],
return state.when(
data: (data) {
if (data.programs.isEmpty) {
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),
);
},
error: (e, s) => Center(child: Text('Error: $e')),
loading: () => const Center(child: CircularProgressIndicator()),
}
return Column(
children: [
ProgramSelector(
programs: data.programs,
activeProgram: data.activeProgram,
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 to view its schedule',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
),
)
: ListView.builder(
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: UIConstants.spacing24),
child: Center(
child: OutlinedButton.icon(
onPressed: controller.addWeek,
icon: const Icon(Icons.add, size: 18),
label: const Text('Add Week'),
),
),
);
}
final week = data.weeks[index];
final weekWorkouts = data.workouts
.where((w) => w.weekId == week.id)
.toList();
return ProgramWeekView(
week: week,
workouts: weekWorkouts,
availablePlans: data.plans,
onAddWorkout: (workout) =>
controller.addWorkout(workout),
onDeleteWorkout: controller.deleteWorkout,
);
},
),
),
],
);
},
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);
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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),
child: Column(
children: [
Text(
activity.name,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
if (!isRest) ...[
Text(
"${activity.sectionName} • Set ${activity.setIndex}/${activity.totalSets}",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
const SizedBox(height: 16),
if (activity.originalExercise != null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildInfo(
context,
"Sets",
"${activity.originalExercise!.sets}",
),
const SizedBox(width: 24),
_buildInfo(
context,
activity.originalExercise!.isTime ? "Secs" : "Reps",
"${activity.originalExercise!.value}",
),
],
),
] else
Text(
"Resting...",
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
],
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: const TextStyle(
color: AppColors.textPrimary,
fontSize: 22,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
if (!isRest) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
'${activity.sectionName ?? ''} \u00B7 Set ${activity.setIndex}/${activity.totalSets}',
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 13,
),
),
if (activity.originalExercise != null) ...[
const SizedBox(height: UIConstants.spacing16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_InfoChip(
label: 'Sets',
value: '${activity.originalExercise!.sets}',
),
const SizedBox(width: UIConstants.spacing16),
_InfoChip(
label: activity.originalExercise!.isTime ? 'Secs' : 'Reps',
value: '${activity.originalExercise!.value}',
),
],
),
],
] else
Padding(
padding: const EdgeInsets.only(top: UIConstants.spacing8),
child: Text(
'Take a break',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
),
),
],
),
);
}
}
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),
],
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,
),
),
],
),
);
}
}

View File

@@ -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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton.filledTonal(
onPressed: onPrevious,
icon: const Icon(Icons.skip_previous),
iconSize: 32,
),
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,
),
],
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: [
_ControlButton(
icon: Icons.skip_previous_rounded,
onTap: onPrevious,
size: 28,
),
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,
),
),
),
);
}
}

View File

@@ -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),
],
),
),
),
),
);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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?";
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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
),
);
}

View File

@@ -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:

View File

@@ -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: