Some refactors
This commit is contained in:
@@ -6,7 +6,9 @@
|
|||||||
"Bash(dart run build_runner:*)",
|
"Bash(dart run build_runner:*)",
|
||||||
"Bash(findstr:*)",
|
"Bash(findstr:*)",
|
||||||
"Bash(dir /s /b \"C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\presentation\\\\*.dart\")",
|
"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
|
@riverpod
|
||||||
class AnalysisController extends _$AnalysisController {
|
class AnalysisController extends _$AnalysisController {
|
||||||
late final AnalysisRepository _repo;
|
late AnalysisRepository _repo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<AnalysisState> build() async {
|
Future<AnalysisState> build() async {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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';
|
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
|
||||||
|
|
||||||
class AnalysisSessionList extends StatelessWidget {
|
class AnalysisSessionList extends StatelessWidget {
|
||||||
@@ -15,26 +17,148 @@ class AnalysisSessionList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (sessions.isEmpty) {
|
|
||||||
return const Center(
|
|
||||||
child: Text('No analysis sessions yet. Tap + to create one.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(UIConstants.spacing24),
|
||||||
itemCount: sessions.length,
|
itemCount: sessions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final session = sessions[index];
|
final session = sessions[index];
|
||||||
return ListTile(
|
return _SessionCard(
|
||||||
leading: const CircleAvatar(child: Icon(Icons.video_library)),
|
session: session,
|
||||||
title: Text(session.name),
|
|
||||||
subtitle: Text(session.date),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
onPressed: () => onDeleteSession(session),
|
|
||||||
),
|
|
||||||
onTap: () => onSessionSelected(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
|
@riverpod
|
||||||
class CalendarController extends _$CalendarController {
|
class CalendarController extends _$CalendarController {
|
||||||
late final ProgramRepository _programRepo;
|
late ProgramRepository _programRepo;
|
||||||
late final TrainingPlanRepository _planRepo;
|
late TrainingPlanRepository _planRepo;
|
||||||
late final ExerciseRepository _exerciseRepo;
|
late ExerciseRepository _exerciseRepo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CalendarState> build() async {
|
Future<CalendarState> build() async {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/calendar_controller.dart';
|
||||||
import 'package:trainhub_flutter/presentation/calendar/widgets/program_selector.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/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()
|
@RoutePage()
|
||||||
class CalendarPage extends ConsumerWidget {
|
class CalendarPage extends ConsumerWidget {
|
||||||
@@ -14,16 +19,15 @@ class CalendarPage extends ConsumerWidget {
|
|||||||
final state = ref.watch(calendarControllerProvider);
|
final state = ref.watch(calendarControllerProvider);
|
||||||
final controller = ref.read(calendarControllerProvider.notifier);
|
final controller = ref.read(calendarControllerProvider.notifier);
|
||||||
|
|
||||||
return Scaffold(
|
return state.when(
|
||||||
appBar: AppBar(title: const Text('Program & Schedule')),
|
|
||||||
body: state.when(
|
|
||||||
data: (data) {
|
data: (data) {
|
||||||
if (data.programs.isEmpty) {
|
if (data.programs.isEmpty) {
|
||||||
return Center(
|
return AppEmptyState(
|
||||||
child: ElevatedButton(
|
icon: Icons.calendar_month_outlined,
|
||||||
onPressed: () => _showCreateProgramDialog(context, controller),
|
title: 'No programs yet',
|
||||||
child: const Text('Create First Program'),
|
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),
|
onProgramSelected: (p) => controller.loadProgram(p.id),
|
||||||
onCreateProgram: () =>
|
onCreateProgram: () =>
|
||||||
_showCreateProgramDialog(context, controller),
|
_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(
|
Expanded(
|
||||||
child: data.activeProgram == null
|
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(
|
: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(UIConstants.spacing24),
|
||||||
itemCount: data.weeks.length + 1,
|
itemCount: data.weeks.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == data.weeks.length) {
|
if (index == data.weeks.length) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(vertical: UIConstants.spacing24),
|
||||||
vertical: 24.0,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: controller.addWeek,
|
onPressed: controller.addWeek,
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add, size: 18),
|
||||||
label: const Text("Add Week"),
|
label: const Text('Add Week'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -65,9 +93,8 @@ class CalendarPage extends ConsumerWidget {
|
|||||||
week: week,
|
week: week,
|
||||||
workouts: weekWorkouts,
|
workouts: weekWorkouts,
|
||||||
availablePlans: data.plans,
|
availablePlans: data.plans,
|
||||||
onAddWorkout: (workout) => controller.addWorkout(
|
onAddWorkout: (workout) =>
|
||||||
workout,
|
controller.addWorkout(workout),
|
||||||
), // logic needs refined params
|
|
||||||
onDeleteWorkout: controller.deleteWorkout,
|
onDeleteWorkout: controller.deleteWorkout,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -76,41 +103,27 @@ class CalendarPage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (e, s) => Center(child: Text('Error: $e')),
|
error: (e, s) => Center(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
child: Text(
|
||||||
|
'Error: $e',
|
||||||
|
style: const TextStyle(color: AppColors.destructive),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateProgramDialog(
|
Future<void> _showCreateProgramDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
CalendarController controller,
|
CalendarController controller,
|
||||||
) {
|
) async {
|
||||||
final textController = TextEditingController();
|
final name = await TextInputDialog.show(
|
||||||
showDialog(
|
context,
|
||||||
context: context,
|
title: 'New Program',
|
||||||
builder: (context) => AlertDialog(
|
hintText: 'e.g. 12 Week Strength',
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (name != null && name.isNotEmpty) {
|
||||||
|
controller.createProgram(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part 'chat_controller.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class ChatController extends _$ChatController {
|
class ChatController extends _$ChatController {
|
||||||
late final ChatRepository _repo;
|
late ChatRepository _repo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ChatState> build() async {
|
Future<ChatState> build() async {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ part 'plan_editor_controller.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class PlanEditorController extends _$PlanEditorController {
|
class PlanEditorController extends _$PlanEditorController {
|
||||||
late final TrainingPlanRepository _planRepo;
|
late TrainingPlanRepository _planRepo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PlanEditorState> build(String planId) async {
|
Future<PlanEditorState> build(String planId) async {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ part 'trainings_controller.g.dart';
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
class TrainingsController extends _$TrainingsController {
|
class TrainingsController extends _$TrainingsController {
|
||||||
late final TrainingPlanRepository _planRepo;
|
late TrainingPlanRepository _planRepo;
|
||||||
late final ExerciseRepository _exerciseRepo;
|
late ExerciseRepository _exerciseRepo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TrainingsState> build() async {
|
Future<TrainingsState> build() async {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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';
|
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
|
||||||
|
|
||||||
class ActivityCard extends StatelessWidget {
|
class ActivityCard extends StatelessWidget {
|
||||||
@@ -8,65 +10,127 @@ class ActivityCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isRest = activity.type == 'rest';
|
final isRest = activity.isRest;
|
||||||
|
final accentColor = isRest ? AppColors.info : AppColors.accent;
|
||||||
|
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 4,
|
padding: const EdgeInsets.symmetric(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
horizontal: UIConstants.spacing24,
|
||||||
child: Padding(
|
vertical: UIConstants.spacing16,
|
||||||
padding: const EdgeInsets.all(24.0),
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceContainer.withValues(alpha: 0.6),
|
||||||
|
borderRadius: UIConstants.cardBorderRadius,
|
||||||
|
border: Border.all(
|
||||||
|
color: accentColor.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
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(
|
Text(
|
||||||
activity.name,
|
activity.name,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: const TextStyle(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (!isRest) ...[
|
if (!isRest) ...[
|
||||||
|
const SizedBox(height: UIConstants.spacing8),
|
||||||
Text(
|
Text(
|
||||||
"${activity.sectionName} • Set ${activity.setIndex}/${activity.totalSets}",
|
'${activity.sectionName ?? ''} \u00B7 Set ${activity.setIndex}/${activity.totalSets}',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: const TextStyle(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
if (activity.originalExercise != null) ...[
|
||||||
if (activity.originalExercise != null)
|
const SizedBox(height: UIConstants.spacing16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildInfo(
|
_InfoChip(
|
||||||
context,
|
label: 'Sets',
|
||||||
"Sets",
|
value: '${activity.originalExercise!.sets}',
|
||||||
"${activity.originalExercise!.sets}",
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: UIConstants.spacing16),
|
||||||
_buildInfo(
|
_InfoChip(
|
||||||
context,
|
label: activity.originalExercise!.isTime ? 'Secs' : 'Reps',
|
||||||
activity.originalExercise!.isTime ? "Secs" : "Reps",
|
value: '${activity.originalExercise!.value}',
|
||||||
"${activity.originalExercise!.value}",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
] else
|
] else
|
||||||
Text(
|
Padding(
|
||||||
"Resting...",
|
padding: const EdgeInsets.only(top: UIConstants.spacing8),
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
child: Text(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
'Take a break',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildInfo(BuildContext context, String label, String value) {
|
class _InfoChip extends StatelessWidget {
|
||||||
return Column(
|
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: [
|
children: [
|
||||||
Text(value, style: Theme.of(context).textTheme.headlineSmall),
|
Text(
|
||||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class SessionControls extends StatelessWidget {
|
||||||
final bool isRunning;
|
final bool isRunning;
|
||||||
@@ -20,35 +22,109 @@ class SessionControls extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isFinished) {
|
if (isFinished) return const SizedBox.shrink();
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Finish Workout'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton.filledTonal(
|
_ControlButton(
|
||||||
onPressed: onPrevious,
|
icon: Icons.skip_previous_rounded,
|
||||||
icon: const Icon(Icons.skip_previous),
|
onTap: onPrevious,
|
||||||
iconSize: 32,
|
size: 28,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: UIConstants.spacing24),
|
||||||
IconButton.filled(
|
// Play/Pause - larger main button
|
||||||
onPressed: isRunning ? onPause : onPlay,
|
Container(
|
||||||
icon: Icon(isRunning ? Icons.pause : Icons.play_arrow),
|
width: 56,
|
||||||
iconSize: 48,
|
height: 56,
|
||||||
style: IconButton.styleFrom(padding: const EdgeInsets.all(16)),
|
decoration: BoxDecoration(
|
||||||
),
|
color: AppColors.zinc50,
|
||||||
const SizedBox(width: 24),
|
shape: BoxShape.circle,
|
||||||
IconButton.filledTonal(
|
boxShadow: [
|
||||||
onPressed: onNext,
|
BoxShadow(
|
||||||
icon: const Icon(Icons.skip_next),
|
color: AppColors.zinc50.withValues(alpha: 0.15),
|
||||||
iconSize: 32,
|
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:flutter/material.dart';
|
||||||
|
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||||
|
|
||||||
class SessionProgressBar extends StatelessWidget {
|
class SessionProgressBar extends StatelessWidget {
|
||||||
final double progress;
|
final double progress;
|
||||||
@@ -7,10 +8,23 @@ class SessionProgressBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LinearProgressIndicator(
|
return Container(
|
||||||
value: progress,
|
height: 4,
|
||||||
minHeight: 8,
|
color: AppColors.zinc800,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
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:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -637,14 +629,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.4"
|
version: "0.17.4"
|
||||||
nested:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: nested
|
|
||||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -757,14 +741,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
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:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ dependencies:
|
|||||||
window_manager: ^0.3.9
|
window_manager: ^0.3.9
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
|
|
||||||
# Keep temporarily during migration
|
|
||||||
provider: ^6.1.1
|
|
||||||
go_router: ^12.1.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user