Initial commit

This commit is contained in:
Kazimierz Ciołek
2026-02-19 02:49:29 +01:00
commit 782986a632
148 changed files with 29230 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
class AppConstants {
AppConstants._();
static const String appName = 'TrainHub';
static const double windowWidth = 1280;
static const double windowHeight = 800;
static const double minWindowWidth = 800;
static const double minWindowHeight = 600;
static const String databaseName = 'trainhub.sqlite';
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
class UIConstants {
UIConstants._();
static const double pagePadding = 24.0;
static const double cardPadding = 16.0;
static const double sidebarWidth = 300.0;
static const double analysisSidebarWidth = 350.0;
static const double dialogWidth = 400.0;
static const double spacing4 = 4.0;
static const double spacing8 = 8.0;
static const double spacing12 = 12.0;
static const double spacing16 = 16.0;
static const double spacing24 = 24.0;
static const double spacing32 = 32.0;
static const double borderRadius = 12.0;
static const double smallBorderRadius = 8.0;
static const Duration animationDuration = Duration(milliseconds: 200);
static const Duration snackBarDuration = Duration(seconds: 2);
static const double navRailWidth = 80.0;
static BorderRadius get cardBorderRadius =>
BorderRadius.circular(borderRadius);
static BorderRadius get smallCardBorderRadius =>
BorderRadius.circular(smallBorderRadius);
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
extension BuildContextExtensions on BuildContext {
ThemeData get theme => Theme.of(this);
ColorScheme get colors => theme.colorScheme;
TextTheme get textTheme => theme.textTheme;
MediaQueryData get mediaQuery => MediaQuery.of(this);
double get screenWidth => mediaQuery.size.width;
double get screenHeight => mediaQuery.size.height;
void showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor:
isError ? AppColors.destructive : AppColors.surfaceContainer,
),
);
}
void showSuccessSnackBar(String message) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: AppColors.success, size: 18),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
extension DateTimeExtensions on DateTime {
String toDisplayDate() {
return '${year.toString()}-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
String toIso() => toIso8601String();
}

View File

@@ -0,0 +1,15 @@
extension DurationExtensions on Duration {
String toMmSs() {
final int minutes = inMinutes.remainder(60);
final int seconds = inSeconds.remainder(60);
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}
extension IntTimeExtensions on int {
String toMmSs() {
final int minutes = this ~/ 60;
final int seconds = this % 60;
return '$minutes:${seconds.toString().padLeft(2, '0')}';
}
}

View File

@@ -0,0 +1,32 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:trainhub_flutter/presentation/analysis/analysis_page.dart';
import 'package:trainhub_flutter/presentation/calendar/calendar_page.dart';
import 'package:trainhub_flutter/presentation/chat/chat_page.dart';
import 'package:trainhub_flutter/presentation/home/home_page.dart';
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_page.dart';
import 'package:trainhub_flutter/presentation/shell/shell_page.dart';
import 'package:trainhub_flutter/presentation/trainings/trainings_page.dart';
import 'package:trainhub_flutter/presentation/workout_session/workout_session_page.dart';
part 'app_router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,Route')
class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(
page: ShellRoute.page,
initial: true,
children: [
AutoRoute(page: HomeRoute.page, initial: true),
AutoRoute(page: TrainingsRoute.page),
AutoRoute(page: AnalysisRoute.page),
AutoRoute(page: CalendarRoute.page),
AutoRoute(page: ChatRoute.page),
],
),
AutoRoute(page: PlanEditorRoute.page),
AutoRoute(page: WorkoutSessionRoute.page),
];
}

View File

@@ -0,0 +1,191 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
part of 'app_router.dart';
/// generated route for
/// [AnalysisPage]
class AnalysisRoute extends PageRouteInfo<void> {
const AnalysisRoute({List<PageRouteInfo>? children})
: super(AnalysisRoute.name, initialChildren: children);
static const String name = 'AnalysisRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const AnalysisPage();
},
);
}
/// generated route for
/// [CalendarPage]
class CalendarRoute extends PageRouteInfo<void> {
const CalendarRoute({List<PageRouteInfo>? children})
: super(CalendarRoute.name, initialChildren: children);
static const String name = 'CalendarRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const CalendarPage();
},
);
}
/// generated route for
/// [ChatPage]
class ChatRoute extends PageRouteInfo<void> {
const ChatRoute({List<PageRouteInfo>? children})
: super(ChatRoute.name, initialChildren: children);
static const String name = 'ChatRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const ChatPage();
},
);
}
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {
const HomeRoute({List<PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const HomePage();
},
);
}
/// generated route for
/// [PlanEditorPage]
class PlanEditorRoute extends PageRouteInfo<PlanEditorRouteArgs> {
PlanEditorRoute({
Key? key,
required String planId,
List<PageRouteInfo>? children,
}) : super(
PlanEditorRoute.name,
args: PlanEditorRouteArgs(key: key, planId: planId),
rawPathParams: {'planId': planId},
initialChildren: children,
);
static const String name = 'PlanEditorRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
final args = data.argsAs<PlanEditorRouteArgs>(
orElse: () =>
PlanEditorRouteArgs(planId: pathParams.getString('planId')),
);
return PlanEditorPage(key: args.key, planId: args.planId);
},
);
}
class PlanEditorRouteArgs {
const PlanEditorRouteArgs({this.key, required this.planId});
final Key? key;
final String planId;
@override
String toString() {
return 'PlanEditorRouteArgs{key: $key, planId: $planId}';
}
}
/// generated route for
/// [ShellPage]
class ShellRoute extends PageRouteInfo<void> {
const ShellRoute({List<PageRouteInfo>? children})
: super(ShellRoute.name, initialChildren: children);
static const String name = 'ShellRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const ShellPage();
},
);
}
/// generated route for
/// [TrainingsPage]
class TrainingsRoute extends PageRouteInfo<void> {
const TrainingsRoute({List<PageRouteInfo>? children})
: super(TrainingsRoute.name, initialChildren: children);
static const String name = 'TrainingsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const TrainingsPage();
},
);
}
/// generated route for
/// [WorkoutSessionPage]
class WorkoutSessionRoute extends PageRouteInfo<WorkoutSessionRouteArgs> {
WorkoutSessionRoute({
Key? key,
required String planId,
List<PageRouteInfo>? children,
}) : super(
WorkoutSessionRoute.name,
args: WorkoutSessionRouteArgs(key: key, planId: planId),
rawPathParams: {'planId': planId},
initialChildren: children,
);
static const String name = 'WorkoutSessionRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final pathParams = data.inheritedPathParams;
final args = data.argsAs<WorkoutSessionRouteArgs>(
orElse: () =>
WorkoutSessionRouteArgs(planId: pathParams.getString('planId')),
);
return WorkoutSessionPage(key: args.key, planId: args.planId);
},
);
}
class WorkoutSessionRouteArgs {
const WorkoutSessionRouteArgs({this.key, required this.planId});
final Key? key;
final String planId;
@override
String toString() {
return 'WorkoutSessionRouteArgs{key: $key, planId: $planId}';
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class AppColors {
AppColors._();
// Zinc palette
static const Color zinc50 = Color(0xFFFAFAFA);
static const Color zinc100 = Color(0xFFF4F4F5);
static const Color zinc200 = Color(0xFFE4E4E7);
static const Color zinc300 = Color(0xFFD4D4D8);
static const Color zinc400 = Color(0xFFA1A1AA);
static const Color zinc500 = Color(0xFF71717A);
static const Color zinc600 = Color(0xFF52525B);
static const Color zinc700 = Color(0xFF3F3F46);
static const Color zinc800 = Color(0xFF27272A);
static const Color zinc900 = Color(0xFF18181B);
static const Color zinc950 = Color(0xFF09090B);
// Semantic colors
static const Color surface = zinc950;
static const Color surfaceContainer = zinc900;
static const Color surfaceContainerHigh = zinc800;
static const Color border = zinc800;
static const Color borderSubtle = Color(0xFF1F1F23);
static const Color textPrimary = zinc50;
static const Color textSecondary = zinc400;
static const Color textMuted = zinc500;
// Accent colors
static const Color accent = Color(0xFFFF9800);
static const Color accentMuted = Color(0x33FF9800);
static const Color success = Color(0xFF22C55E);
static const Color successMuted = Color(0x3322C55E);
static const Color destructive = Color(0xFFEF4444);
static const Color destructiveMuted = Color(0x33EF4444);
static const Color info = Color(0xFF3B82F6);
static const Color infoMuted = Color(0x333B82F6);
static const Color purple = Color(0xFF8B5CF6);
static const Color purpleMuted = Color(0x338B5CF6);
static const Color warning = Color(0xFFF59E0B);
}

View File

@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
class AppTheme {
AppTheme._();
static final ThemeData dark = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: const ColorScheme.dark(
primary: AppColors.zinc50,
onPrimary: AppColors.zinc950,
secondary: AppColors.zinc200,
onSecondary: AppColors.zinc950,
surface: AppColors.zinc950,
onSurface: AppColors.zinc50,
surfaceContainer: AppColors.zinc900,
error: AppColors.destructive,
onError: AppColors.zinc50,
outline: AppColors.zinc800,
outlineVariant: AppColors.zinc700,
),
scaffoldBackgroundColor: AppColors.surface,
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme).apply(
bodyColor: AppColors.textPrimary,
displayColor: AppColors.textPrimary,
),
cardTheme: CardThemeData(
color: AppColors.surfaceContainer,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.cardBorderRadius,
side: const BorderSide(color: AppColors.border, width: 1),
),
margin: EdgeInsets.zero,
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
iconTheme: const IconThemeData(
color: AppColors.textSecondary,
size: 20,
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: AppColors.surfaceContainer,
selectedIconTheme: const IconThemeData(color: AppColors.textPrimary),
unselectedIconTheme: const IconThemeData(color: AppColors.textMuted),
selectedLabelTextStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
unselectedLabelTextStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.textMuted,
),
indicatorColor: AppColors.zinc700,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceContainer,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
border: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.zinc400, width: 1),
),
hintStyle: GoogleFonts.inter(
color: AppColors.textMuted,
fontSize: 14,
),
labelStyle: GoogleFonts.inter(
color: AppColors.textSecondary,
fontSize: 14,
),
),
dialogTheme: DialogThemeData(
backgroundColor: AppColors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.cardBorderRadius,
side: const BorderSide(color: AppColors.border),
),
titleTextStyle: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.zinc50,
foregroundColor: AppColors.zinc950,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.textSecondary,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
tabBarTheme: TabBarThemeData(
labelColor: AppColors.textPrimary,
unselectedLabelColor: AppColors.textMuted,
indicatorColor: AppColors.textPrimary,
dividerColor: AppColors.border,
labelStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.surfaceContainer,
contentTextStyle: GoogleFonts.inter(
color: AppColors.textPrimary,
fontSize: 14,
),
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
side: const BorderSide(color: AppColors.border),
),
behavior: SnackBarBehavior.floating,
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: AppColors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
),
dropdownMenuTheme: DropdownMenuThemeData(
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceContainer,
border: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.border),
),
),
),
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.zinc50;
}
return Colors.transparent;
}),
checkColor: WidgetStateProperty.all(AppColors.zinc950),
side: const BorderSide(color: AppColors.zinc400),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
tooltipTheme: TooltipThemeData(
decoration: BoxDecoration(
color: AppColors.zinc800,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppColors.zinc700),
),
textStyle: GoogleFonts.inter(
color: AppColors.textPrimary,
fontSize: 12,
),
),
);
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
class AppTypography {
AppTypography._();
static TextStyle get displayLarge => GoogleFonts.inter(
fontSize: 36,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
height: 1.2,
);
static TextStyle get headlineLarge => GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
height: 1.3,
);
static TextStyle get headlineMedium => GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
height: 1.3,
);
static TextStyle get titleLarge => GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
height: 1.4,
);
static TextStyle get titleMedium => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
height: 1.4,
);
static TextStyle get titleSmall => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
height: 1.4,
);
static TextStyle get bodyLarge => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
height: 1.5,
);
static TextStyle get bodyMedium => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
height: 1.5,
);
static TextStyle get bodySmall => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.textSecondary,
height: 1.5,
);
static TextStyle get labelLarge => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
height: 1.4,
);
static TextStyle get labelMedium => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
height: 1.4,
);
static TextStyle get caption => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.textMuted,
height: 1.4,
);
static TextStyle get monoLarge => GoogleFonts.jetBrainsMono(
fontSize: 72,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
);
static TextStyle get monoMedium => GoogleFonts.jetBrainsMono(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.textPrimary,
);
}

View File

@@ -0,0 +1,9 @@
import 'package:uuid/uuid.dart';
class IdGenerator {
IdGenerator._();
static const Uuid _uuid = Uuid();
static String generate() => _uuid.v4();
}

View File

@@ -0,0 +1,20 @@
import 'dart:convert';
class JsonUtils {
JsonUtils._();
static List<dynamic> safeDecodeList(String? json) {
if (json == null || json.isEmpty) return [];
try {
final decoded = jsonDecode(json);
if (decoded is List) return decoded;
return [];
} catch (_) {
return [];
}
}
static String encodeList(List<Map<String, dynamic>> list) {
return jsonEncode(list);
}
}

View File

@@ -0,0 +1,152 @@
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 'package:trainhub_flutter/core/constants/app_constants.dart';
import 'package:trainhub_flutter/data/database/daos/exercise_dao.dart';
import 'package:trainhub_flutter/data/database/daos/training_plan_dao.dart';
import 'package:trainhub_flutter/data/database/daos/program_dao.dart';
import 'package:trainhub_flutter/data/database/daos/analysis_dao.dart';
import 'package:trainhub_flutter/data/database/daos/chat_dao.dart';
import 'dart:io';
part 'app_database.g.dart';
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()();
TextColumn get videoUrl => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class TrainingPlans extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get sections => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class Programs extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get createdAt => text()();
@override
Set<Column> get primaryKey => {id};
}
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};
}
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()();
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};
}
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};
}
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};
}
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};
}
class ChatMessages extends Table {
TextColumn get id => text()();
TextColumn get sessionId =>
text().references(ChatSessions, #id, onDelete: KeyAction.cascade)();
TextColumn get role => text()();
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,
],
daos: [
ExerciseDao,
TrainingPlanDao,
ProgramDao,
AnalysisDao,
ChatDao,
],
)
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, AppConstants.databaseName));
return NativeDatabase.createInBackground(file);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
part 'analysis_dao.g.dart';
@DriftAccessor(tables: [AnalysisSessions, Annotations])
class AnalysisDao extends DatabaseAccessor<AppDatabase>
with _$AnalysisDaoMixin {
AnalysisDao(super.db);
Future<List<AnalysisSession>> getAllSessions() =>
select(analysisSessions).get();
Future<AnalysisSession?> getSession(String id) =>
(select(analysisSessions)..where((t) => t.id.equals(id)))
.getSingleOrNull();
Future<void> insertSession(AnalysisSessionsCompanion entry) =>
into(analysisSessions).insert(entry);
Future<void> deleteSession(String id) =>
(delete(analysisSessions)..where((t) => t.id.equals(id))).go();
Future<List<Annotation>> getAnnotations(String sessionId) =>
(select(annotations)..where((t) => t.sessionId.equals(sessionId))).get();
Future<void> insertAnnotation(AnnotationsCompanion entry) =>
into(annotations).insert(entry);
Future<void> deleteAnnotation(String id) =>
(delete(annotations)..where((t) => t.id.equals(id))).go();
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'analysis_dao.dart';
// ignore_for_file: type=lint
mixin _$AnalysisDaoMixin on DatabaseAccessor<AppDatabase> {
$AnalysisSessionsTable get analysisSessions =>
attachedDatabase.analysisSessions;
$AnnotationsTable get annotations => attachedDatabase.annotations;
}

View File

@@ -0,0 +1,43 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
part 'chat_dao.g.dart';
@DriftAccessor(tables: [ChatSessions, ChatMessages])
class ChatDao extends DatabaseAccessor<AppDatabase> with _$ChatDaoMixin {
ChatDao(super.db);
Future<List<ChatSession>> getAllSessions() =>
(select(chatSessions)
..orderBy([
(t) => OrderingTerm(
expression: t.createdAt, mode: OrderingMode.desc)
]))
.get();
Future<ChatSession?> getSession(String id) =>
(select(chatSessions)..where((t) => t.id.equals(id)))
.getSingleOrNull();
Future<void> insertSession(ChatSessionsCompanion entry) =>
into(chatSessions).insert(entry);
Future<void> deleteSession(String id) =>
(delete(chatSessions)..where((t) => t.id.equals(id))).go();
Future<void> updateSessionTitle(String id, String title) =>
(update(chatSessions)..where((t) => t.id.equals(id)))
.write(ChatSessionsCompanion(title: Value(title)));
Future<List<ChatMessage>> getMessages(String sessionId) =>
(select(chatMessages)
..where((t) => t.sessionId.equals(sessionId))
..orderBy([
(t) =>
OrderingTerm(expression: t.createdAt, mode: OrderingMode.asc)
]))
.get();
Future<void> insertMessage(ChatMessagesCompanion entry) =>
into(chatMessages).insert(entry);
}

View File

@@ -0,0 +1,9 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_dao.dart';
// ignore_for_file: type=lint
mixin _$ChatDaoMixin on DatabaseAccessor<AppDatabase> {
$ChatSessionsTable get chatSessions => attachedDatabase.chatSessions;
$ChatMessagesTable get chatMessages => attachedDatabase.chatMessages;
}

View File

@@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
part 'exercise_dao.g.dart';
@DriftAccessor(tables: [Exercises])
class ExerciseDao extends DatabaseAccessor<AppDatabase>
with _$ExerciseDaoMixin {
ExerciseDao(super.db);
Future<List<Exercise>> getAllExercises() => select(exercises).get();
Future<Exercise> getExerciseById(String id) =>
(select(exercises)..where((t) => t.id.equals(id))).getSingle();
Future<void> insertExercise(ExercisesCompanion entry) =>
into(exercises).insert(entry);
Future<void> updateExercise(String id, ExercisesCompanion entry) =>
(update(exercises)..where((t) => t.id.equals(id))).write(entry);
Future<void> deleteExercise(String id) =>
(delete(exercises)..where((t) => t.id.equals(id))).go();
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'exercise_dao.dart';
// ignore_for_file: type=lint
mixin _$ExerciseDaoMixin on DatabaseAccessor<AppDatabase> {
$ExercisesTable get exercises => attachedDatabase.exercises;
}

View File

@@ -0,0 +1,60 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
part 'program_dao.g.dart';
@DriftAccessor(tables: [Programs, ProgramWeeks, ProgramWorkouts])
class ProgramDao extends DatabaseAccessor<AppDatabase>
with _$ProgramDaoMixin {
ProgramDao(super.db);
Future<List<Program>> getAllPrograms() =>
(select(programs)
..orderBy([
(t) =>
OrderingTerm(expression: t.createdAt, mode: OrderingMode.desc)
]))
.get();
Future<Program?> getProgram(String id) =>
(select(programs)..where((t) => t.id.equals(id))).getSingleOrNull();
Future<void> insertProgram(ProgramsCompanion entry) =>
into(programs).insert(entry);
Future<void> deleteProgram(String id) =>
(delete(programs)..where((t) => t.id.equals(id))).go();
Future<List<ProgramWeek>> getWeeks(String programId) =>
(select(programWeeks)
..where((t) => t.programId.equals(programId))
..orderBy([(t) => OrderingTerm(expression: t.position)]))
.get();
Future<void> insertWeek(ProgramWeeksCompanion entry) =>
into(programWeeks).insert(entry);
Future<void> deleteWeek(String id) =>
(delete(programWeeks)..where((t) => t.id.equals(id))).go();
Future<void> updateWeekNote(String weekId, String note) =>
(update(programWeeks)..where((t) => t.id.equals(weekId)))
.write(ProgramWeeksCompanion(notes: Value(note)));
Future<List<ProgramWorkout>> getWorkouts(String programId) =>
(select(programWorkouts)..where((t) => t.programId.equals(programId)))
.get();
Future<void> insertWorkout(ProgramWorkoutsCompanion entry) =>
into(programWorkouts).insert(entry);
Future<void> updateWorkout(String id, ProgramWorkoutsCompanion entry) =>
(update(programWorkouts)..where((t) => t.id.equals(id))).write(entry);
Future<void> deleteWorkout(String id) =>
(delete(programWorkouts)..where((t) => t.id.equals(id))).go();
Future<void> toggleWorkoutComplete(String id, bool currentStatus) =>
(update(programWorkouts)..where((t) => t.id.equals(id)))
.write(ProgramWorkoutsCompanion(completed: Value(!currentStatus)));
}

View File

@@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'program_dao.dart';
// ignore_for_file: type=lint
mixin _$ProgramDaoMixin on DatabaseAccessor<AppDatabase> {
$ProgramsTable get programs => attachedDatabase.programs;
$ProgramWeeksTable get programWeeks => attachedDatabase.programWeeks;
$ProgramWorkoutsTable get programWorkouts => attachedDatabase.programWorkouts;
}

View File

@@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
part 'training_plan_dao.g.dart';
@DriftAccessor(tables: [TrainingPlans])
class TrainingPlanDao extends DatabaseAccessor<AppDatabase>
with _$TrainingPlanDaoMixin {
TrainingPlanDao(super.db);
Future<List<TrainingPlan>> getAllPlans() => select(trainingPlans).get();
Future<TrainingPlan> getPlanById(String id) =>
(select(trainingPlans)..where((t) => t.id.equals(id))).getSingle();
Future<void> insertPlan(TrainingPlansCompanion entry) =>
into(trainingPlans).insert(entry);
Future<void> updatePlan(String id, TrainingPlansCompanion entry) =>
(update(trainingPlans)..where((t) => t.id.equals(id))).write(entry);
Future<void> deletePlan(String id) =>
(delete(trainingPlans)..where((t) => t.id.equals(id))).go();
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'training_plan_dao.dart';
// ignore_for_file: type=lint
mixin _$TrainingPlanDaoMixin on DatabaseAccessor<AppDatabase> {
$TrainingPlansTable get trainingPlans => attachedDatabase.trainingPlans;
}

View File

@@ -0,0 +1,28 @@
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
import 'package:trainhub_flutter/domain/entities/annotation.dart';
class AnalysisMapper {
AnalysisMapper._();
static AnalysisSessionEntity sessionToEntity(AnalysisSession row) {
return AnalysisSessionEntity(
id: row.id,
name: row.name,
date: row.date,
videoPath: row.videoPath,
);
}
static AnnotationEntity annotationToEntity(Annotation row) {
return AnnotationEntity(
id: row.id,
sessionId: row.sessionId,
startTime: row.startTime,
endTime: row.endTime,
name: row.name,
description: row.description,
color: row.color,
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
class ChatMapper {
ChatMapper._();
static ChatSessionEntity sessionToEntity(ChatSession row) {
return ChatSessionEntity(
id: row.id,
title: row.title,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
);
}
static ChatMessageEntity messageToEntity(ChatMessage row) {
return ChatMessageEntity(
id: row.id,
sessionId: row.sessionId,
role: row.role,
content: row.content,
createdAt: row.createdAt,
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/domain/entities/exercise.dart';
class ExerciseMapper {
ExerciseMapper._();
static ExerciseEntity toEntity(Exercise row) {
return ExerciseEntity(
id: row.id,
name: row.name,
instructions: row.instructions,
enrichment: row.enrichment,
tags: row.tags,
videoUrl: row.videoUrl,
);
}
static ExercisesCompanion toCompanion(ExerciseEntity entity) {
return ExercisesCompanion(
id: Value(entity.id),
name: Value(entity.name),
instructions: Value(entity.instructions),
enrichment: Value(entity.enrichment),
tags: Value(entity.tags),
videoUrl: Value(entity.videoUrl),
);
}
static ExercisesCompanion toUpdateCompanion(ExerciseEntity entity) {
return ExercisesCompanion(
name: Value(entity.name),
instructions: Value(entity.instructions),
enrichment: Value(entity.enrichment),
tags: Value(entity.tags),
videoUrl: Value(entity.videoUrl),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/domain/entities/program.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
class ProgramMapper {
ProgramMapper._();
static ProgramEntity toEntity(Program row) {
return ProgramEntity(
id: row.id,
name: row.name,
createdAt: row.createdAt,
);
}
static ProgramWeekEntity weekToEntity(ProgramWeek row) {
return ProgramWeekEntity(
id: row.id,
programId: row.programId,
position: row.position,
notes: row.notes,
);
}
static ProgramWorkoutEntity workoutToEntity(ProgramWorkout row) {
return ProgramWorkoutEntity(
id: row.id,
weekId: row.weekId,
programId: row.programId,
day: row.day,
type: row.type,
refId: row.refId,
name: row.name,
description: row.description,
completed: row.completed,
);
}
static ProgramWorkoutsCompanion workoutToCompanion(
ProgramWorkoutEntity entity) {
return ProgramWorkoutsCompanion(
id: Value(entity.id),
weekId: Value(entity.weekId),
programId: Value(entity.programId),
day: Value(entity.day),
type: Value(entity.type),
refId: Value(entity.refId),
name: Value(entity.name),
description: Value(entity.description),
completed: Value(entity.completed),
);
}
static ProgramWorkoutsCompanion workoutToUpdateCompanion(
ProgramWorkoutEntity entity) {
return ProgramWorkoutsCompanion(
day: Value(entity.day),
type: Value(entity.type),
refId: Value(entity.refId),
name: Value(entity.name),
description: Value(entity.description),
completed: Value(entity.completed),
weekId: Value(entity.weekId),
);
}
}

View File

@@ -0,0 +1,87 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
import 'package:trainhub_flutter/domain/entities/training_section.dart';
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
class TrainingPlanMapper {
TrainingPlanMapper._();
static TrainingPlanEntity toEntity(TrainingPlan row) {
final List<dynamic> sectionsJson =
row.sections != null && row.sections!.isNotEmpty
? jsonDecode(row.sections!) as List
: [];
return TrainingPlanEntity(
id: row.id,
name: row.name,
sections: sectionsJson
.map((s) => _mapSection(s as Map<String, dynamic>))
.toList(),
);
}
static TrainingSectionEntity _mapSection(Map<String, dynamic> json) {
final exercisesJson = json['exercises'] as List<dynamic>? ?? [];
return TrainingSectionEntity(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
exercises: exercisesJson
.map((e) => _mapExercise(e as Map<String, dynamic>))
.toList(),
);
}
static TrainingExerciseEntity _mapExercise(Map<String, dynamic> json) {
return TrainingExerciseEntity(
instanceId: json['instanceId'] as String? ?? '',
exerciseId: json['exerciseId'] as String? ?? '',
name: json['name'] as String? ?? '',
sets: json['sets'] as int? ?? 3,
value: json['value'] as int? ?? 10,
isTime: json['isTime'] as bool? ?? false,
rest: json['rest'] as int? ?? 60,
);
}
static String sectionsToJson(List<TrainingSectionEntity> sections) {
return jsonEncode(sections.map((s) => _sectionToMap(s)).toList());
}
static Map<String, dynamic> _sectionToMap(TrainingSectionEntity section) {
return {
'id': section.id,
'name': section.name,
'exercises': section.exercises.map((e) => _exerciseToMap(e)).toList(),
};
}
static Map<String, dynamic> _exerciseToMap(TrainingExerciseEntity exercise) {
return {
'instanceId': exercise.instanceId,
'exerciseId': exercise.exerciseId,
'name': exercise.name,
'sets': exercise.sets,
'value': exercise.value,
'isTime': exercise.isTime,
'rest': exercise.rest,
};
}
static TrainingPlansCompanion toInsertCompanion(
String id, String name) {
return TrainingPlansCompanion.insert(
id: id,
name: name,
sections: const Value('[]'),
);
}
static TrainingPlansCompanion toUpdateCompanion(TrainingPlanEntity entity) {
return TrainingPlansCompanion(
name: Value(entity.name),
sections: Value(sectionsToJson(entity.sections)),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/data/database/daos/analysis_dao.dart';
import 'package:trainhub_flutter/data/mappers/analysis_mapper.dart';
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
import 'package:trainhub_flutter/domain/entities/annotation.dart';
import 'package:trainhub_flutter/domain/repositories/analysis_repository.dart';
class AnalysisRepositoryImpl implements AnalysisRepository {
final AnalysisDao _dao;
AnalysisRepositoryImpl(this._dao);
@override
Future<List<AnalysisSessionEntity>> getAllSessions() async {
final rows = await _dao.getAllSessions();
return rows.map(AnalysisMapper.sessionToEntity).toList();
}
@override
Future<AnalysisSessionEntity?> getSession(String id) async {
final row = await _dao.getSession(id);
return row == null ? null : AnalysisMapper.sessionToEntity(row);
}
@override
Future<AnalysisSessionEntity> createSession(
String name, String videoPath) async {
final String id = IdGenerator.generate();
await _dao.insertSession(
AnalysisSessionsCompanion.insert(
id: id,
name: name,
videoPath: Value(videoPath),
date: DateTime.now().toIso8601String(),
),
);
final row = await _dao.getSession(id);
return AnalysisMapper.sessionToEntity(row!);
}
@override
Future<void> deleteSession(String id) async {
await _dao.deleteSession(id);
}
@override
Future<List<AnnotationEntity>> getAnnotations(String sessionId) async {
final rows = await _dao.getAnnotations(sessionId);
return rows.map(AnalysisMapper.annotationToEntity).toList();
}
@override
Future<AnnotationEntity> addAnnotation({
required String sessionId,
required String name,
required String description,
required double startTime,
required double endTime,
required String color,
}) async {
final String id = IdGenerator.generate();
await _dao.insertAnnotation(
AnnotationsCompanion.insert(
id: id,
sessionId: sessionId,
name: Value(name),
description: Value(description),
startTime: startTime,
endTime: endTime,
color: Value(color),
),
);
return AnnotationEntity(
id: id,
sessionId: sessionId,
startTime: startTime,
endTime: endTime,
name: name,
description: description,
color: color,
);
}
@override
Future<void> deleteAnnotation(String id) async {
await _dao.deleteAnnotation(id);
}
}

View File

@@ -0,0 +1,84 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/data/database/daos/chat_dao.dart';
import 'package:trainhub_flutter/data/mappers/chat_mapper.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
import 'package:trainhub_flutter/domain/repositories/chat_repository.dart';
class ChatRepositoryImpl implements ChatRepository {
final ChatDao _dao;
ChatRepositoryImpl(this._dao);
@override
Future<List<ChatSessionEntity>> getAllSessions() async {
final rows = await _dao.getAllSessions();
return rows.map(ChatMapper.sessionToEntity).toList();
}
@override
Future<ChatSessionEntity?> getSession(String id) async {
final row = await _dao.getSession(id);
return row == null ? null : ChatMapper.sessionToEntity(row);
}
@override
Future<ChatSessionEntity> createSession() async {
final String id = IdGenerator.generate();
final String now = DateTime.now().toIso8601String();
await _dao.insertSession(
ChatSessionsCompanion.insert(
id: id,
title: const Value('New Chat'),
createdAt: now,
updatedAt: now,
),
);
final row = await _dao.getSession(id);
return ChatMapper.sessionToEntity(row!);
}
@override
Future<void> deleteSession(String id) async {
await _dao.deleteSession(id);
}
@override
Future<List<ChatMessageEntity>> getMessages(String sessionId) async {
final rows = await _dao.getMessages(sessionId);
return rows.map(ChatMapper.messageToEntity).toList();
}
@override
Future<ChatMessageEntity> addMessage({
required String sessionId,
required String role,
required String content,
}) async {
final String id = IdGenerator.generate();
final String now = DateTime.now().toIso8601String();
await _dao.insertMessage(
ChatMessagesCompanion.insert(
id: id,
sessionId: sessionId,
role: role,
content: content,
createdAt: now,
),
);
return ChatMessageEntity(
id: id,
sessionId: sessionId,
role: role,
content: content,
createdAt: now,
);
}
@override
Future<void> updateSessionTitle(String sessionId, String title) async {
await _dao.updateSessionTitle(sessionId, title);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/data/database/daos/exercise_dao.dart';
import 'package:trainhub_flutter/data/mappers/exercise_mapper.dart';
import 'package:trainhub_flutter/domain/entities/exercise.dart';
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
class ExerciseRepositoryImpl implements ExerciseRepository {
final ExerciseDao _dao;
ExerciseRepositoryImpl(this._dao);
@override
Future<List<ExerciseEntity>> getAll() async {
final rows = await _dao.getAllExercises();
return rows.map(ExerciseMapper.toEntity).toList();
}
@override
Future<ExerciseEntity> create({
required String name,
String? instructions,
String? tags,
String? videoUrl,
}) async {
final String id = IdGenerator.generate();
await _dao.insertExercise(
ExercisesCompanion.insert(
id: id,
name: name,
instructions: Value(instructions),
tags: Value(tags),
videoUrl: Value(videoUrl),
),
);
final row = await _dao.getExerciseById(id);
return ExerciseMapper.toEntity(row);
}
@override
Future<void> update(ExerciseEntity exercise) async {
await _dao.updateExercise(
exercise.id,
ExerciseMapper.toUpdateCompanion(exercise),
);
}
@override
Future<void> delete(String id) async {
await _dao.deleteExercise(id);
}
}

View File

@@ -0,0 +1,151 @@
import 'package:drift/drift.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/data/database/daos/program_dao.dart';
import 'package:trainhub_flutter/data/mappers/program_mapper.dart';
import 'package:trainhub_flutter/domain/entities/program.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/domain/repositories/program_repository.dart';
class ProgramRepositoryImpl implements ProgramRepository {
final ProgramDao _dao;
ProgramRepositoryImpl(this._dao);
@override
Future<List<ProgramEntity>> getAllPrograms() async {
final rows = await _dao.getAllPrograms();
return rows.map(ProgramMapper.toEntity).toList();
}
@override
Future<ProgramEntity?> getProgram(String id) async {
final row = await _dao.getProgram(id);
return row == null ? null : ProgramMapper.toEntity(row);
}
@override
Future<List<ProgramWeekEntity>> getWeeks(String programId) async {
final rows = await _dao.getWeeks(programId);
return rows.map(ProgramMapper.weekToEntity).toList();
}
@override
Future<List<ProgramWorkoutEntity>> getWorkouts(String programId) async {
final rows = await _dao.getWorkouts(programId);
return rows.map(ProgramMapper.workoutToEntity).toList();
}
@override
Future<ProgramEntity> createProgram(String name) async {
final String id = IdGenerator.generate();
await _dao.insertProgram(
ProgramsCompanion.insert(
id: id,
name: name,
createdAt: DateTime.now().toIso8601String(),
),
);
final row = await _dao.getProgram(id);
return ProgramMapper.toEntity(row!);
}
@override
Future<void> deleteProgram(String id) async {
await _dao.deleteProgram(id);
}
@override
Future<void> duplicateProgram(String sourceId) async {
final sourceProgram = await _dao.getProgram(sourceId);
if (sourceProgram == null) return;
final String newId = IdGenerator.generate();
await _dao.insertProgram(
ProgramsCompanion.insert(
id: newId,
name: '${sourceProgram.name} (Copy)',
createdAt: DateTime.now().toIso8601String(),
),
);
final weeks = await _dao.getWeeks(sourceId);
final workouts = await _dao.getWorkouts(sourceId);
for (final week in weeks) {
final String newWeekId = IdGenerator.generate();
await _dao.insertWeek(
ProgramWeeksCompanion.insert(
id: newWeekId,
programId: newId,
position: week.position,
notes: Value(week.notes),
),
);
final weekWorkouts = workouts.where((w) => w.weekId == week.id);
for (final workout in weekWorkouts) {
await _dao.insertWorkout(
ProgramWorkoutsCompanion.insert(
id: IdGenerator.generate(),
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),
),
);
}
}
}
@override
Future<ProgramWeekEntity> addWeek(String programId, int position) async {
final String id = IdGenerator.generate();
await _dao.insertWeek(
ProgramWeeksCompanion.insert(
id: id,
programId: programId,
position: position,
),
);
final weeks = await _dao.getWeeks(programId);
final week = weeks.firstWhere((w) => w.id == id);
return ProgramMapper.weekToEntity(week);
}
@override
Future<void> deleteWeek(String id) async {
await _dao.deleteWeek(id);
}
@override
Future<void> updateWeekNote(String weekId, String note) async {
await _dao.updateWeekNote(weekId, note);
}
@override
Future<ProgramWorkoutEntity> addWorkout(
ProgramWorkoutEntity workout) async {
await _dao.insertWorkout(ProgramMapper.workoutToCompanion(workout));
return workout;
}
@override
Future<void> updateWorkout(ProgramWorkoutEntity workout) async {
await _dao.updateWorkout(
workout.id,
ProgramMapper.workoutToUpdateCompanion(workout),
);
}
@override
Future<void> deleteWorkout(String id) async {
await _dao.deleteWorkout(id);
}
@override
Future<void> toggleWorkoutComplete(String id, bool currentStatus) async {
await _dao.toggleWorkoutComplete(id, currentStatus);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/data/database/daos/training_plan_dao.dart';
import 'package:trainhub_flutter/data/mappers/training_plan_mapper.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
class TrainingPlanRepositoryImpl implements TrainingPlanRepository {
final TrainingPlanDao _dao;
TrainingPlanRepositoryImpl(this._dao);
@override
Future<List<TrainingPlanEntity>> getAll() async {
final rows = await _dao.getAllPlans();
return rows.map(TrainingPlanMapper.toEntity).toList();
}
@override
Future<TrainingPlanEntity> getById(String id) async {
final row = await _dao.getPlanById(id);
return TrainingPlanMapper.toEntity(row);
}
@override
Future<TrainingPlanEntity> create(String name) async {
final String id = IdGenerator.generate();
await _dao.insertPlan(TrainingPlanMapper.toInsertCompanion(id, name));
return getById(id);
}
@override
Future<void> update(TrainingPlanEntity plan) async {
await _dao.updatePlan(plan.id, TrainingPlanMapper.toUpdateCompanion(plan));
}
@override
Future<void> delete(String id) async {
await _dao.deletePlan(id);
}
}

148
lib/database/database.dart Normal file
View File

@@ -0,0 +1,148 @@
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);
});
}

6242
lib/database/database.g.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'analysis_session.freezed.dart';
@freezed
class AnalysisSessionEntity with _$AnalysisSessionEntity {
const factory AnalysisSessionEntity({
required String id,
required String name,
required String date,
String? videoPath,
}) = _AnalysisSessionEntity;
}

View File

@@ -0,0 +1,219 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'analysis_session.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$AnalysisSessionEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get date => throw _privateConstructorUsedError;
String? get videoPath => throw _privateConstructorUsedError;
/// Create a copy of AnalysisSessionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AnalysisSessionEntityCopyWith<AnalysisSessionEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AnalysisSessionEntityCopyWith<$Res> {
factory $AnalysisSessionEntityCopyWith(
AnalysisSessionEntity value,
$Res Function(AnalysisSessionEntity) then,
) = _$AnalysisSessionEntityCopyWithImpl<$Res, AnalysisSessionEntity>;
@useResult
$Res call({String id, String name, String date, String? videoPath});
}
/// @nodoc
class _$AnalysisSessionEntityCopyWithImpl<
$Res,
$Val extends AnalysisSessionEntity
>
implements $AnalysisSessionEntityCopyWith<$Res> {
_$AnalysisSessionEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AnalysisSessionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? date = null,
Object? videoPath = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String,
videoPath: freezed == videoPath
? _value.videoPath
: videoPath // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$AnalysisSessionEntityImplCopyWith<$Res>
implements $AnalysisSessionEntityCopyWith<$Res> {
factory _$$AnalysisSessionEntityImplCopyWith(
_$AnalysisSessionEntityImpl value,
$Res Function(_$AnalysisSessionEntityImpl) then,
) = __$$AnalysisSessionEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, String date, String? videoPath});
}
/// @nodoc
class __$$AnalysisSessionEntityImplCopyWithImpl<$Res>
extends
_$AnalysisSessionEntityCopyWithImpl<$Res, _$AnalysisSessionEntityImpl>
implements _$$AnalysisSessionEntityImplCopyWith<$Res> {
__$$AnalysisSessionEntityImplCopyWithImpl(
_$AnalysisSessionEntityImpl _value,
$Res Function(_$AnalysisSessionEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of AnalysisSessionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? date = null,
Object? videoPath = freezed,
}) {
return _then(
_$AnalysisSessionEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as String,
videoPath: freezed == videoPath
? _value.videoPath
: videoPath // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$AnalysisSessionEntityImpl implements _AnalysisSessionEntity {
const _$AnalysisSessionEntityImpl({
required this.id,
required this.name,
required this.date,
this.videoPath,
});
@override
final String id;
@override
final String name;
@override
final String date;
@override
final String? videoPath;
@override
String toString() {
return 'AnalysisSessionEntity(id: $id, name: $name, date: $date, videoPath: $videoPath)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AnalysisSessionEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.date, date) || other.date == date) &&
(identical(other.videoPath, videoPath) ||
other.videoPath == videoPath));
}
@override
int get hashCode => Object.hash(runtimeType, id, name, date, videoPath);
/// Create a copy of AnalysisSessionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AnalysisSessionEntityImplCopyWith<_$AnalysisSessionEntityImpl>
get copyWith =>
__$$AnalysisSessionEntityImplCopyWithImpl<_$AnalysisSessionEntityImpl>(
this,
_$identity,
);
}
abstract class _AnalysisSessionEntity implements AnalysisSessionEntity {
const factory _AnalysisSessionEntity({
required final String id,
required final String name,
required final String date,
final String? videoPath,
}) = _$AnalysisSessionEntityImpl;
@override
String get id;
@override
String get name;
@override
String get date;
@override
String? get videoPath;
/// Create a copy of AnalysisSessionEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AnalysisSessionEntityImplCopyWith<_$AnalysisSessionEntityImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'annotation.freezed.dart';
@freezed
class AnnotationEntity with _$AnnotationEntity {
const factory AnnotationEntity({
required String id,
required String sessionId,
required double startTime,
required double endTime,
String? name,
String? description,
String? color,
}) = _AnnotationEntity;
}

View File

@@ -0,0 +1,295 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'annotation.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$AnnotationEntity {
String get id => throw _privateConstructorUsedError;
String get sessionId => throw _privateConstructorUsedError;
double get startTime => throw _privateConstructorUsedError;
double get endTime => throw _privateConstructorUsedError;
String? get name => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
String? get color => throw _privateConstructorUsedError;
/// Create a copy of AnnotationEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AnnotationEntityCopyWith<AnnotationEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AnnotationEntityCopyWith<$Res> {
factory $AnnotationEntityCopyWith(
AnnotationEntity value,
$Res Function(AnnotationEntity) then,
) = _$AnnotationEntityCopyWithImpl<$Res, AnnotationEntity>;
@useResult
$Res call({
String id,
String sessionId,
double startTime,
double endTime,
String? name,
String? description,
String? color,
});
}
/// @nodoc
class _$AnnotationEntityCopyWithImpl<$Res, $Val extends AnnotationEntity>
implements $AnnotationEntityCopyWith<$Res> {
_$AnnotationEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AnnotationEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? startTime = null,
Object? endTime = null,
Object? name = freezed,
Object? description = freezed,
Object? color = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
startTime: null == startTime
? _value.startTime
: startTime // ignore: cast_nullable_to_non_nullable
as double,
endTime: null == endTime
? _value.endTime
: endTime // ignore: cast_nullable_to_non_nullable
as double,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
color: freezed == color
? _value.color
: color // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$AnnotationEntityImplCopyWith<$Res>
implements $AnnotationEntityCopyWith<$Res> {
factory _$$AnnotationEntityImplCopyWith(
_$AnnotationEntityImpl value,
$Res Function(_$AnnotationEntityImpl) then,
) = __$$AnnotationEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String sessionId,
double startTime,
double endTime,
String? name,
String? description,
String? color,
});
}
/// @nodoc
class __$$AnnotationEntityImplCopyWithImpl<$Res>
extends _$AnnotationEntityCopyWithImpl<$Res, _$AnnotationEntityImpl>
implements _$$AnnotationEntityImplCopyWith<$Res> {
__$$AnnotationEntityImplCopyWithImpl(
_$AnnotationEntityImpl _value,
$Res Function(_$AnnotationEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of AnnotationEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? startTime = null,
Object? endTime = null,
Object? name = freezed,
Object? description = freezed,
Object? color = freezed,
}) {
return _then(
_$AnnotationEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
startTime: null == startTime
? _value.startTime
: startTime // ignore: cast_nullable_to_non_nullable
as double,
endTime: null == endTime
? _value.endTime
: endTime // ignore: cast_nullable_to_non_nullable
as double,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
color: freezed == color
? _value.color
: color // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$AnnotationEntityImpl implements _AnnotationEntity {
const _$AnnotationEntityImpl({
required this.id,
required this.sessionId,
required this.startTime,
required this.endTime,
this.name,
this.description,
this.color,
});
@override
final String id;
@override
final String sessionId;
@override
final double startTime;
@override
final double endTime;
@override
final String? name;
@override
final String? description;
@override
final String? color;
@override
String toString() {
return 'AnnotationEntity(id: $id, sessionId: $sessionId, startTime: $startTime, endTime: $endTime, name: $name, description: $description, color: $color)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AnnotationEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.sessionId, sessionId) ||
other.sessionId == sessionId) &&
(identical(other.startTime, startTime) ||
other.startTime == startTime) &&
(identical(other.endTime, endTime) || other.endTime == endTime) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.color, color) || other.color == color));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
sessionId,
startTime,
endTime,
name,
description,
color,
);
/// Create a copy of AnnotationEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AnnotationEntityImplCopyWith<_$AnnotationEntityImpl> get copyWith =>
__$$AnnotationEntityImplCopyWithImpl<_$AnnotationEntityImpl>(
this,
_$identity,
);
}
abstract class _AnnotationEntity implements AnnotationEntity {
const factory _AnnotationEntity({
required final String id,
required final String sessionId,
required final double startTime,
required final double endTime,
final String? name,
final String? description,
final String? color,
}) = _$AnnotationEntityImpl;
@override
String get id;
@override
String get sessionId;
@override
double get startTime;
@override
double get endTime;
@override
String? get name;
@override
String? get description;
@override
String? get color;
/// Create a copy of AnnotationEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AnnotationEntityImplCopyWith<_$AnnotationEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_message.freezed.dart';
@freezed
class ChatMessageEntity with _$ChatMessageEntity {
const factory ChatMessageEntity({
required String id,
required String sessionId,
required String role,
required String content,
required String createdAt,
}) = _ChatMessageEntity;
const ChatMessageEntity._();
bool get isUser => role == 'user';
bool get isAssistant => role == 'assistant';
}

View File

@@ -0,0 +1,247 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_message.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ChatMessageEntity {
String get id => throw _privateConstructorUsedError;
String get sessionId => throw _privateConstructorUsedError;
String get role => throw _privateConstructorUsedError;
String get content => throw _privateConstructorUsedError;
String get createdAt => throw _privateConstructorUsedError;
/// Create a copy of ChatMessageEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatMessageEntityCopyWith<ChatMessageEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatMessageEntityCopyWith<$Res> {
factory $ChatMessageEntityCopyWith(
ChatMessageEntity value,
$Res Function(ChatMessageEntity) then,
) = _$ChatMessageEntityCopyWithImpl<$Res, ChatMessageEntity>;
@useResult
$Res call({
String id,
String sessionId,
String role,
String content,
String createdAt,
});
}
/// @nodoc
class _$ChatMessageEntityCopyWithImpl<$Res, $Val extends ChatMessageEntity>
implements $ChatMessageEntityCopyWith<$Res> {
_$ChatMessageEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatMessageEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? role = null,
Object? content = null,
Object? createdAt = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
role: null == role
? _value.role
: role // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ChatMessageEntityImplCopyWith<$Res>
implements $ChatMessageEntityCopyWith<$Res> {
factory _$$ChatMessageEntityImplCopyWith(
_$ChatMessageEntityImpl value,
$Res Function(_$ChatMessageEntityImpl) then,
) = __$$ChatMessageEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String sessionId,
String role,
String content,
String createdAt,
});
}
/// @nodoc
class __$$ChatMessageEntityImplCopyWithImpl<$Res>
extends _$ChatMessageEntityCopyWithImpl<$Res, _$ChatMessageEntityImpl>
implements _$$ChatMessageEntityImplCopyWith<$Res> {
__$$ChatMessageEntityImplCopyWithImpl(
_$ChatMessageEntityImpl _value,
$Res Function(_$ChatMessageEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatMessageEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? role = null,
Object? content = null,
Object? createdAt = null,
}) {
return _then(
_$ChatMessageEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
role: null == role
? _value.role
: role // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$ChatMessageEntityImpl extends _ChatMessageEntity {
const _$ChatMessageEntityImpl({
required this.id,
required this.sessionId,
required this.role,
required this.content,
required this.createdAt,
}) : super._();
@override
final String id;
@override
final String sessionId;
@override
final String role;
@override
final String content;
@override
final String createdAt;
@override
String toString() {
return 'ChatMessageEntity(id: $id, sessionId: $sessionId, role: $role, content: $content, createdAt: $createdAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatMessageEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.sessionId, sessionId) ||
other.sessionId == sessionId) &&
(identical(other.role, role) || other.role == role) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt));
}
@override
int get hashCode =>
Object.hash(runtimeType, id, sessionId, role, content, createdAt);
/// Create a copy of ChatMessageEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatMessageEntityImplCopyWith<_$ChatMessageEntityImpl> get copyWith =>
__$$ChatMessageEntityImplCopyWithImpl<_$ChatMessageEntityImpl>(
this,
_$identity,
);
}
abstract class _ChatMessageEntity extends ChatMessageEntity {
const factory _ChatMessageEntity({
required final String id,
required final String sessionId,
required final String role,
required final String content,
required final String createdAt,
}) = _$ChatMessageEntityImpl;
const _ChatMessageEntity._() : super._();
@override
String get id;
@override
String get sessionId;
@override
String get role;
@override
String get content;
@override
String get createdAt;
/// Create a copy of ChatMessageEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatMessageEntityImplCopyWith<_$ChatMessageEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_session.freezed.dart';
@freezed
class ChatSessionEntity with _$ChatSessionEntity {
const factory ChatSessionEntity({
required String id,
String? title,
required String createdAt,
required String updatedAt,
}) = _ChatSessionEntity;
}

View File

@@ -0,0 +1,215 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_session.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ChatSessionEntity {
String get id => throw _privateConstructorUsedError;
String? get title => throw _privateConstructorUsedError;
String get createdAt => throw _privateConstructorUsedError;
String get updatedAt => throw _privateConstructorUsedError;
/// Create a copy of ChatSessionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatSessionEntityCopyWith<ChatSessionEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatSessionEntityCopyWith<$Res> {
factory $ChatSessionEntityCopyWith(
ChatSessionEntity value,
$Res Function(ChatSessionEntity) then,
) = _$ChatSessionEntityCopyWithImpl<$Res, ChatSessionEntity>;
@useResult
$Res call({String id, String? title, String createdAt, String updatedAt});
}
/// @nodoc
class _$ChatSessionEntityCopyWithImpl<$Res, $Val extends ChatSessionEntity>
implements $ChatSessionEntityCopyWith<$Res> {
_$ChatSessionEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatSessionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = freezed,
Object? createdAt = null,
Object? updatedAt = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ChatSessionEntityImplCopyWith<$Res>
implements $ChatSessionEntityCopyWith<$Res> {
factory _$$ChatSessionEntityImplCopyWith(
_$ChatSessionEntityImpl value,
$Res Function(_$ChatSessionEntityImpl) then,
) = __$$ChatSessionEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String? title, String createdAt, String updatedAt});
}
/// @nodoc
class __$$ChatSessionEntityImplCopyWithImpl<$Res>
extends _$ChatSessionEntityCopyWithImpl<$Res, _$ChatSessionEntityImpl>
implements _$$ChatSessionEntityImplCopyWith<$Res> {
__$$ChatSessionEntityImplCopyWithImpl(
_$ChatSessionEntityImpl _value,
$Res Function(_$ChatSessionEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatSessionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = freezed,
Object? createdAt = null,
Object? updatedAt = null,
}) {
return _then(
_$ChatSessionEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$ChatSessionEntityImpl implements _ChatSessionEntity {
const _$ChatSessionEntityImpl({
required this.id,
this.title,
required this.createdAt,
required this.updatedAt,
});
@override
final String id;
@override
final String? title;
@override
final String createdAt;
@override
final String updatedAt;
@override
String toString() {
return 'ChatSessionEntity(id: $id, title: $title, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatSessionEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
}
@override
int get hashCode => Object.hash(runtimeType, id, title, createdAt, updatedAt);
/// Create a copy of ChatSessionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatSessionEntityImplCopyWith<_$ChatSessionEntityImpl> get copyWith =>
__$$ChatSessionEntityImplCopyWithImpl<_$ChatSessionEntityImpl>(
this,
_$identity,
);
}
abstract class _ChatSessionEntity implements ChatSessionEntity {
const factory _ChatSessionEntity({
required final String id,
final String? title,
required final String createdAt,
required final String updatedAt,
}) = _$ChatSessionEntityImpl;
@override
String get id;
@override
String? get title;
@override
String get createdAt;
@override
String get updatedAt;
/// Create a copy of ChatSessionEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatSessionEntityImplCopyWith<_$ChatSessionEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'exercise.freezed.dart';
@freezed
class ExerciseEntity with _$ExerciseEntity {
const factory ExerciseEntity({
required String id,
required String name,
String? instructions,
String? enrichment,
String? tags,
String? videoUrl,
String? muscleGroup,
}) = _ExerciseEntity;
}

View File

@@ -0,0 +1,296 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'exercise.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ExerciseEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String? get instructions => throw _privateConstructorUsedError;
String? get enrichment => throw _privateConstructorUsedError;
String? get tags => throw _privateConstructorUsedError;
String? get videoUrl => throw _privateConstructorUsedError;
String? get muscleGroup => throw _privateConstructorUsedError;
/// Create a copy of ExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ExerciseEntityCopyWith<ExerciseEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ExerciseEntityCopyWith<$Res> {
factory $ExerciseEntityCopyWith(
ExerciseEntity value,
$Res Function(ExerciseEntity) then,
) = _$ExerciseEntityCopyWithImpl<$Res, ExerciseEntity>;
@useResult
$Res call({
String id,
String name,
String? instructions,
String? enrichment,
String? tags,
String? videoUrl,
String? muscleGroup,
});
}
/// @nodoc
class _$ExerciseEntityCopyWithImpl<$Res, $Val extends ExerciseEntity>
implements $ExerciseEntityCopyWith<$Res> {
_$ExerciseEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? instructions = freezed,
Object? enrichment = freezed,
Object? tags = freezed,
Object? videoUrl = freezed,
Object? muscleGroup = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
instructions: freezed == instructions
? _value.instructions
: instructions // ignore: cast_nullable_to_non_nullable
as String?,
enrichment: freezed == enrichment
? _value.enrichment
: enrichment // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as String?,
videoUrl: freezed == videoUrl
? _value.videoUrl
: videoUrl // ignore: cast_nullable_to_non_nullable
as String?,
muscleGroup: freezed == muscleGroup
? _value.muscleGroup
: muscleGroup // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ExerciseEntityImplCopyWith<$Res>
implements $ExerciseEntityCopyWith<$Res> {
factory _$$ExerciseEntityImplCopyWith(
_$ExerciseEntityImpl value,
$Res Function(_$ExerciseEntityImpl) then,
) = __$$ExerciseEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String name,
String? instructions,
String? enrichment,
String? tags,
String? videoUrl,
String? muscleGroup,
});
}
/// @nodoc
class __$$ExerciseEntityImplCopyWithImpl<$Res>
extends _$ExerciseEntityCopyWithImpl<$Res, _$ExerciseEntityImpl>
implements _$$ExerciseEntityImplCopyWith<$Res> {
__$$ExerciseEntityImplCopyWithImpl(
_$ExerciseEntityImpl _value,
$Res Function(_$ExerciseEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? instructions = freezed,
Object? enrichment = freezed,
Object? tags = freezed,
Object? videoUrl = freezed,
Object? muscleGroup = freezed,
}) {
return _then(
_$ExerciseEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
instructions: freezed == instructions
? _value.instructions
: instructions // ignore: cast_nullable_to_non_nullable
as String?,
enrichment: freezed == enrichment
? _value.enrichment
: enrichment // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as String?,
videoUrl: freezed == videoUrl
? _value.videoUrl
: videoUrl // ignore: cast_nullable_to_non_nullable
as String?,
muscleGroup: freezed == muscleGroup
? _value.muscleGroup
: muscleGroup // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$ExerciseEntityImpl implements _ExerciseEntity {
const _$ExerciseEntityImpl({
required this.id,
required this.name,
this.instructions,
this.enrichment,
this.tags,
this.videoUrl,
this.muscleGroup,
});
@override
final String id;
@override
final String name;
@override
final String? instructions;
@override
final String? enrichment;
@override
final String? tags;
@override
final String? videoUrl;
@override
final String? muscleGroup;
@override
String toString() {
return 'ExerciseEntity(id: $id, name: $name, instructions: $instructions, enrichment: $enrichment, tags: $tags, videoUrl: $videoUrl, muscleGroup: $muscleGroup)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExerciseEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.instructions, instructions) ||
other.instructions == instructions) &&
(identical(other.enrichment, enrichment) ||
other.enrichment == enrichment) &&
(identical(other.tags, tags) || other.tags == tags) &&
(identical(other.videoUrl, videoUrl) ||
other.videoUrl == videoUrl) &&
(identical(other.muscleGroup, muscleGroup) ||
other.muscleGroup == muscleGroup));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
instructions,
enrichment,
tags,
videoUrl,
muscleGroup,
);
/// Create a copy of ExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ExerciseEntityImplCopyWith<_$ExerciseEntityImpl> get copyWith =>
__$$ExerciseEntityImplCopyWithImpl<_$ExerciseEntityImpl>(
this,
_$identity,
);
}
abstract class _ExerciseEntity implements ExerciseEntity {
const factory _ExerciseEntity({
required final String id,
required final String name,
final String? instructions,
final String? enrichment,
final String? tags,
final String? videoUrl,
final String? muscleGroup,
}) = _$ExerciseEntityImpl;
@override
String get id;
@override
String get name;
@override
String? get instructions;
@override
String? get enrichment;
@override
String? get tags;
@override
String? get videoUrl;
@override
String? get muscleGroup;
/// Create a copy of ExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ExerciseEntityImplCopyWith<_$ExerciseEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,12 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'program.freezed.dart';
@freezed
class ProgramEntity with _$ProgramEntity {
const factory ProgramEntity({
required String id,
required String name,
required String createdAt,
}) = _ProgramEntity;
}

View File

@@ -0,0 +1,193 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'program.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ProgramEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get createdAt => throw _privateConstructorUsedError;
/// Create a copy of ProgramEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProgramEntityCopyWith<ProgramEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProgramEntityCopyWith<$Res> {
factory $ProgramEntityCopyWith(
ProgramEntity value,
$Res Function(ProgramEntity) then,
) = _$ProgramEntityCopyWithImpl<$Res, ProgramEntity>;
@useResult
$Res call({String id, String name, String createdAt});
}
/// @nodoc
class _$ProgramEntityCopyWithImpl<$Res, $Val extends ProgramEntity>
implements $ProgramEntityCopyWith<$Res> {
_$ProgramEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProgramEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? createdAt = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ProgramEntityImplCopyWith<$Res>
implements $ProgramEntityCopyWith<$Res> {
factory _$$ProgramEntityImplCopyWith(
_$ProgramEntityImpl value,
$Res Function(_$ProgramEntityImpl) then,
) = __$$ProgramEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, String createdAt});
}
/// @nodoc
class __$$ProgramEntityImplCopyWithImpl<$Res>
extends _$ProgramEntityCopyWithImpl<$Res, _$ProgramEntityImpl>
implements _$$ProgramEntityImplCopyWith<$Res> {
__$$ProgramEntityImplCopyWithImpl(
_$ProgramEntityImpl _value,
$Res Function(_$ProgramEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ProgramEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? createdAt = null,
}) {
return _then(
_$ProgramEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$ProgramEntityImpl implements _ProgramEntity {
const _$ProgramEntityImpl({
required this.id,
required this.name,
required this.createdAt,
});
@override
final String id;
@override
final String name;
@override
final String createdAt;
@override
String toString() {
return 'ProgramEntity(id: $id, name: $name, createdAt: $createdAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProgramEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt));
}
@override
int get hashCode => Object.hash(runtimeType, id, name, createdAt);
/// Create a copy of ProgramEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProgramEntityImplCopyWith<_$ProgramEntityImpl> get copyWith =>
__$$ProgramEntityImplCopyWithImpl<_$ProgramEntityImpl>(this, _$identity);
}
abstract class _ProgramEntity implements ProgramEntity {
const factory _ProgramEntity({
required final String id,
required final String name,
required final String createdAt,
}) = _$ProgramEntityImpl;
@override
String get id;
@override
String get name;
@override
String get createdAt;
/// Create a copy of ProgramEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProgramEntityImplCopyWith<_$ProgramEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'program_week.freezed.dart';
@freezed
class ProgramWeekEntity with _$ProgramWeekEntity {
const factory ProgramWeekEntity({
required String id,
required String programId,
required int position,
String? notes,
}) = _ProgramWeekEntity;
}

View File

@@ -0,0 +1,215 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'program_week.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ProgramWeekEntity {
String get id => throw _privateConstructorUsedError;
String get programId => throw _privateConstructorUsedError;
int get position => throw _privateConstructorUsedError;
String? get notes => throw _privateConstructorUsedError;
/// Create a copy of ProgramWeekEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProgramWeekEntityCopyWith<ProgramWeekEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProgramWeekEntityCopyWith<$Res> {
factory $ProgramWeekEntityCopyWith(
ProgramWeekEntity value,
$Res Function(ProgramWeekEntity) then,
) = _$ProgramWeekEntityCopyWithImpl<$Res, ProgramWeekEntity>;
@useResult
$Res call({String id, String programId, int position, String? notes});
}
/// @nodoc
class _$ProgramWeekEntityCopyWithImpl<$Res, $Val extends ProgramWeekEntity>
implements $ProgramWeekEntityCopyWith<$Res> {
_$ProgramWeekEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProgramWeekEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? programId = null,
Object? position = null,
Object? notes = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
programId: null == programId
? _value.programId
: programId // ignore: cast_nullable_to_non_nullable
as String,
position: null == position
? _value.position
: position // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ProgramWeekEntityImplCopyWith<$Res>
implements $ProgramWeekEntityCopyWith<$Res> {
factory _$$ProgramWeekEntityImplCopyWith(
_$ProgramWeekEntityImpl value,
$Res Function(_$ProgramWeekEntityImpl) then,
) = __$$ProgramWeekEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String programId, int position, String? notes});
}
/// @nodoc
class __$$ProgramWeekEntityImplCopyWithImpl<$Res>
extends _$ProgramWeekEntityCopyWithImpl<$Res, _$ProgramWeekEntityImpl>
implements _$$ProgramWeekEntityImplCopyWith<$Res> {
__$$ProgramWeekEntityImplCopyWithImpl(
_$ProgramWeekEntityImpl _value,
$Res Function(_$ProgramWeekEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ProgramWeekEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? programId = null,
Object? position = null,
Object? notes = freezed,
}) {
return _then(
_$ProgramWeekEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
programId: null == programId
? _value.programId
: programId // ignore: cast_nullable_to_non_nullable
as String,
position: null == position
? _value.position
: position // ignore: cast_nullable_to_non_nullable
as int,
notes: freezed == notes
? _value.notes
: notes // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$ProgramWeekEntityImpl implements _ProgramWeekEntity {
const _$ProgramWeekEntityImpl({
required this.id,
required this.programId,
required this.position,
this.notes,
});
@override
final String id;
@override
final String programId;
@override
final int position;
@override
final String? notes;
@override
String toString() {
return 'ProgramWeekEntity(id: $id, programId: $programId, position: $position, notes: $notes)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProgramWeekEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.programId, programId) ||
other.programId == programId) &&
(identical(other.position, position) ||
other.position == position) &&
(identical(other.notes, notes) || other.notes == notes));
}
@override
int get hashCode => Object.hash(runtimeType, id, programId, position, notes);
/// Create a copy of ProgramWeekEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProgramWeekEntityImplCopyWith<_$ProgramWeekEntityImpl> get copyWith =>
__$$ProgramWeekEntityImplCopyWithImpl<_$ProgramWeekEntityImpl>(
this,
_$identity,
);
}
abstract class _ProgramWeekEntity implements ProgramWeekEntity {
const factory _ProgramWeekEntity({
required final String id,
required final String programId,
required final int position,
final String? notes,
}) = _$ProgramWeekEntityImpl;
@override
String get id;
@override
String get programId;
@override
int get position;
@override
String? get notes;
/// Create a copy of ProgramWeekEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProgramWeekEntityImplCopyWith<_$ProgramWeekEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'program_workout.freezed.dart';
@freezed
class ProgramWorkoutEntity with _$ProgramWorkoutEntity {
const factory ProgramWorkoutEntity({
required String id,
required String weekId,
required String programId,
required String day,
required String type,
String? refId,
String? name,
String? description,
@Default(false) bool completed,
}) = _ProgramWorkoutEntity;
}

View File

@@ -0,0 +1,342 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'program_workout.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ProgramWorkoutEntity {
String get id => throw _privateConstructorUsedError;
String get weekId => throw _privateConstructorUsedError;
String get programId => throw _privateConstructorUsedError;
String get day => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
String? get refId => throw _privateConstructorUsedError;
String? get name => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
bool get completed => throw _privateConstructorUsedError;
/// Create a copy of ProgramWorkoutEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProgramWorkoutEntityCopyWith<ProgramWorkoutEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProgramWorkoutEntityCopyWith<$Res> {
factory $ProgramWorkoutEntityCopyWith(
ProgramWorkoutEntity value,
$Res Function(ProgramWorkoutEntity) then,
) = _$ProgramWorkoutEntityCopyWithImpl<$Res, ProgramWorkoutEntity>;
@useResult
$Res call({
String id,
String weekId,
String programId,
String day,
String type,
String? refId,
String? name,
String? description,
bool completed,
});
}
/// @nodoc
class _$ProgramWorkoutEntityCopyWithImpl<
$Res,
$Val extends ProgramWorkoutEntity
>
implements $ProgramWorkoutEntityCopyWith<$Res> {
_$ProgramWorkoutEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ProgramWorkoutEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? weekId = null,
Object? programId = null,
Object? day = null,
Object? type = null,
Object? refId = freezed,
Object? name = freezed,
Object? description = freezed,
Object? completed = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
weekId: null == weekId
? _value.weekId
: weekId // ignore: cast_nullable_to_non_nullable
as String,
programId: null == programId
? _value.programId
: programId // ignore: cast_nullable_to_non_nullable
as String,
day: null == day
? _value.day
: day // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
refId: freezed == refId
? _value.refId
: refId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
completed: null == completed
? _value.completed
: completed // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ProgramWorkoutEntityImplCopyWith<$Res>
implements $ProgramWorkoutEntityCopyWith<$Res> {
factory _$$ProgramWorkoutEntityImplCopyWith(
_$ProgramWorkoutEntityImpl value,
$Res Function(_$ProgramWorkoutEntityImpl) then,
) = __$$ProgramWorkoutEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String weekId,
String programId,
String day,
String type,
String? refId,
String? name,
String? description,
bool completed,
});
}
/// @nodoc
class __$$ProgramWorkoutEntityImplCopyWithImpl<$Res>
extends _$ProgramWorkoutEntityCopyWithImpl<$Res, _$ProgramWorkoutEntityImpl>
implements _$$ProgramWorkoutEntityImplCopyWith<$Res> {
__$$ProgramWorkoutEntityImplCopyWithImpl(
_$ProgramWorkoutEntityImpl _value,
$Res Function(_$ProgramWorkoutEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of ProgramWorkoutEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? weekId = null,
Object? programId = null,
Object? day = null,
Object? type = null,
Object? refId = freezed,
Object? name = freezed,
Object? description = freezed,
Object? completed = null,
}) {
return _then(
_$ProgramWorkoutEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
weekId: null == weekId
? _value.weekId
: weekId // ignore: cast_nullable_to_non_nullable
as String,
programId: null == programId
? _value.programId
: programId // ignore: cast_nullable_to_non_nullable
as String,
day: null == day
? _value.day
: day // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
refId: freezed == refId
? _value.refId
: refId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
completed: null == completed
? _value.completed
: completed // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$ProgramWorkoutEntityImpl implements _ProgramWorkoutEntity {
const _$ProgramWorkoutEntityImpl({
required this.id,
required this.weekId,
required this.programId,
required this.day,
required this.type,
this.refId,
this.name,
this.description,
this.completed = false,
});
@override
final String id;
@override
final String weekId;
@override
final String programId;
@override
final String day;
@override
final String type;
@override
final String? refId;
@override
final String? name;
@override
final String? description;
@override
@JsonKey()
final bool completed;
@override
String toString() {
return 'ProgramWorkoutEntity(id: $id, weekId: $weekId, programId: $programId, day: $day, type: $type, refId: $refId, name: $name, description: $description, completed: $completed)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProgramWorkoutEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.weekId, weekId) || other.weekId == weekId) &&
(identical(other.programId, programId) ||
other.programId == programId) &&
(identical(other.day, day) || other.day == day) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.refId, refId) || other.refId == refId) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.completed, completed) ||
other.completed == completed));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
weekId,
programId,
day,
type,
refId,
name,
description,
completed,
);
/// Create a copy of ProgramWorkoutEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProgramWorkoutEntityImplCopyWith<_$ProgramWorkoutEntityImpl>
get copyWith =>
__$$ProgramWorkoutEntityImplCopyWithImpl<_$ProgramWorkoutEntityImpl>(
this,
_$identity,
);
}
abstract class _ProgramWorkoutEntity implements ProgramWorkoutEntity {
const factory _ProgramWorkoutEntity({
required final String id,
required final String weekId,
required final String programId,
required final String day,
required final String type,
final String? refId,
final String? name,
final String? description,
final bool completed,
}) = _$ProgramWorkoutEntityImpl;
@override
String get id;
@override
String get weekId;
@override
String get programId;
@override
String get day;
@override
String get type;
@override
String? get refId;
@override
String? get name;
@override
String? get description;
@override
bool get completed;
/// Create a copy of ProgramWorkoutEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProgramWorkoutEntityImplCopyWith<_$ProgramWorkoutEntityImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'training_exercise.freezed.dart';
@freezed
class TrainingExerciseEntity with _$TrainingExerciseEntity {
const factory TrainingExerciseEntity({
required String instanceId,
required String exerciseId,
required String name,
@Default(3) int sets,
@Default(10) int value,
@Default(false) bool isTime,
@Default(60) int rest,
}) = _TrainingExerciseEntity;
}

View File

@@ -0,0 +1,303 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'training_exercise.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$TrainingExerciseEntity {
String get instanceId => throw _privateConstructorUsedError;
String get exerciseId => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
int get sets => throw _privateConstructorUsedError;
int get value => throw _privateConstructorUsedError;
bool get isTime => throw _privateConstructorUsedError;
int get rest => throw _privateConstructorUsedError;
/// Create a copy of TrainingExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrainingExerciseEntityCopyWith<TrainingExerciseEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrainingExerciseEntityCopyWith<$Res> {
factory $TrainingExerciseEntityCopyWith(
TrainingExerciseEntity value,
$Res Function(TrainingExerciseEntity) then,
) = _$TrainingExerciseEntityCopyWithImpl<$Res, TrainingExerciseEntity>;
@useResult
$Res call({
String instanceId,
String exerciseId,
String name,
int sets,
int value,
bool isTime,
int rest,
});
}
/// @nodoc
class _$TrainingExerciseEntityCopyWithImpl<
$Res,
$Val extends TrainingExerciseEntity
>
implements $TrainingExerciseEntityCopyWith<$Res> {
_$TrainingExerciseEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrainingExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? instanceId = null,
Object? exerciseId = null,
Object? name = null,
Object? sets = null,
Object? value = null,
Object? isTime = null,
Object? rest = null,
}) {
return _then(
_value.copyWith(
instanceId: null == instanceId
? _value.instanceId
: instanceId // ignore: cast_nullable_to_non_nullable
as String,
exerciseId: null == exerciseId
? _value.exerciseId
: exerciseId // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
sets: null == sets
? _value.sets
: sets // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
isTime: null == isTime
? _value.isTime
: isTime // ignore: cast_nullable_to_non_nullable
as bool,
rest: null == rest
? _value.rest
: rest // ignore: cast_nullable_to_non_nullable
as int,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$TrainingExerciseEntityImplCopyWith<$Res>
implements $TrainingExerciseEntityCopyWith<$Res> {
factory _$$TrainingExerciseEntityImplCopyWith(
_$TrainingExerciseEntityImpl value,
$Res Function(_$TrainingExerciseEntityImpl) then,
) = __$$TrainingExerciseEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String instanceId,
String exerciseId,
String name,
int sets,
int value,
bool isTime,
int rest,
});
}
/// @nodoc
class __$$TrainingExerciseEntityImplCopyWithImpl<$Res>
extends
_$TrainingExerciseEntityCopyWithImpl<$Res, _$TrainingExerciseEntityImpl>
implements _$$TrainingExerciseEntityImplCopyWith<$Res> {
__$$TrainingExerciseEntityImplCopyWithImpl(
_$TrainingExerciseEntityImpl _value,
$Res Function(_$TrainingExerciseEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of TrainingExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? instanceId = null,
Object? exerciseId = null,
Object? name = null,
Object? sets = null,
Object? value = null,
Object? isTime = null,
Object? rest = null,
}) {
return _then(
_$TrainingExerciseEntityImpl(
instanceId: null == instanceId
? _value.instanceId
: instanceId // ignore: cast_nullable_to_non_nullable
as String,
exerciseId: null == exerciseId
? _value.exerciseId
: exerciseId // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
sets: null == sets
? _value.sets
: sets // ignore: cast_nullable_to_non_nullable
as int,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as int,
isTime: null == isTime
? _value.isTime
: isTime // ignore: cast_nullable_to_non_nullable
as bool,
rest: null == rest
? _value.rest
: rest // ignore: cast_nullable_to_non_nullable
as int,
),
);
}
}
/// @nodoc
class _$TrainingExerciseEntityImpl implements _TrainingExerciseEntity {
const _$TrainingExerciseEntityImpl({
required this.instanceId,
required this.exerciseId,
required this.name,
this.sets = 3,
this.value = 10,
this.isTime = false,
this.rest = 60,
});
@override
final String instanceId;
@override
final String exerciseId;
@override
final String name;
@override
@JsonKey()
final int sets;
@override
@JsonKey()
final int value;
@override
@JsonKey()
final bool isTime;
@override
@JsonKey()
final int rest;
@override
String toString() {
return 'TrainingExerciseEntity(instanceId: $instanceId, exerciseId: $exerciseId, name: $name, sets: $sets, value: $value, isTime: $isTime, rest: $rest)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrainingExerciseEntityImpl &&
(identical(other.instanceId, instanceId) ||
other.instanceId == instanceId) &&
(identical(other.exerciseId, exerciseId) ||
other.exerciseId == exerciseId) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.sets, sets) || other.sets == sets) &&
(identical(other.value, value) || other.value == value) &&
(identical(other.isTime, isTime) || other.isTime == isTime) &&
(identical(other.rest, rest) || other.rest == rest));
}
@override
int get hashCode => Object.hash(
runtimeType,
instanceId,
exerciseId,
name,
sets,
value,
isTime,
rest,
);
/// Create a copy of TrainingExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrainingExerciseEntityImplCopyWith<_$TrainingExerciseEntityImpl>
get copyWith =>
__$$TrainingExerciseEntityImplCopyWithImpl<_$TrainingExerciseEntityImpl>(
this,
_$identity,
);
}
abstract class _TrainingExerciseEntity implements TrainingExerciseEntity {
const factory _TrainingExerciseEntity({
required final String instanceId,
required final String exerciseId,
required final String name,
final int sets,
final int value,
final bool isTime,
final int rest,
}) = _$TrainingExerciseEntityImpl;
@override
String get instanceId;
@override
String get exerciseId;
@override
String get name;
@override
int get sets;
@override
int get value;
@override
bool get isTime;
@override
int get rest;
/// Create a copy of TrainingExerciseEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrainingExerciseEntityImplCopyWith<_$TrainingExerciseEntityImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/training_section.dart';
part 'training_plan.freezed.dart';
@freezed
class TrainingPlanEntity with _$TrainingPlanEntity {
const factory TrainingPlanEntity({
required String id,
required String name,
@Default([]) List<TrainingSectionEntity> sections,
}) = _TrainingPlanEntity;
const TrainingPlanEntity._();
int get totalExercises =>
sections.fold(0, (sum, s) => sum + s.exercises.length);
}

View File

@@ -0,0 +1,201 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'training_plan.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$TrainingPlanEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
List<TrainingSectionEntity> get sections =>
throw _privateConstructorUsedError;
/// Create a copy of TrainingPlanEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrainingPlanEntityCopyWith<TrainingPlanEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrainingPlanEntityCopyWith<$Res> {
factory $TrainingPlanEntityCopyWith(
TrainingPlanEntity value,
$Res Function(TrainingPlanEntity) then,
) = _$TrainingPlanEntityCopyWithImpl<$Res, TrainingPlanEntity>;
@useResult
$Res call({String id, String name, List<TrainingSectionEntity> sections});
}
/// @nodoc
class _$TrainingPlanEntityCopyWithImpl<$Res, $Val extends TrainingPlanEntity>
implements $TrainingPlanEntityCopyWith<$Res> {
_$TrainingPlanEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrainingPlanEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? id = null, Object? name = null, Object? sections = null}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
sections: null == sections
? _value.sections
: sections // ignore: cast_nullable_to_non_nullable
as List<TrainingSectionEntity>,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$TrainingPlanEntityImplCopyWith<$Res>
implements $TrainingPlanEntityCopyWith<$Res> {
factory _$$TrainingPlanEntityImplCopyWith(
_$TrainingPlanEntityImpl value,
$Res Function(_$TrainingPlanEntityImpl) then,
) = __$$TrainingPlanEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, List<TrainingSectionEntity> sections});
}
/// @nodoc
class __$$TrainingPlanEntityImplCopyWithImpl<$Res>
extends _$TrainingPlanEntityCopyWithImpl<$Res, _$TrainingPlanEntityImpl>
implements _$$TrainingPlanEntityImplCopyWith<$Res> {
__$$TrainingPlanEntityImplCopyWithImpl(
_$TrainingPlanEntityImpl _value,
$Res Function(_$TrainingPlanEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of TrainingPlanEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? id = null, Object? name = null, Object? sections = null}) {
return _then(
_$TrainingPlanEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
sections: null == sections
? _value._sections
: sections // ignore: cast_nullable_to_non_nullable
as List<TrainingSectionEntity>,
),
);
}
}
/// @nodoc
class _$TrainingPlanEntityImpl extends _TrainingPlanEntity {
const _$TrainingPlanEntityImpl({
required this.id,
required this.name,
final List<TrainingSectionEntity> sections = const [],
}) : _sections = sections,
super._();
@override
final String id;
@override
final String name;
final List<TrainingSectionEntity> _sections;
@override
@JsonKey()
List<TrainingSectionEntity> get sections {
if (_sections is EqualUnmodifiableListView) return _sections;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sections);
}
@override
String toString() {
return 'TrainingPlanEntity(id: $id, name: $name, sections: $sections)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrainingPlanEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
const DeepCollectionEquality().equals(other._sections, _sections));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
const DeepCollectionEquality().hash(_sections),
);
/// Create a copy of TrainingPlanEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrainingPlanEntityImplCopyWith<_$TrainingPlanEntityImpl> get copyWith =>
__$$TrainingPlanEntityImplCopyWithImpl<_$TrainingPlanEntityImpl>(
this,
_$identity,
);
}
abstract class _TrainingPlanEntity extends TrainingPlanEntity {
const factory _TrainingPlanEntity({
required final String id,
required final String name,
final List<TrainingSectionEntity> sections,
}) = _$TrainingPlanEntityImpl;
const _TrainingPlanEntity._() : super._();
@override
String get id;
@override
String get name;
@override
List<TrainingSectionEntity> get sections;
/// Create a copy of TrainingPlanEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrainingPlanEntityImplCopyWith<_$TrainingPlanEntityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
part 'training_section.freezed.dart';
@freezed
class TrainingSectionEntity with _$TrainingSectionEntity {
const factory TrainingSectionEntity({
required String id,
required String name,
@Default([]) List<TrainingExerciseEntity> exercises,
}) = _TrainingSectionEntity;
}

View File

@@ -0,0 +1,215 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'training_section.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$TrainingSectionEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
List<TrainingExerciseEntity> get exercises =>
throw _privateConstructorUsedError;
/// Create a copy of TrainingSectionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrainingSectionEntityCopyWith<TrainingSectionEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrainingSectionEntityCopyWith<$Res> {
factory $TrainingSectionEntityCopyWith(
TrainingSectionEntity value,
$Res Function(TrainingSectionEntity) then,
) = _$TrainingSectionEntityCopyWithImpl<$Res, TrainingSectionEntity>;
@useResult
$Res call({String id, String name, List<TrainingExerciseEntity> exercises});
}
/// @nodoc
class _$TrainingSectionEntityCopyWithImpl<
$Res,
$Val extends TrainingSectionEntity
>
implements $TrainingSectionEntityCopyWith<$Res> {
_$TrainingSectionEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrainingSectionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? exercises = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
exercises: null == exercises
? _value.exercises
: exercises // ignore: cast_nullable_to_non_nullable
as List<TrainingExerciseEntity>,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$TrainingSectionEntityImplCopyWith<$Res>
implements $TrainingSectionEntityCopyWith<$Res> {
factory _$$TrainingSectionEntityImplCopyWith(
_$TrainingSectionEntityImpl value,
$Res Function(_$TrainingSectionEntityImpl) then,
) = __$$TrainingSectionEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, List<TrainingExerciseEntity> exercises});
}
/// @nodoc
class __$$TrainingSectionEntityImplCopyWithImpl<$Res>
extends
_$TrainingSectionEntityCopyWithImpl<$Res, _$TrainingSectionEntityImpl>
implements _$$TrainingSectionEntityImplCopyWith<$Res> {
__$$TrainingSectionEntityImplCopyWithImpl(
_$TrainingSectionEntityImpl _value,
$Res Function(_$TrainingSectionEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of TrainingSectionEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? exercises = null,
}) {
return _then(
_$TrainingSectionEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
exercises: null == exercises
? _value._exercises
: exercises // ignore: cast_nullable_to_non_nullable
as List<TrainingExerciseEntity>,
),
);
}
}
/// @nodoc
class _$TrainingSectionEntityImpl implements _TrainingSectionEntity {
const _$TrainingSectionEntityImpl({
required this.id,
required this.name,
final List<TrainingExerciseEntity> exercises = const [],
}) : _exercises = exercises;
@override
final String id;
@override
final String name;
final List<TrainingExerciseEntity> _exercises;
@override
@JsonKey()
List<TrainingExerciseEntity> get exercises {
if (_exercises is EqualUnmodifiableListView) return _exercises;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_exercises);
}
@override
String toString() {
return 'TrainingSectionEntity(id: $id, name: $name, exercises: $exercises)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrainingSectionEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
const DeepCollectionEquality().equals(
other._exercises,
_exercises,
));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
const DeepCollectionEquality().hash(_exercises),
);
/// Create a copy of TrainingSectionEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrainingSectionEntityImplCopyWith<_$TrainingSectionEntityImpl>
get copyWith =>
__$$TrainingSectionEntityImplCopyWithImpl<_$TrainingSectionEntityImpl>(
this,
_$identity,
);
}
abstract class _TrainingSectionEntity implements TrainingSectionEntity {
const factory _TrainingSectionEntity({
required final String id,
required final String name,
final List<TrainingExerciseEntity> exercises,
}) = _$TrainingSectionEntityImpl;
@override
String get id;
@override
String get name;
@override
List<TrainingExerciseEntity> get exercises;
/// Create a copy of TrainingSectionEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrainingSectionEntityImplCopyWith<_$TrainingSectionEntityImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
part 'workout_activity.freezed.dart';
@freezed
class WorkoutActivityEntity with _$WorkoutActivityEntity {
const factory WorkoutActivityEntity({
required String id,
required String name,
required String type,
required int duration,
TrainingExerciseEntity? originalExercise,
String? sectionName,
int? setIndex,
int? totalSets,
}) = _WorkoutActivityEntity;
const WorkoutActivityEntity._();
bool get isRest => type == 'rest';
bool get isWork => type == 'work';
bool get isTimeBased => duration > 0;
}

View File

@@ -0,0 +1,346 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'workout_activity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$WorkoutActivityEntity {
String get id => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
int get duration => throw _privateConstructorUsedError;
TrainingExerciseEntity? get originalExercise =>
throw _privateConstructorUsedError;
String? get sectionName => throw _privateConstructorUsedError;
int? get setIndex => throw _privateConstructorUsedError;
int? get totalSets => throw _privateConstructorUsedError;
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$WorkoutActivityEntityCopyWith<WorkoutActivityEntity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $WorkoutActivityEntityCopyWith<$Res> {
factory $WorkoutActivityEntityCopyWith(
WorkoutActivityEntity value,
$Res Function(WorkoutActivityEntity) then,
) = _$WorkoutActivityEntityCopyWithImpl<$Res, WorkoutActivityEntity>;
@useResult
$Res call({
String id,
String name,
String type,
int duration,
TrainingExerciseEntity? originalExercise,
String? sectionName,
int? setIndex,
int? totalSets,
});
$TrainingExerciseEntityCopyWith<$Res>? get originalExercise;
}
/// @nodoc
class _$WorkoutActivityEntityCopyWithImpl<
$Res,
$Val extends WorkoutActivityEntity
>
implements $WorkoutActivityEntityCopyWith<$Res> {
_$WorkoutActivityEntityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? type = null,
Object? duration = null,
Object? originalExercise = freezed,
Object? sectionName = freezed,
Object? setIndex = freezed,
Object? totalSets = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
originalExercise: freezed == originalExercise
? _value.originalExercise
: originalExercise // ignore: cast_nullable_to_non_nullable
as TrainingExerciseEntity?,
sectionName: freezed == sectionName
? _value.sectionName
: sectionName // ignore: cast_nullable_to_non_nullable
as String?,
setIndex: freezed == setIndex
? _value.setIndex
: setIndex // ignore: cast_nullable_to_non_nullable
as int?,
totalSets: freezed == totalSets
? _value.totalSets
: totalSets // ignore: cast_nullable_to_non_nullable
as int?,
)
as $Val,
);
}
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$TrainingExerciseEntityCopyWith<$Res>? get originalExercise {
if (_value.originalExercise == null) {
return null;
}
return $TrainingExerciseEntityCopyWith<$Res>(_value.originalExercise!, (
value,
) {
return _then(_value.copyWith(originalExercise: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$WorkoutActivityEntityImplCopyWith<$Res>
implements $WorkoutActivityEntityCopyWith<$Res> {
factory _$$WorkoutActivityEntityImplCopyWith(
_$WorkoutActivityEntityImpl value,
$Res Function(_$WorkoutActivityEntityImpl) then,
) = __$$WorkoutActivityEntityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String name,
String type,
int duration,
TrainingExerciseEntity? originalExercise,
String? sectionName,
int? setIndex,
int? totalSets,
});
@override
$TrainingExerciseEntityCopyWith<$Res>? get originalExercise;
}
/// @nodoc
class __$$WorkoutActivityEntityImplCopyWithImpl<$Res>
extends
_$WorkoutActivityEntityCopyWithImpl<$Res, _$WorkoutActivityEntityImpl>
implements _$$WorkoutActivityEntityImplCopyWith<$Res> {
__$$WorkoutActivityEntityImplCopyWithImpl(
_$WorkoutActivityEntityImpl _value,
$Res Function(_$WorkoutActivityEntityImpl) _then,
) : super(_value, _then);
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? type = null,
Object? duration = null,
Object? originalExercise = freezed,
Object? sectionName = freezed,
Object? setIndex = freezed,
Object? totalSets = freezed,
}) {
return _then(
_$WorkoutActivityEntityImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
originalExercise: freezed == originalExercise
? _value.originalExercise
: originalExercise // ignore: cast_nullable_to_non_nullable
as TrainingExerciseEntity?,
sectionName: freezed == sectionName
? _value.sectionName
: sectionName // ignore: cast_nullable_to_non_nullable
as String?,
setIndex: freezed == setIndex
? _value.setIndex
: setIndex // ignore: cast_nullable_to_non_nullable
as int?,
totalSets: freezed == totalSets
? _value.totalSets
: totalSets // ignore: cast_nullable_to_non_nullable
as int?,
),
);
}
}
/// @nodoc
class _$WorkoutActivityEntityImpl extends _WorkoutActivityEntity {
const _$WorkoutActivityEntityImpl({
required this.id,
required this.name,
required this.type,
required this.duration,
this.originalExercise,
this.sectionName,
this.setIndex,
this.totalSets,
}) : super._();
@override
final String id;
@override
final String name;
@override
final String type;
@override
final int duration;
@override
final TrainingExerciseEntity? originalExercise;
@override
final String? sectionName;
@override
final int? setIndex;
@override
final int? totalSets;
@override
String toString() {
return 'WorkoutActivityEntity(id: $id, name: $name, type: $type, duration: $duration, originalExercise: $originalExercise, sectionName: $sectionName, setIndex: $setIndex, totalSets: $totalSets)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$WorkoutActivityEntityImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.duration, duration) ||
other.duration == duration) &&
(identical(other.originalExercise, originalExercise) ||
other.originalExercise == originalExercise) &&
(identical(other.sectionName, sectionName) ||
other.sectionName == sectionName) &&
(identical(other.setIndex, setIndex) ||
other.setIndex == setIndex) &&
(identical(other.totalSets, totalSets) ||
other.totalSets == totalSets));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
type,
duration,
originalExercise,
sectionName,
setIndex,
totalSets,
);
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$WorkoutActivityEntityImplCopyWith<_$WorkoutActivityEntityImpl>
get copyWith =>
__$$WorkoutActivityEntityImplCopyWithImpl<_$WorkoutActivityEntityImpl>(
this,
_$identity,
);
}
abstract class _WorkoutActivityEntity extends WorkoutActivityEntity {
const factory _WorkoutActivityEntity({
required final String id,
required final String name,
required final String type,
required final int duration,
final TrainingExerciseEntity? originalExercise,
final String? sectionName,
final int? setIndex,
final int? totalSets,
}) = _$WorkoutActivityEntityImpl;
const _WorkoutActivityEntity._() : super._();
@override
String get id;
@override
String get name;
@override
String get type;
@override
int get duration;
@override
TrainingExerciseEntity? get originalExercise;
@override
String? get sectionName;
@override
int? get setIndex;
@override
int? get totalSets;
/// Create a copy of WorkoutActivityEntity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$WorkoutActivityEntityImplCopyWith<_$WorkoutActivityEntityImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,19 @@
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
import 'package:trainhub_flutter/domain/entities/annotation.dart';
abstract class AnalysisRepository {
Future<List<AnalysisSessionEntity>> getAllSessions();
Future<AnalysisSessionEntity?> getSession(String id);
Future<AnalysisSessionEntity> createSession(String name, String videoPath);
Future<void> deleteSession(String id);
Future<List<AnnotationEntity>> getAnnotations(String sessionId);
Future<AnnotationEntity> addAnnotation({
required String sessionId,
required String name,
required String description,
required double startTime,
required double endTime,
required String color,
});
Future<void> deleteAnnotation(String id);
}

View File

@@ -0,0 +1,16 @@
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
abstract class ChatRepository {
Future<List<ChatSessionEntity>> getAllSessions();
Future<ChatSessionEntity?> getSession(String id);
Future<ChatSessionEntity> createSession();
Future<void> deleteSession(String id);
Future<List<ChatMessageEntity>> getMessages(String sessionId);
Future<ChatMessageEntity> addMessage({
required String sessionId,
required String role,
required String content,
});
Future<void> updateSessionTitle(String sessionId, String title);
}

View File

@@ -0,0 +1,13 @@
import 'package:trainhub_flutter/domain/entities/exercise.dart';
abstract class ExerciseRepository {
Future<List<ExerciseEntity>> getAll();
Future<ExerciseEntity> create({
required String name,
String? instructions,
String? tags,
String? videoUrl,
});
Future<void> update(ExerciseEntity exercise);
Future<void> delete(String id);
}

View File

@@ -0,0 +1,20 @@
import 'package:trainhub_flutter/domain/entities/program.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
abstract class ProgramRepository {
Future<List<ProgramEntity>> getAllPrograms();
Future<ProgramEntity?> getProgram(String id);
Future<List<ProgramWeekEntity>> getWeeks(String programId);
Future<List<ProgramWorkoutEntity>> getWorkouts(String programId);
Future<ProgramEntity> createProgram(String name);
Future<void> deleteProgram(String id);
Future<void> duplicateProgram(String sourceId);
Future<ProgramWeekEntity> addWeek(String programId, int position);
Future<void> deleteWeek(String id);
Future<void> updateWeekNote(String weekId, String note);
Future<ProgramWorkoutEntity> addWorkout(ProgramWorkoutEntity workout);
Future<void> updateWorkout(ProgramWorkoutEntity workout);
Future<void> deleteWorkout(String id);
Future<void> toggleWorkoutComplete(String id, bool currentStatus);
}

View File

@@ -0,0 +1,9 @@
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
abstract class TrainingPlanRepository {
Future<List<TrainingPlanEntity>> getAll();
Future<TrainingPlanEntity> getById(String id);
Future<TrainingPlanEntity> create(String name);
Future<void> update(TrainingPlanEntity plan);
Future<void> delete(String id);
}

50
lib/injection.dart Normal file
View File

@@ -0,0 +1,50 @@
import 'package:get_it/get_it.dart';
import 'package:trainhub_flutter/data/database/app_database.dart';
import 'package:trainhub_flutter/data/database/daos/exercise_dao.dart';
import 'package:trainhub_flutter/data/database/daos/training_plan_dao.dart';
import 'package:trainhub_flutter/data/database/daos/program_dao.dart';
import 'package:trainhub_flutter/data/database/daos/analysis_dao.dart';
import 'package:trainhub_flutter/data/database/daos/chat_dao.dart';
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
import 'package:trainhub_flutter/domain/repositories/program_repository.dart';
import 'package:trainhub_flutter/domain/repositories/analysis_repository.dart';
import 'package:trainhub_flutter/domain/repositories/chat_repository.dart';
import 'package:trainhub_flutter/data/repositories/exercise_repository_impl.dart';
import 'package:trainhub_flutter/data/repositories/training_plan_repository_impl.dart';
import 'package:trainhub_flutter/data/repositories/program_repository_impl.dart';
import 'package:trainhub_flutter/data/repositories/analysis_repository_impl.dart';
import 'package:trainhub_flutter/data/repositories/chat_repository_impl.dart';
final GetIt getIt = GetIt.instance;
void init() {
// Database
getIt.registerSingleton<AppDatabase>(AppDatabase());
// DAOs
getIt.registerSingleton<ExerciseDao>(ExerciseDao(getIt<AppDatabase>()));
getIt.registerSingleton<TrainingPlanDao>(
TrainingPlanDao(getIt<AppDatabase>()),
);
getIt.registerSingleton<ProgramDao>(ProgramDao(getIt<AppDatabase>()));
getIt.registerSingleton<AnalysisDao>(AnalysisDao(getIt<AppDatabase>()));
getIt.registerSingleton<ChatDao>(ChatDao(getIt<AppDatabase>()));
// Repositories
getIt.registerLazySingleton<ExerciseRepository>(
() => ExerciseRepositoryImpl(getIt<ExerciseDao>()),
);
getIt.registerLazySingleton<TrainingPlanRepository>(
() => TrainingPlanRepositoryImpl(getIt<TrainingPlanDao>()),
);
getIt.registerLazySingleton<ProgramRepository>(
() => ProgramRepositoryImpl(getIt<ProgramDao>()),
);
getIt.registerLazySingleton<AnalysisRepository>(
() => AnalysisRepositoryImpl(getIt<AnalysisDao>()),
);
getIt.registerLazySingleton<ChatRepository>(
() => ChatRepositoryImpl(getIt<ChatDao>()),
);
}

47
lib/main.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import 'package:trainhub_flutter/core/router/app_router.dart';
import 'package:trainhub_flutter/core/theme/app_theme.dart';
import 'package:trainhub_flutter/injection.dart' as di;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
// Initialize dependency injection
di.init();
WindowOptions windowOptions = const WindowOptions(
size: Size(1280, 800),
minimumSize: Size(800, 600),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.normal,
title: 'TrainHub',
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const ProviderScope(child: TrainHubApp()));
}
class TrainHubApp extends StatelessWidget {
const TrainHubApp({super.key});
@override
Widget build(BuildContext context) {
final appRouter = AppRouter();
return MaterialApp.router(
title: 'TrainHub',
theme: AppTheme.dark,
routerConfig: appRouter.config(),
debugShowCheckedModeBanner: false,
);
}
}

View File

@@ -0,0 +1,107 @@
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

@@ -0,0 +1,87 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/repositories/analysis_repository.dart';
import 'package:trainhub_flutter/presentation/analysis/analysis_state.dart';
part 'analysis_controller.g.dart';
@riverpod
class AnalysisController extends _$AnalysisController {
late final AnalysisRepository _repo;
@override
Future<AnalysisState> build() async {
_repo = getIt<AnalysisRepository>();
final sessions = await _repo.getAllSessions();
return AnalysisState(sessions: sessions);
}
Future<void> createSession(String name, String videoPath) async {
final session = await _repo.createSession(name, videoPath);
final sessions = await _repo.getAllSessions();
final annotations = await _repo.getAnnotations(session.id);
state = AsyncValue.data(
AnalysisState(
sessions: sessions,
activeSession: session,
annotations: annotations,
),
);
}
Future<void> loadSession(String id) async {
final session = await _repo.getSession(id);
if (session == null) return;
final annotations = await _repo.getAnnotations(id);
final current = state.valueOrNull ?? const AnalysisState();
state = AsyncValue.data(
current.copyWith(
activeSession: session,
annotations: annotations,
),
);
}
Future<void> deleteSession(String id) async {
await _repo.deleteSession(id);
final sessions = await _repo.getAllSessions();
final current = state.valueOrNull ?? const AnalysisState();
state = AsyncValue.data(
current.copyWith(
sessions: sessions,
activeSession:
current.activeSession?.id == id ? null : current.activeSession,
annotations: current.activeSession?.id == id ? [] : current.annotations,
),
);
}
Future<void> addAnnotation({
required String name,
required String description,
required double startTime,
required double endTime,
required String color,
}) async {
final current = state.valueOrNull;
if (current?.activeSession == null) return;
await _repo.addAnnotation(
sessionId: current!.activeSession!.id,
name: name,
description: description,
startTime: startTime,
endTime: endTime,
color: color,
);
final annotations = await _repo.getAnnotations(current.activeSession!.id);
state = AsyncValue.data(current.copyWith(annotations: annotations));
}
Future<void> deleteAnnotation(String id) async {
await _repo.deleteAnnotation(id);
final current = state.valueOrNull;
if (current?.activeSession == null) return;
final annotations = await _repo.getAnnotations(current!.activeSession!.id);
state = AsyncValue.data(current.copyWith(annotations: annotations));
}
}

View File

@@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'analysis_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$analysisControllerHash() =>
r'855d4ab55b8dc398e10c19d0ed245a60f104feed';
/// See also [AnalysisController].
@ProviderFor(AnalysisController)
final analysisControllerProvider =
AutoDisposeAsyncNotifierProvider<
AnalysisController,
AnalysisState
>.internal(
AnalysisController.new,
name: r'analysisControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$analysisControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AnalysisController = AutoDisposeAsyncNotifier<AnalysisState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,232 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/presentation/analysis/analysis_controller.dart';
import 'package:trainhub_flutter/presentation/analysis/widgets/analysis_session_list.dart';
import 'package:trainhub_flutter/presentation/analysis/widgets/analysis_viewer.dart';
import 'package:trainhub_flutter/presentation/common/widgets/app_empty_state.dart';
@RoutePage()
class AnalysisPage extends ConsumerWidget {
const AnalysisPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(analysisControllerProvider);
final controller = ref.read(analysisControllerProvider.notifier);
return state.when(
data: (data) {
if (data.activeSession != null) {
return AnalysisViewer(
session: data.activeSession!,
annotations: data.annotations,
onClose: () {
ref.invalidate(analysisControllerProvider);
},
);
}
return Column(
children: [
// Header toolbar
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing16,
),
decoration: const BoxDecoration(
color: AppColors.surfaceContainer,
border: Border(
bottom: BorderSide(color: AppColors.border),
),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.purpleMuted,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.video_library,
color: AppColors.purple,
size: 20,
),
),
const SizedBox(width: UIConstants.spacing12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Video Analysis',
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(
'Analyze and annotate your training videos',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 12,
),
),
],
),
const Spacer(),
FilledButton.icon(
onPressed: () =>
_showAddSessionDialog(context, controller),
icon: const Icon(Icons.add, size: 18),
label: const Text('New Session'),
),
],
),
),
// Session list
Expanded(
child: data.sessions.isEmpty
? AppEmptyState(
icon: Icons.video_library_outlined,
title: 'No analysis sessions yet',
subtitle:
'Create a session to analyze your training videos',
actionLabel: 'Create Session',
onAction: () =>
_showAddSessionDialog(context, controller),
)
: AnalysisSessionList(
sessions: data.sessions,
onSessionSelected: (session) =>
controller.loadSession(session.id),
onDeleteSession: (session) =>
controller.deleteSession(session.id),
),
),
],
);
},
error: (e, s) => Center(
child: Text(
'Error: $e',
style: const TextStyle(color: AppColors.destructive),
),
),
loading: () => const Center(child: CircularProgressIndicator()),
);
}
void _showAddSessionDialog(
BuildContext context,
AnalysisController controller,
) {
final textController = TextEditingController();
String? selectedVideoPath;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('New Analysis Session'),
content: SizedBox(
width: UIConstants.dialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Session Name',
hintText: 'e.g. Squat Form Check',
),
autofocus: true,
),
const SizedBox(height: UIConstants.spacing16),
Container(
padding: const EdgeInsets.all(UIConstants.spacing12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: UIConstants.smallCardBorderRadius,
border: Border.all(color: AppColors.border),
),
child: Row(
children: [
Icon(
selectedVideoPath != null
? Icons.videocam
: Icons.videocam_off_outlined,
color: selectedVideoPath != null
? AppColors.success
: AppColors.textMuted,
size: 20,
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Text(
selectedVideoPath != null
? selectedVideoPath!
.split(RegExp(r'[\\/]'))
.last
: 'No video selected',
style: TextStyle(
color: selectedVideoPath != null
? AppColors.textPrimary
: AppColors.textMuted,
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: UIConstants.spacing8),
OutlinedButton.icon(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.video,
allowMultiple: false,
);
if (result != null &&
result.files.single.path != null) {
setState(() {
selectedVideoPath = result.files.single.path;
});
}
},
icon: const Icon(Icons.folder_open, size: 16),
label: const Text('Browse'),
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
if (textController.text.isNotEmpty &&
selectedVideoPath != null) {
controller.createSession(
textController.text,
selectedVideoPath!,
);
Navigator.pop(context);
}
},
child: const Text('Create'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
import 'package:trainhub_flutter/domain/entities/annotation.dart';
part 'analysis_state.freezed.dart';
@freezed
class AnalysisState with _$AnalysisState {
const factory AnalysisState({
@Default([]) List<AnalysisSessionEntity> sessions,
AnalysisSessionEntity? activeSession,
@Default([]) List<AnnotationEntity> annotations,
}) = _AnalysisState;
}

View File

@@ -0,0 +1,244 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'analysis_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$AnalysisState {
List<AnalysisSessionEntity> get sessions =>
throw _privateConstructorUsedError;
AnalysisSessionEntity? get activeSession =>
throw _privateConstructorUsedError;
List<AnnotationEntity> get annotations => throw _privateConstructorUsedError;
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AnalysisStateCopyWith<AnalysisState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AnalysisStateCopyWith<$Res> {
factory $AnalysisStateCopyWith(
AnalysisState value,
$Res Function(AnalysisState) then,
) = _$AnalysisStateCopyWithImpl<$Res, AnalysisState>;
@useResult
$Res call({
List<AnalysisSessionEntity> sessions,
AnalysisSessionEntity? activeSession,
List<AnnotationEntity> annotations,
});
$AnalysisSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class _$AnalysisStateCopyWithImpl<$Res, $Val extends AnalysisState>
implements $AnalysisStateCopyWith<$Res> {
_$AnalysisStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? annotations = null,
}) {
return _then(
_value.copyWith(
sessions: null == sessions
? _value.sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<AnalysisSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as AnalysisSessionEntity?,
annotations: null == annotations
? _value.annotations
: annotations // ignore: cast_nullable_to_non_nullable
as List<AnnotationEntity>,
)
as $Val,
);
}
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AnalysisSessionEntityCopyWith<$Res>? get activeSession {
if (_value.activeSession == null) {
return null;
}
return $AnalysisSessionEntityCopyWith<$Res>(_value.activeSession!, (value) {
return _then(_value.copyWith(activeSession: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AnalysisStateImplCopyWith<$Res>
implements $AnalysisStateCopyWith<$Res> {
factory _$$AnalysisStateImplCopyWith(
_$AnalysisStateImpl value,
$Res Function(_$AnalysisStateImpl) then,
) = __$$AnalysisStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
List<AnalysisSessionEntity> sessions,
AnalysisSessionEntity? activeSession,
List<AnnotationEntity> annotations,
});
@override
$AnalysisSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class __$$AnalysisStateImplCopyWithImpl<$Res>
extends _$AnalysisStateCopyWithImpl<$Res, _$AnalysisStateImpl>
implements _$$AnalysisStateImplCopyWith<$Res> {
__$$AnalysisStateImplCopyWithImpl(
_$AnalysisStateImpl _value,
$Res Function(_$AnalysisStateImpl) _then,
) : super(_value, _then);
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? annotations = null,
}) {
return _then(
_$AnalysisStateImpl(
sessions: null == sessions
? _value._sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<AnalysisSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as AnalysisSessionEntity?,
annotations: null == annotations
? _value._annotations
: annotations // ignore: cast_nullable_to_non_nullable
as List<AnnotationEntity>,
),
);
}
}
/// @nodoc
class _$AnalysisStateImpl implements _AnalysisState {
const _$AnalysisStateImpl({
final List<AnalysisSessionEntity> sessions = const [],
this.activeSession,
final List<AnnotationEntity> annotations = const [],
}) : _sessions = sessions,
_annotations = annotations;
final List<AnalysisSessionEntity> _sessions;
@override
@JsonKey()
List<AnalysisSessionEntity> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sessions);
}
@override
final AnalysisSessionEntity? activeSession;
final List<AnnotationEntity> _annotations;
@override
@JsonKey()
List<AnnotationEntity> get annotations {
if (_annotations is EqualUnmodifiableListView) return _annotations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_annotations);
}
@override
String toString() {
return 'AnalysisState(sessions: $sessions, activeSession: $activeSession, annotations: $annotations)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AnalysisStateImpl &&
const DeepCollectionEquality().equals(other._sessions, _sessions) &&
(identical(other.activeSession, activeSession) ||
other.activeSession == activeSession) &&
const DeepCollectionEquality().equals(
other._annotations,
_annotations,
));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_sessions),
activeSession,
const DeepCollectionEquality().hash(_annotations),
);
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AnalysisStateImplCopyWith<_$AnalysisStateImpl> get copyWith =>
__$$AnalysisStateImplCopyWithImpl<_$AnalysisStateImpl>(this, _$identity);
}
abstract class _AnalysisState implements AnalysisState {
const factory _AnalysisState({
final List<AnalysisSessionEntity> sessions,
final AnalysisSessionEntity? activeSession,
final List<AnnotationEntity> annotations,
}) = _$AnalysisStateImpl;
@override
List<AnalysisSessionEntity> get sessions;
@override
AnalysisSessionEntity? get activeSession;
@override
List<AnnotationEntity> get annotations;
/// Create a copy of AnalysisState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AnalysisStateImplCopyWith<_$AnalysisStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
class AnalysisSessionList extends StatelessWidget {
final List<AnalysisSessionEntity> sessions;
final Function(AnalysisSessionEntity) onSessionSelected;
final Function(AnalysisSessionEntity) onDeleteSession;
const AnalysisSessionList({
super.key,
required this.sessions,
required this.onSessionSelected,
required this.onDeleteSession,
});
@override
Widget build(BuildContext context) {
if (sessions.isEmpty) {
return const Center(
child: Text('No analysis sessions yet. Tap + to create one.'),
);
}
return ListView.builder(
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),
),
onTap: () => onSessionSelected(session),
);
},
);
}
}

View File

@@ -0,0 +1,470 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trainhub_flutter/domain/entities/analysis_session.dart';
import 'package:trainhub_flutter/domain/entities/annotation.dart';
import 'package:trainhub_flutter/presentation/analysis/analysis_controller.dart';
import 'package:video_player/video_player.dart';
class AnalysisViewer extends ConsumerStatefulWidget {
final AnalysisSessionEntity session;
final List<AnnotationEntity> annotations;
final VoidCallback onClose;
const AnalysisViewer({
super.key,
required this.session,
required this.annotations,
required this.onClose,
});
@override
ConsumerState<AnalysisViewer> createState() => _AnalysisViewerState();
}
class _AnalysisViewerState extends ConsumerState<AnalysisViewer> {
VideoPlayerController? _videoController;
bool _isPlaying = false;
double _currentPosition = 0.0;
double _totalDuration = 1.0;
// IN/OUT points
double? _inPoint;
double? _outPoint;
bool _isLooping = false;
@override
void initState() {
super.initState();
_initializeVideo();
}
@override
void didUpdateWidget(covariant AnalysisViewer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.session.videoPath != widget.session.videoPath) {
_initializeVideo();
}
}
Future<void> _initializeVideo() async {
final path = widget.session.videoPath;
if (path == null) return;
_videoController?.dispose();
final file = File(path);
if (await file.exists()) {
_videoController = VideoPlayerController.file(file);
await _videoController!.initialize();
setState(() {
_totalDuration = _videoController!.value.duration.inSeconds.toDouble();
});
_videoController!.addListener(_videoListener);
}
}
void _videoListener() {
if (_videoController == null) return;
final bool isPlaying = _videoController!.value.isPlaying;
final double position =
_videoController!.value.position.inMilliseconds / 1000.0;
// Loop logic
if (_isLooping && _outPoint != null && position >= _outPoint!) {
_seekTo(_inPoint ?? 0.0);
return;
}
if (isPlaying != _isPlaying || (position - _currentPosition).abs() > 0.1) {
setState(() {
_isPlaying = isPlaying;
_currentPosition = position;
});
}
}
@override
void dispose() {
_videoController?.removeListener(_videoListener);
_videoController?.dispose();
super.dispose();
}
void _togglePlay() {
if (_videoController == null) return;
if (_videoController!.value.isPlaying) {
_videoController!.pause();
} else {
if (_isLooping && _outPoint != null && _currentPosition >= _outPoint!) {
_seekTo(_inPoint ?? 0.0);
}
_videoController!.play();
}
}
void _seekTo(double value) {
if (_videoController == null) return;
_videoController!.seekTo(Duration(milliseconds: (value * 1000).toInt()));
}
void _setInPoint() {
setState(() {
_inPoint = _currentPosition;
if (_outPoint != null && _inPoint! > _outPoint!) {
_outPoint = null;
}
});
}
void _setOutPoint() {
setState(() {
_outPoint = _currentPosition;
if (_inPoint != null && _outPoint! < _inPoint!) {
_inPoint = null;
}
});
}
void _clearPoints() {
setState(() {
_inPoint = null;
_outPoint = null;
_isLooping = false;
});
}
void _toggleLoop() {
setState(() {
_isLooping = !_isLooping;
});
}
void _playRange(double start, double end) {
setState(() {
_inPoint = start;
_outPoint = end;
_isLooping = true;
});
_seekTo(start);
_videoController?.play();
}
@override
Widget build(BuildContext context) {
final controller = ref.read(analysisControllerProvider.notifier);
return Column(
children: [
// Video Area
Expanded(
flex: 3,
child: Container(
color: Colors.black,
alignment: Alignment.center,
child:
_videoController != null &&
_videoController!.value.isInitialized
? Stack(
alignment: Alignment.bottomCenter,
children: [
Center(
child: AspectRatio(
aspectRatio: _videoController!.value.aspectRatio,
child: VideoPlayer(_videoController!),
),
),
_buildTimelineControls(),
],
)
: const Center(child: CircularProgressIndicator()),
),
),
// Annotations List
Expanded(
flex: 2,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Annotations",
style: Theme.of(context).textTheme.titleMedium,
),
ElevatedButton.icon(
onPressed: () {
double start = _inPoint ?? _currentPosition;
double end = _outPoint ?? (_currentPosition + 5.0);
if (end > _totalDuration) end = _totalDuration;
controller.addAnnotation(
name: "New Annotation",
description:
"${_formatDuration(start)} - ${_formatDuration(end)}",
startTime: start,
endTime: end,
color: "red",
);
},
icon: const Icon(Icons.add),
label: const Text("Add from Selection"),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) =>
const Divider(height: 1),
itemCount: widget.annotations.length,
itemBuilder: (context, index) {
final note = widget.annotations[index];
final bool isActive =
_currentPosition >= note.startTime &&
_currentPosition <= note.endTime;
return Container(
color: isActive
? Theme.of(
context,
).colorScheme.primaryContainer.withOpacity(0.2)
: null,
child: ListTile(
leading: Icon(
Icons.label,
color: _parseColor(note.color ?? 'grey'),
),
title: Text(note.name ?? 'Untitled'),
subtitle: Text(
"${_formatDuration(note.startTime)} - ${_formatDuration(note.endTime)}",
style: Theme.of(context).textTheme.bodySmall,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.play_circle_outline),
onPressed: () =>
_playRange(note.startTime, note.endTime),
tooltip: "Play Range",
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () =>
controller.deleteAnnotation(note.id),
),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: double.infinity,
child: TextButton(
onPressed: widget.onClose,
child: const Text('Back'),
),
),
),
],
),
),
],
);
}
Widget _buildTimelineControls() {
return Container(
color: Colors.black.withOpacity(0.8),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Timeline Visualization
SizedBox(
height: 20,
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
// Annotations
...widget.annotations.map((note) {
final left =
(note.startTime / _totalDuration) *
constraints.maxWidth;
final width =
((note.endTime - note.startTime) / _totalDuration) *
constraints.maxWidth;
return Positioned(
left: left,
width: width,
top: 4,
bottom: 4,
child: Container(
decoration: BoxDecoration(
color: _parseColor(
note.color ?? 'grey',
).withOpacity(0.6),
borderRadius: BorderRadius.circular(2),
),
),
);
}),
// IN Point
if (_inPoint != null)
Positioned(
left:
(_inPoint! / _totalDuration) * constraints.maxWidth,
top: 0,
bottom: 0,
child: Container(width: 2, color: Colors.green),
),
// OUT Point
if (_outPoint != null)
Positioned(
left:
(_outPoint! / _totalDuration) *
constraints.maxWidth,
top: 0,
bottom: 0,
child: Container(width: 2, color: Colors.red),
),
],
);
},
),
),
// Slider
SliderTheme(
data: SliderTheme.of(context).copyWith(
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 10),
trackHeight: 2,
),
child: Slider(
value: _currentPosition.clamp(0.0, _totalDuration),
min: 0.0,
max: _totalDuration,
onChanged: (value) => _seekTo(value),
activeColor: Theme.of(context).colorScheme.primary,
inactiveColor: Colors.grey,
),
),
// Controls Row
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatDuration(_currentPosition),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
const SizedBox(width: 16),
IconButton(
icon: const Icon(
Icons.keyboard_arrow_left,
color: Colors.white,
),
onPressed: () => _seekTo(_currentPosition - 1),
),
IconButton(
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: _togglePlay,
),
IconButton(
icon: const Icon(
Icons.keyboard_arrow_right,
color: Colors.white,
),
onPressed: () => _seekTo(_currentPosition + 1),
),
const SizedBox(width: 16),
// IN/OUT Controls
IconButton(
icon: const Icon(Icons.login, color: Colors.green),
tooltip: "Set IN Point",
onPressed: _setInPoint,
),
IconButton(
icon: const Icon(Icons.logout, color: Colors.red),
tooltip: "Set OUT Point",
onPressed: _setOutPoint,
),
IconButton(
icon: Icon(
Icons.loop,
color: _isLooping
? Theme.of(context).colorScheme.primary
: Colors.white,
),
tooltip: "Toggle Loop",
onPressed: _toggleLoop,
),
if (_inPoint != null || _outPoint != null)
IconButton(
icon: const Icon(
Icons.cancel_outlined,
color: Colors.white,
),
tooltip: "Clear Points",
onPressed: _clearPoints,
),
const Spacer(),
Text(
_formatDuration(_totalDuration),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
],
),
);
}
String _formatDuration(double seconds) {
final duration = Duration(milliseconds: (seconds * 1000).toInt());
final mins = duration.inMinutes;
final secs = duration.inSeconds % 60;
return '$mins:${secs.toString().padLeft(2, '0')}';
}
Color _parseColor(String colorName) {
switch (colorName.toLowerCase()) {
case 'red':
return Colors.red;
case 'green':
return Colors.green;
case 'blue':
return Colors.blue;
case 'yellow':
return Colors.yellow;
default:
return Colors.grey;
}
}
}

View File

@@ -0,0 +1,125 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/domain/repositories/program_repository.dart';
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
import 'package:trainhub_flutter/presentation/calendar/calendar_state.dart';
part 'calendar_controller.g.dart';
@riverpod
class CalendarController extends _$CalendarController {
late final ProgramRepository _programRepo;
late final TrainingPlanRepository _planRepo;
late final ExerciseRepository _exerciseRepo;
@override
Future<CalendarState> build() async {
_programRepo = getIt<ProgramRepository>();
_planRepo = getIt<TrainingPlanRepository>();
_exerciseRepo = getIt<ExerciseRepository>();
final programs = await _programRepo.getAllPrograms();
final plans = await _planRepo.getAll();
final exercises = await _exerciseRepo.getAll();
if (programs.isEmpty) {
return CalendarState(plans: plans, exercises: exercises);
}
final activeProgram = programs.first;
final weeks = await _programRepo.getWeeks(activeProgram.id);
final workouts = await _programRepo.getWorkouts(activeProgram.id);
return CalendarState(
programs: programs,
activeProgram: activeProgram,
weeks: weeks,
workouts: workouts,
plans: plans,
exercises: exercises,
);
}
Future<void> loadProgram(String id) async {
final program = await _programRepo.getProgram(id);
if (program == null) return;
final weeks = await _programRepo.getWeeks(id);
final workouts = await _programRepo.getWorkouts(id);
final current = state.valueOrNull ?? const CalendarState();
state = AsyncValue.data(
current.copyWith(
activeProgram: program,
weeks: weeks,
workouts: workouts,
),
);
}
Future<void> createProgram(String name) async {
final program = await _programRepo.createProgram(name);
await _reloadFull();
await loadProgram(program.id);
}
Future<void> deleteProgram(String id) async {
await _programRepo.deleteProgram(id);
state = await AsyncValue.guard(() => build());
}
Future<void> duplicateProgram(String sourceId) async {
await _programRepo.duplicateProgram(sourceId);
await _reloadFull();
}
Future<void> addWeek() async {
final current = state.valueOrNull;
if (current?.activeProgram == null) return;
final nextPosition =
current!.weeks.isEmpty ? 1 : current.weeks.last.position + 1;
await _programRepo.addWeek(current.activeProgram!.id, nextPosition);
await _reloadProgramDetails();
}
Future<void> deleteWeek(String id) async {
await _programRepo.deleteWeek(id);
await _reloadProgramDetails();
}
Future<void> updateWeekNote(String weekId, String note) async {
await _programRepo.updateWeekNote(weekId, note);
await _reloadProgramDetails();
}
Future<void> addWorkout(ProgramWorkoutEntity workout) async {
await _programRepo.addWorkout(workout);
await _reloadProgramDetails();
}
Future<void> updateWorkout(ProgramWorkoutEntity workout) async {
await _programRepo.updateWorkout(workout);
await _reloadProgramDetails();
}
Future<void> deleteWorkout(String id) async {
await _programRepo.deleteWorkout(id);
await _reloadProgramDetails();
}
Future<void> toggleWorkoutComplete(String id, bool currentStatus) async {
await _programRepo.toggleWorkoutComplete(id, currentStatus);
await _reloadProgramDetails();
}
Future<void> _reloadProgramDetails() async {
final current = state.valueOrNull;
if (current?.activeProgram == null) return;
final weeks = await _programRepo.getWeeks(current!.activeProgram!.id);
final workouts =
await _programRepo.getWorkouts(current.activeProgram!.id);
state = AsyncValue.data(
current.copyWith(weeks: weeks, workouts: workouts),
);
}
Future<void> _reloadFull() async {
state = await AsyncValue.guard(() => build());
}
}

View File

@@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'calendar_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$calendarControllerHash() =>
r'747a59ba47bf4d1b6a66e3bcc82276e4ad81eb1a';
/// See also [CalendarController].
@ProviderFor(CalendarController)
final calendarControllerProvider =
AutoDisposeAsyncNotifierProvider<
CalendarController,
CalendarState
>.internal(
CalendarController.new,
name: r'calendarControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$calendarControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CalendarController = AutoDisposeAsyncNotifier<CalendarState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,116 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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';
@RoutePage()
class CalendarPage extends ConsumerWidget {
const CalendarPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
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,
);
},
),
),
],
);
},
error: (e, s) => Center(child: Text('Error: $e')),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
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'),
),
],
),
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/program.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
import 'package:trainhub_flutter/domain/entities/exercise.dart';
part 'calendar_state.freezed.dart';
@freezed
class CalendarState with _$CalendarState {
const factory CalendarState({
@Default([]) List<ProgramEntity> programs,
ProgramEntity? activeProgram,
@Default([]) List<ProgramWeekEntity> weeks,
@Default([]) List<ProgramWorkoutEntity> workouts,
@Default([]) List<TrainingPlanEntity> plans,
@Default([]) List<ExerciseEntity> exercises,
}) = _CalendarState;
}

View File

@@ -0,0 +1,329 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'calendar_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$CalendarState {
List<ProgramEntity> get programs => throw _privateConstructorUsedError;
ProgramEntity? get activeProgram => throw _privateConstructorUsedError;
List<ProgramWeekEntity> get weeks => throw _privateConstructorUsedError;
List<ProgramWorkoutEntity> get workouts => throw _privateConstructorUsedError;
List<TrainingPlanEntity> get plans => throw _privateConstructorUsedError;
List<ExerciseEntity> get exercises => throw _privateConstructorUsedError;
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CalendarStateCopyWith<CalendarState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CalendarStateCopyWith<$Res> {
factory $CalendarStateCopyWith(
CalendarState value,
$Res Function(CalendarState) then,
) = _$CalendarStateCopyWithImpl<$Res, CalendarState>;
@useResult
$Res call({
List<ProgramEntity> programs,
ProgramEntity? activeProgram,
List<ProgramWeekEntity> weeks,
List<ProgramWorkoutEntity> workouts,
List<TrainingPlanEntity> plans,
List<ExerciseEntity> exercises,
});
$ProgramEntityCopyWith<$Res>? get activeProgram;
}
/// @nodoc
class _$CalendarStateCopyWithImpl<$Res, $Val extends CalendarState>
implements $CalendarStateCopyWith<$Res> {
_$CalendarStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? programs = null,
Object? activeProgram = freezed,
Object? weeks = null,
Object? workouts = null,
Object? plans = null,
Object? exercises = null,
}) {
return _then(
_value.copyWith(
programs: null == programs
? _value.programs
: programs // ignore: cast_nullable_to_non_nullable
as List<ProgramEntity>,
activeProgram: freezed == activeProgram
? _value.activeProgram
: activeProgram // ignore: cast_nullable_to_non_nullable
as ProgramEntity?,
weeks: null == weeks
? _value.weeks
: weeks // ignore: cast_nullable_to_non_nullable
as List<ProgramWeekEntity>,
workouts: null == workouts
? _value.workouts
: workouts // ignore: cast_nullable_to_non_nullable
as List<ProgramWorkoutEntity>,
plans: null == plans
? _value.plans
: plans // ignore: cast_nullable_to_non_nullable
as List<TrainingPlanEntity>,
exercises: null == exercises
? _value.exercises
: exercises // ignore: cast_nullable_to_non_nullable
as List<ExerciseEntity>,
)
as $Val,
);
}
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ProgramEntityCopyWith<$Res>? get activeProgram {
if (_value.activeProgram == null) {
return null;
}
return $ProgramEntityCopyWith<$Res>(_value.activeProgram!, (value) {
return _then(_value.copyWith(activeProgram: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$CalendarStateImplCopyWith<$Res>
implements $CalendarStateCopyWith<$Res> {
factory _$$CalendarStateImplCopyWith(
_$CalendarStateImpl value,
$Res Function(_$CalendarStateImpl) then,
) = __$$CalendarStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
List<ProgramEntity> programs,
ProgramEntity? activeProgram,
List<ProgramWeekEntity> weeks,
List<ProgramWorkoutEntity> workouts,
List<TrainingPlanEntity> plans,
List<ExerciseEntity> exercises,
});
@override
$ProgramEntityCopyWith<$Res>? get activeProgram;
}
/// @nodoc
class __$$CalendarStateImplCopyWithImpl<$Res>
extends _$CalendarStateCopyWithImpl<$Res, _$CalendarStateImpl>
implements _$$CalendarStateImplCopyWith<$Res> {
__$$CalendarStateImplCopyWithImpl(
_$CalendarStateImpl _value,
$Res Function(_$CalendarStateImpl) _then,
) : super(_value, _then);
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? programs = null,
Object? activeProgram = freezed,
Object? weeks = null,
Object? workouts = null,
Object? plans = null,
Object? exercises = null,
}) {
return _then(
_$CalendarStateImpl(
programs: null == programs
? _value._programs
: programs // ignore: cast_nullable_to_non_nullable
as List<ProgramEntity>,
activeProgram: freezed == activeProgram
? _value.activeProgram
: activeProgram // ignore: cast_nullable_to_non_nullable
as ProgramEntity?,
weeks: null == weeks
? _value._weeks
: weeks // ignore: cast_nullable_to_non_nullable
as List<ProgramWeekEntity>,
workouts: null == workouts
? _value._workouts
: workouts // ignore: cast_nullable_to_non_nullable
as List<ProgramWorkoutEntity>,
plans: null == plans
? _value._plans
: plans // ignore: cast_nullable_to_non_nullable
as List<TrainingPlanEntity>,
exercises: null == exercises
? _value._exercises
: exercises // ignore: cast_nullable_to_non_nullable
as List<ExerciseEntity>,
),
);
}
}
/// @nodoc
class _$CalendarStateImpl implements _CalendarState {
const _$CalendarStateImpl({
final List<ProgramEntity> programs = const [],
this.activeProgram,
final List<ProgramWeekEntity> weeks = const [],
final List<ProgramWorkoutEntity> workouts = const [],
final List<TrainingPlanEntity> plans = const [],
final List<ExerciseEntity> exercises = const [],
}) : _programs = programs,
_weeks = weeks,
_workouts = workouts,
_plans = plans,
_exercises = exercises;
final List<ProgramEntity> _programs;
@override
@JsonKey()
List<ProgramEntity> get programs {
if (_programs is EqualUnmodifiableListView) return _programs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_programs);
}
@override
final ProgramEntity? activeProgram;
final List<ProgramWeekEntity> _weeks;
@override
@JsonKey()
List<ProgramWeekEntity> get weeks {
if (_weeks is EqualUnmodifiableListView) return _weeks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_weeks);
}
final List<ProgramWorkoutEntity> _workouts;
@override
@JsonKey()
List<ProgramWorkoutEntity> get workouts {
if (_workouts is EqualUnmodifiableListView) return _workouts;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_workouts);
}
final List<TrainingPlanEntity> _plans;
@override
@JsonKey()
List<TrainingPlanEntity> get plans {
if (_plans is EqualUnmodifiableListView) return _plans;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_plans);
}
final List<ExerciseEntity> _exercises;
@override
@JsonKey()
List<ExerciseEntity> get exercises {
if (_exercises is EqualUnmodifiableListView) return _exercises;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_exercises);
}
@override
String toString() {
return 'CalendarState(programs: $programs, activeProgram: $activeProgram, weeks: $weeks, workouts: $workouts, plans: $plans, exercises: $exercises)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CalendarStateImpl &&
const DeepCollectionEquality().equals(other._programs, _programs) &&
(identical(other.activeProgram, activeProgram) ||
other.activeProgram == activeProgram) &&
const DeepCollectionEquality().equals(other._weeks, _weeks) &&
const DeepCollectionEquality().equals(other._workouts, _workouts) &&
const DeepCollectionEquality().equals(other._plans, _plans) &&
const DeepCollectionEquality().equals(
other._exercises,
_exercises,
));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_programs),
activeProgram,
const DeepCollectionEquality().hash(_weeks),
const DeepCollectionEquality().hash(_workouts),
const DeepCollectionEquality().hash(_plans),
const DeepCollectionEquality().hash(_exercises),
);
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CalendarStateImplCopyWith<_$CalendarStateImpl> get copyWith =>
__$$CalendarStateImplCopyWithImpl<_$CalendarStateImpl>(this, _$identity);
}
abstract class _CalendarState implements CalendarState {
const factory _CalendarState({
final List<ProgramEntity> programs,
final ProgramEntity? activeProgram,
final List<ProgramWeekEntity> weeks,
final List<ProgramWorkoutEntity> workouts,
final List<TrainingPlanEntity> plans,
final List<ExerciseEntity> exercises,
}) = _$CalendarStateImpl;
@override
List<ProgramEntity> get programs;
@override
ProgramEntity? get activeProgram;
@override
List<ProgramWeekEntity> get weeks;
@override
List<ProgramWorkoutEntity> get workouts;
@override
List<TrainingPlanEntity> get plans;
@override
List<ExerciseEntity> get exercises;
/// Create a copy of CalendarState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CalendarStateImplCopyWith<_$CalendarStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/domain/entities/program.dart';
class ProgramSelector extends StatelessWidget {
final List<ProgramEntity> programs;
final ProgramEntity? activeProgram;
final ValueChanged<ProgramEntity> onProgramSelected;
final VoidCallback onCreateProgram;
final VoidCallback onDuplicateProgram;
final VoidCallback onDeleteProgram;
const ProgramSelector({
super.key,
required this.programs,
required this.activeProgram,
required this.onProgramSelected,
required this.onCreateProgram,
required this.onDuplicateProgram,
required this.onDeleteProgram,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.pagePadding,
vertical: UIConstants.spacing12,
),
decoration: const BoxDecoration(
color: AppColors.surfaceContainer,
border: Border(
bottom: BorderSide(color: AppColors.border),
),
),
child: Row(
children: [
// Program icon
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.accentMuted,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
),
child: const Icon(
Icons.calendar_month_rounded,
color: AppColors.accent,
size: 18,
),
),
const SizedBox(width: UIConstants.spacing12),
// Program dropdown
Expanded(
child: _ProgramDropdown(
programs: programs,
activeProgram: activeProgram,
onProgramSelected: onProgramSelected,
),
),
const SizedBox(width: UIConstants.spacing12),
// Action buttons
_ToolbarButton(
icon: Icons.add_rounded,
label: 'New Program',
onPressed: onCreateProgram,
isPrimary: true,
),
const SizedBox(width: UIConstants.spacing8),
_ToolbarButton(
icon: Icons.copy_rounded,
label: 'Duplicate',
onPressed: activeProgram != null ? onDuplicateProgram : null,
),
const SizedBox(width: UIConstants.spacing8),
_ToolbarButton(
icon: Icons.delete_outline_rounded,
label: 'Delete',
onPressed: activeProgram != null ? onDeleteProgram : null,
isDestructive: true,
),
],
),
);
}
}
class _ProgramDropdown extends StatelessWidget {
final List<ProgramEntity> programs;
final ProgramEntity? activeProgram;
final ValueChanged<ProgramEntity> onProgramSelected;
const _ProgramDropdown({
required this.programs,
required this.activeProgram,
required this.onProgramSelected,
});
@override
Widget build(BuildContext context) {
return Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: UIConstants.spacing12),
decoration: BoxDecoration(
color: AppColors.zinc950,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: Border.all(color: AppColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: activeProgram?.id,
isExpanded: true,
icon: const Icon(
Icons.unfold_more_rounded,
size: 18,
color: AppColors.textMuted,
),
dropdownColor: AppColors.surfaceContainer,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
hint: Text(
'Select a program',
style: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textMuted,
),
),
items: programs.map((p) {
return DropdownMenuItem<String>(
value: p.id,
child: Text(
p.name,
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: (id) {
if (id != null) {
final program = programs.firstWhere((p) => p.id == id);
onProgramSelected(program);
}
},
),
),
);
}
}
class _ToolbarButton extends StatefulWidget {
final IconData icon;
final String label;
final VoidCallback? onPressed;
final bool isPrimary;
final bool isDestructive;
const _ToolbarButton({
required this.icon,
required this.label,
required this.onPressed,
this.isPrimary = false,
this.isDestructive = false,
});
@override
State<_ToolbarButton> createState() => _ToolbarButtonState();
}
class _ToolbarButtonState extends State<_ToolbarButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
final isDisabled = widget.onPressed == null;
Color bgColor;
Color fgColor;
Color borderColor;
if (isDisabled) {
bgColor = Colors.transparent;
fgColor = AppColors.zinc600;
borderColor = AppColors.border;
} else if (widget.isPrimary) {
bgColor = _isHovered ? AppColors.accent : AppColors.accentMuted;
fgColor = _isHovered ? AppColors.zinc950 : AppColors.accent;
borderColor = AppColors.accent.withValues(alpha: 0.3);
} else if (widget.isDestructive) {
bgColor =
_isHovered ? AppColors.destructiveMuted : Colors.transparent;
fgColor =
_isHovered ? AppColors.destructive : AppColors.textSecondary;
borderColor = _isHovered ? AppColors.destructive.withValues(alpha: 0.3) : AppColors.border;
} else {
bgColor = _isHovered ? AppColors.surfaceContainerHigh : Colors.transparent;
fgColor = _isHovered ? AppColors.textPrimary : AppColors.textSecondary;
borderColor = AppColors.border;
}
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: Tooltip(
message: widget.label,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onPressed,
borderRadius:
BorderRadius.circular(UIConstants.smallBorderRadius),
child: AnimatedContainer(
duration: UIConstants.animationDuration,
height: 34,
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: bgColor,
borderRadius:
BorderRadius.circular(UIConstants.smallBorderRadius),
border: Border.all(color: borderColor),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 16, color: fgColor),
const SizedBox(width: UIConstants.spacing4),
Text(
widget.label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: fgColor,
),
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,244 @@
import 'package:flutter/material.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/domain/entities/program_week.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
class ProgramWeekView extends StatelessWidget {
final ProgramWeekEntity week;
final List<ProgramWorkoutEntity> workouts;
final List<TrainingPlanEntity> availablePlans;
final Function(ProgramWorkoutEntity) onAddWorkout;
final Function(String) onDeleteWorkout;
const ProgramWeekView({
super.key,
required this.week,
required this.workouts,
required this.availablePlans,
required this.onAddWorkout,
required this.onDeleteWorkout,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 24),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Week ${week.position}",
style: Theme.of(context).textTheme.headlineSmall,
),
const Divider(),
SizedBox(
height: 500, // Fixed height for the week grid, or make it dynamic
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: List.generate(7, (dayIndex) {
final dayNum = dayIndex + 1;
final dayWorkouts = workouts
.where((w) => w.day == dayNum.toString())
.toList();
return Expanded(
child: Container(
decoration: BoxDecoration(
border: dayIndex < 6
? const Border(
right: BorderSide(
color: Colors.grey,
width: 0.5,
),
)
: null,
color: dayIndex % 2 == 0
? Theme.of(context).colorScheme.surfaceContainerLow
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHigh,
border: const Border(
bottom: BorderSide(
color: Colors.grey,
width: 0.5,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_getDayName(dayNum),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
InkWell(
onTap: () =>
_showAddWorkoutSheet(context, dayNum),
borderRadius: BorderRadius.circular(16),
child: const Icon(
Icons.add_circle_outline,
size: 20,
),
),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(4),
child: Column(
children: [
if (dayWorkouts.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Center(
child: Text(
"Rest",
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
),
)
else
...dayWorkouts.map(
(w) => Card(
margin: const EdgeInsets.only(
bottom: 4,
),
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 0,
),
visualDensity: VisualDensity.compact,
title: Text(
w.name ?? 'Untitled',
style: const TextStyle(
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: InkWell(
onTap: () => onDeleteWorkout(w.id),
borderRadius: BorderRadius.circular(
12,
),
child: const Icon(
Icons.close,
size: 14,
),
),
onTap: () {
// Optional: Edit on tap
},
),
),
),
],
),
),
),
],
),
),
);
}),
),
),
],
),
),
);
}
String _getDayName(int day) {
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
if (day >= 1 && day <= 7) return days[day - 1];
return 'Day $day';
}
void _showAddWorkoutSheet(BuildContext context, int dayNum) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
"Select Training Plan",
style: Theme.of(context).textTheme.titleMedium,
),
),
const Divider(),
if (availablePlans.isEmpty)
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text("No training plans available. Create one first!"),
),
),
...availablePlans
.map(
(plan) => ListTile(
leading: const Icon(Icons.fitness_center),
title: Text(plan.name),
subtitle: Text("${plan.totalExercises} exercises"),
onTap: () {
final newWorkout = ProgramWorkoutEntity(
id: IdGenerator.generate(),
programId: week.programId,
weekId: week.id,
day: dayNum.toString(),
type: 'workout',
name: plan.name,
refId: plan.id,
description: "${plan.sections.length} sections",
completed: false,
);
onAddWorkout(newWorkout);
Navigator.pop(context);
},
),
)
.toList(),
],
),
),
);
}
}

View File

@@ -0,0 +1,109 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/repositories/chat_repository.dart';
import 'package:trainhub_flutter/presentation/chat/chat_state.dart';
part 'chat_controller.g.dart';
@riverpod
class ChatController extends _$ChatController {
late final ChatRepository _repo;
@override
Future<ChatState> build() async {
_repo = getIt<ChatRepository>();
final sessions = await _repo.getAllSessions();
return ChatState(sessions: sessions);
}
Future<void> createSession() async {
final session = await _repo.createSession();
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
ChatState(sessions: sessions, activeSession: session),
);
}
Future<void> loadSession(String id) async {
final session = await _repo.getSession(id);
if (session == null) return;
final messages = await _repo.getMessages(id);
final current = state.valueOrNull ?? const ChatState();
state = AsyncValue.data(
current.copyWith(activeSession: session, messages: messages),
);
}
Future<void> deleteSession(String id) async {
await _repo.deleteSession(id);
final sessions = await _repo.getAllSessions();
final current = state.valueOrNull ?? const ChatState();
state = AsyncValue.data(
current.copyWith(
sessions: sessions,
activeSession:
current.activeSession?.id == id ? null : current.activeSession,
messages: current.activeSession?.id == id ? [] : current.messages,
),
);
}
Future<void> sendMessage(String content) async {
final current = state.valueOrNull;
if (current == null) return;
String sessionId;
if (current.activeSession == null) {
final session = await _repo.createSession();
sessionId = session.id;
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
current.copyWith(sessions: sessions, activeSession: session),
);
} else {
sessionId = current.activeSession!.id;
}
await _repo.addMessage(
sessionId: sessionId,
role: 'user',
content: content,
);
final messagesAfterUser = await _repo.getMessages(sessionId);
state = AsyncValue.data(
state.valueOrNull!.copyWith(messages: messagesAfterUser, isTyping: true),
);
await Future<void>.delayed(const Duration(seconds: 1));
final String response = _getMockResponse(content);
await _repo.addMessage(
sessionId: sessionId,
role: 'assistant',
content: response,
);
final messagesAfterAi = await _repo.getMessages(sessionId);
if (messagesAfterAi.length <= 2) {
final title = content.length > 30
? '${content.substring(0, 30)}...'
: content;
await _repo.updateSessionTitle(sessionId, title);
}
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
state.valueOrNull!.copyWith(
messages: messagesAfterAi,
isTyping: false,
sessions: sessions,
),
);
}
String _getMockResponse(String input) {
final String lower = input.toLowerCase();
if (lower.contains('plan') || lower.contains('program')) {
return "I can help you design a training plan! What are your goals? Strength, hypertrophy, or endurance?";
} else if (lower.contains('squat') || lower.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 (lower.contains('nutrition') || lower.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

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$chatControllerHash() => r'44a3d0e906eaad16f7a9c292fe847b8bd144c835';
/// See also [ChatController].
@ProviderFor(ChatController)
final chatControllerProvider =
AutoDisposeAsyncNotifierProvider<ChatController, ChatState>.internal(
ChatController.new,
name: r'chatControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$chatControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ChatController = AutoDisposeAsyncNotifier<ChatState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,766 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/presentation/chat/chat_controller.dart';
import 'package:trainhub_flutter/presentation/chat/chat_state.dart';
@RoutePage()
class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key});
@override
ConsumerState<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends ConsumerState<ChatPage> {
final TextEditingController _inputController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final FocusNode _inputFocusNode = FocusNode();
String? _hoveredSessionId;
@override
void dispose() {
_inputController.dispose();
_scrollController.dispose();
_inputFocusNode.dispose();
super.dispose();
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _sendMessage(ChatController controller) {
final text = _inputController.text.trim();
if (text.isNotEmpty) {
controller.sendMessage(text);
_inputController.clear();
_inputFocusNode.requestFocus();
}
}
String _formatTimestamp(String timestamp) {
try {
final dt = DateTime.parse(timestamp);
final now = DateTime.now();
final hour = dt.hour.toString().padLeft(2, '0');
final minute = dt.minute.toString().padLeft(2, '0');
if (dt.year == now.year &&
dt.month == now.month &&
dt.day == now.day) {
return '$hour:$minute';
}
return '${dt.day}/${dt.month} $hour:$minute';
} catch (_) {
return '';
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(chatControllerProvider);
final controller = ref.read(chatControllerProvider.notifier);
ref.listen(chatControllerProvider, (prev, next) {
if (next.hasValue &&
(prev?.value?.messages.length ?? 0) <
next.value!.messages.length) {
_scrollToBottom();
}
if (next.hasValue && next.value!.isTyping && !(prev?.value?.isTyping ?? false)) {
_scrollToBottom();
}
});
return Scaffold(
backgroundColor: AppColors.surface,
body: Row(
children: [
// --- Side Panel ---
_buildSidePanel(state, controller),
// --- Main Chat Area ---
Expanded(
child: _buildChatArea(state, controller),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Side Panel
// ---------------------------------------------------------------------------
Widget _buildSidePanel(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
return Container(
width: 250,
decoration: const BoxDecoration(
color: AppColors.surfaceContainer,
border: Border(
right: BorderSide(color: AppColors.border, width: 1),
),
),
child: Column(
children: [
// New Chat button
Padding(
padding: const EdgeInsets.all(UIConstants.spacing12),
child: SizedBox(
width: double.infinity,
child: _NewChatButton(onPressed: controller.createSession),
),
),
const Divider(height: 1, color: AppColors.border),
// Session list
Expanded(
child: asyncState.when(
data: (data) {
if (data.sessions.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing24),
child: Text(
'No conversations yet',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 13,
),
),
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(
vertical: UIConstants.spacing8,
),
itemCount: data.sessions.length,
itemBuilder: (context, index) {
final session = data.sessions[index];
final isActive =
session.id == data.activeSession?.id;
return _buildSessionTile(
session: session,
isActive: isActive,
controller: controller,
);
},
);
},
error: (_, __) => Center(
child: Text(
'Error loading sessions',
style: TextStyle(color: AppColors.textMuted, fontSize: 13),
),
),
loading: () => const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.textMuted,
),
),
),
),
),
],
),
);
}
Widget _buildSessionTile({
required ChatSessionEntity session,
required bool isActive,
required ChatController controller,
}) {
final isHovered = _hoveredSessionId == session.id;
return MouseRegion(
onEnter: (_) => setState(() => _hoveredSessionId = session.id),
onExit: (_) => setState(() {
if (_hoveredSessionId == session.id) {
_hoveredSessionId = null;
}
}),
child: GestureDetector(
onTap: () => controller.loadSession(session.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
margin: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing8,
vertical: 2,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
decoration: BoxDecoration(
color: isActive
? AppColors.zinc700.withValues(alpha: 0.7)
: isHovered
? AppColors.zinc800.withValues(alpha: 0.6)
: Colors.transparent,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: isActive
? Border.all(
color: AppColors.accent.withValues(alpha: 0.3),
width: 1,
)
: null,
),
child: Row(
children: [
Icon(
Icons.chat_bubble_outline_rounded,
size: 14,
color: isActive ? AppColors.accent : AppColors.textMuted,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
session.title ?? 'New Chat',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isActive
? AppColors.textPrimary
: AppColors.textSecondary,
fontSize: 13,
fontWeight:
isActive ? FontWeight.w500 : FontWeight.normal,
),
),
),
// Delete button appears on hover
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: isHovered ? 1.0 : 0.0,
child: IgnorePointer(
ignoring: !isHovered,
child: SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 14,
splashRadius: 14,
icon: const Icon(
Icons.delete_outline_rounded,
color: AppColors.textMuted,
),
onPressed: () => controller.deleteSession(session.id),
tooltip: 'Delete',
),
),
),
),
],
),
),
),
);
}
// ---------------------------------------------------------------------------
// Chat Area
// ---------------------------------------------------------------------------
Widget _buildChatArea(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
return Column(
children: [
// Messages
Expanded(
child: asyncState.when(
data: (data) {
if (data.messages.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing16,
),
itemCount:
data.messages.length + (data.isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == data.messages.length) {
return const _TypingIndicator();
}
final msg = data.messages[index];
return _MessageBubble(
message: msg,
formattedTime: _formatTimestamp(msg.createdAt),
);
},
);
},
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline_rounded,
color: AppColors.destructive,
size: 40,
),
const SizedBox(height: UIConstants.spacing12),
Text(
'Something went wrong',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
],
),
),
loading: () => const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.textMuted,
),
),
),
),
),
// Input area
_buildInputBar(asyncState, controller),
],
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.auto_awesome_outlined,
size: 32,
color: AppColors.textMuted,
),
),
const SizedBox(height: UIConstants.spacing16),
const Text(
'Ask me anything about your training!',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: UIConstants.spacing8),
const Text(
'Start a conversation to get personalized advice.',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 13,
),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Input Bar
// ---------------------------------------------------------------------------
Widget _buildInputBar(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
final isTyping = asyncState.valueOrNull?.isTyping ?? false;
return Container(
decoration: const BoxDecoration(
color: AppColors.surface,
border: Border(
top: BorderSide(color: AppColors.border, width: 1),
),
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
border: Border.all(color: AppColors.border, width: 1),
),
child: TextField(
controller: _inputController,
focusNode: _inputFocusNode,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
),
maxLines: 4,
minLines: 1,
decoration: InputDecoration(
hintText: 'Type a message...',
hintStyle: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onSubmitted: (_) => _sendMessage(controller),
textInputAction: TextInputAction.send,
),
),
),
const SizedBox(width: UIConstants.spacing8),
SizedBox(
width: 40,
height: 40,
child: Material(
color: isTyping ? AppColors.zinc700 : AppColors.accent,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
child: InkWell(
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
onTap: isTyping ? null : () => _sendMessage(controller),
child: Icon(
Icons.arrow_upward_rounded,
color: isTyping ? AppColors.textMuted : AppColors.zinc950,
size: 20,
),
),
),
),
],
),
);
}
}
// =============================================================================
// New Chat Button
// =============================================================================
class _NewChatButton extends StatefulWidget {
const _NewChatButton({required this.onPressed});
final VoidCallback onPressed;
@override
State<_NewChatButton> createState() => _NewChatButtonState();
}
class _NewChatButtonState extends State<_NewChatButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: _isHovered
? AppColors.zinc700
: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: Border.all(color: AppColors.border, width: 1),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.circular(UIConstants.smallBorderRadius),
onTap: widget.onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.add_rounded,
size: 16,
color: AppColors.textSecondary,
),
SizedBox(width: UIConstants.spacing8),
Text(
'New Chat',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
);
}
}
// =============================================================================
// Message Bubble
// =============================================================================
class _MessageBubble extends StatelessWidget {
const _MessageBubble({
required this.message,
required this.formattedTime,
});
final ChatMessageEntity message;
final String formattedTime;
@override
Widget build(BuildContext context) {
final isUser = message.isUser;
final maxWidth = MediaQuery.of(context).size.width * 0.55;
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing12),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.auto_awesome_rounded,
size: 14,
color: AppColors.accent,
),
),
const SizedBox(width: UIConstants.spacing8),
],
Flexible(
child: Column(
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
constraints: BoxConstraints(maxWidth: maxWidth),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: isUser
? AppColors.zinc700
: AppColors.surfaceContainer,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft:
isUser ? const Radius.circular(16) : const Radius.circular(4),
bottomRight:
isUser ? const Radius.circular(4) : const Radius.circular(16),
),
border: isUser
? null
: Border.all(color: AppColors.border, width: 1),
),
child: SelectableText(
message.content,
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
height: 1.5,
),
),
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
formattedTime,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 11,
),
),
),
],
),
),
if (isUser) ...[
const SizedBox(width: UIConstants.spacing8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.person_rounded,
size: 14,
color: AppColors.accent,
),
),
],
],
),
);
}
}
// =============================================================================
// Typing Indicator (3 animated bouncing dots)
// =============================================================================
class _TypingIndicator extends StatefulWidget {
const _TypingIndicator();
@override
State<_TypingIndicator> createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<_TypingIndicator>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.auto_awesome_rounded,
size: 14,
color: AppColors.accent,
),
),
const SizedBox(width: UIConstants.spacing8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(16),
),
border: Border.all(color: AppColors.border, width: 1),
),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
// Stagger each dot by 0.2 of the animation cycle
final delay = index * 0.2;
final t = (_controller.value - delay) % 1.0;
// Bounce: use a sin curve over the first half, rest at 0
final bounce =
t < 0.5 ? math.sin(t * math.pi * 2) * 4.0 : 0.0;
return Padding(
padding: EdgeInsets.only(
left: index == 0 ? 0 : 4,
),
child: Transform.translate(
offset: Offset(0, -bounce.abs()),
child: Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: AppColors.textMuted,
shape: BoxShape.circle,
),
),
),
);
}),
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
part 'chat_state.freezed.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
@Default([]) List<ChatMessageEntity> messages,
@Default(false) bool isTyping,
}) = _ChatState;
}

View File

@@ -0,0 +1,261 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ChatState {
List<ChatSessionEntity> get sessions => throw _privateConstructorUsedError;
ChatSessionEntity? get activeSession => throw _privateConstructorUsedError;
List<ChatMessageEntity> get messages => throw _privateConstructorUsedError;
bool get isTyping => throw _privateConstructorUsedError;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatStateCopyWith<ChatState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatStateCopyWith<$Res> {
factory $ChatStateCopyWith(ChatState value, $Res Function(ChatState) then) =
_$ChatStateCopyWithImpl<$Res, ChatState>;
@useResult
$Res call({
List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
List<ChatMessageEntity> messages,
bool isTyping,
});
$ChatSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class _$ChatStateCopyWithImpl<$Res, $Val extends ChatState>
implements $ChatStateCopyWith<$Res> {
_$ChatStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? messages = null,
Object? isTyping = null,
}) {
return _then(
_value.copyWith(
sessions: null == sessions
? _value.sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<ChatSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as ChatSessionEntity?,
messages: null == messages
? _value.messages
: messages // ignore: cast_nullable_to_non_nullable
as List<ChatMessageEntity>,
isTyping: null == isTyping
? _value.isTyping
: isTyping // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ChatSessionEntityCopyWith<$Res>? get activeSession {
if (_value.activeSession == null) {
return null;
}
return $ChatSessionEntityCopyWith<$Res>(_value.activeSession!, (value) {
return _then(_value.copyWith(activeSession: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ChatStateImplCopyWith<$Res>
implements $ChatStateCopyWith<$Res> {
factory _$$ChatStateImplCopyWith(
_$ChatStateImpl value,
$Res Function(_$ChatStateImpl) then,
) = __$$ChatStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
List<ChatMessageEntity> messages,
bool isTyping,
});
@override
$ChatSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class __$$ChatStateImplCopyWithImpl<$Res>
extends _$ChatStateCopyWithImpl<$Res, _$ChatStateImpl>
implements _$$ChatStateImplCopyWith<$Res> {
__$$ChatStateImplCopyWithImpl(
_$ChatStateImpl _value,
$Res Function(_$ChatStateImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? messages = null,
Object? isTyping = null,
}) {
return _then(
_$ChatStateImpl(
sessions: null == sessions
? _value._sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<ChatSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as ChatSessionEntity?,
messages: null == messages
? _value._messages
: messages // ignore: cast_nullable_to_non_nullable
as List<ChatMessageEntity>,
isTyping: null == isTyping
? _value.isTyping
: isTyping // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$ChatStateImpl implements _ChatState {
const _$ChatStateImpl({
final List<ChatSessionEntity> sessions = const [],
this.activeSession,
final List<ChatMessageEntity> messages = const [],
this.isTyping = false,
}) : _sessions = sessions,
_messages = messages;
final List<ChatSessionEntity> _sessions;
@override
@JsonKey()
List<ChatSessionEntity> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sessions);
}
@override
final ChatSessionEntity? activeSession;
final List<ChatMessageEntity> _messages;
@override
@JsonKey()
List<ChatMessageEntity> get messages {
if (_messages is EqualUnmodifiableListView) return _messages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_messages);
}
@override
@JsonKey()
final bool isTyping;
@override
String toString() {
return 'ChatState(sessions: $sessions, activeSession: $activeSession, messages: $messages, isTyping: $isTyping)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatStateImpl &&
const DeepCollectionEquality().equals(other._sessions, _sessions) &&
(identical(other.activeSession, activeSession) ||
other.activeSession == activeSession) &&
const DeepCollectionEquality().equals(other._messages, _messages) &&
(identical(other.isTyping, isTyping) ||
other.isTyping == isTyping));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_sessions),
activeSession,
const DeepCollectionEquality().hash(_messages),
isTyping,
);
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatStateImplCopyWith<_$ChatStateImpl> get copyWith =>
__$$ChatStateImplCopyWithImpl<_$ChatStateImpl>(this, _$identity);
}
abstract class _ChatState implements ChatState {
const factory _ChatState({
final List<ChatSessionEntity> sessions,
final ChatSessionEntity? activeSession,
final List<ChatMessageEntity> messages,
final bool isTyping,
}) = _$ChatStateImpl;
@override
List<ChatSessionEntity> get sessions;
@override
ChatSessionEntity? get activeSession;
@override
List<ChatMessageEntity> get messages;
@override
bool get isTyping;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatStateImplCopyWith<_$ChatStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
class ConfirmDialog {
ConfirmDialog._();
static Future<bool?> show(
BuildContext context, {
required String title,
required String message,
String confirmLabel = 'Delete',
Color? confirmColor,
}) {
final color = confirmColor ?? AppColors.destructive;
return showDialog<bool>(
context: context,
builder: (context) => Dialog(
backgroundColor: AppColors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.cardBorderRadius,
side: const BorderSide(color: AppColors.border),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: UIConstants.dialogWidth),
child: Padding(
padding: const EdgeInsets.all(UIConstants.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: UIConstants.spacing8),
Text(
message,
style: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: UIConstants.spacing24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
style: TextButton.styleFrom(
foregroundColor: AppColors.textSecondary,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
),
child: Text(
'Cancel',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: UIConstants.spacing8),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: color,
foregroundColor: AppColors.zinc50,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
),
child: Text(
confirmLabel,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
class TextInputDialog {
TextInputDialog._();
static Future<String?> show(
BuildContext context, {
required String title,
String? hintText,
String? initialValue,
String confirmLabel = 'Create',
}) {
final controller = TextEditingController(text: initialValue);
return showDialog<String>(
context: context,
builder: (context) => Dialog(
backgroundColor: AppColors.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.cardBorderRadius,
side: const BorderSide(color: AppColors.border),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: UIConstants.dialogWidth),
child: Padding(
padding: const EdgeInsets.all(UIConstants.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: UIConstants.spacing16),
TextField(
controller: controller,
autofocus: true,
style: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textPrimary,
),
cursorColor: AppColors.accent,
decoration: InputDecoration(
hintText: hintText,
hintStyle: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textMuted,
),
filled: true,
fillColor: AppColors.zinc950,
contentPadding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing12,
),
border: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: UIConstants.smallCardBorderRadius,
borderSide: const BorderSide(color: AppColors.accent),
),
),
onSubmitted: (value) {
final text = value.trim();
if (text.isNotEmpty) {
Navigator.of(context).pop(text);
}
},
),
const SizedBox(height: UIConstants.spacing24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
foregroundColor: AppColors.textSecondary,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
),
child: Text(
'Cancel',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: UIConstants.spacing8),
FilledButton(
onPressed: () {
final text = controller.text.trim();
if (text.isNotEmpty) {
Navigator.of(context).pop(text);
}
},
style: FilledButton.styleFrom(
backgroundColor: AppColors.accent,
foregroundColor: AppColors.zinc950,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
),
child: Text(
confirmLabel,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
class AppEmptyState extends StatelessWidget {
const AppEmptyState({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.actionLabel,
this.onAction,
});
final IconData icon;
final String title;
final String? subtitle;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UIConstants.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: BorderRadius.circular(UIConstants.borderRadius),
border: Border.all(color: AppColors.border),
),
child: Icon(
icon,
size: 28,
color: AppColors.textMuted,
),
),
const SizedBox(height: UIConstants.spacing16),
Text(
title,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
textAlign: TextAlign.center,
),
if (subtitle != null) ...[
const SizedBox(height: UIConstants.spacing8),
Text(
subtitle!,
style: GoogleFonts.inter(
fontSize: 14,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: UIConstants.spacing24),
FilledButton(
onPressed: onAction,
style: FilledButton.styleFrom(
backgroundColor: AppColors.accent,
foregroundColor: AppColors.zinc950,
shape: RoundedRectangleBorder(
borderRadius: UIConstants.smallCardBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing12,
),
),
child: Text(
actionLabel!,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
],
),
),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
class AppStatCard extends StatelessWidget {
const AppStatCard({
super.key,
required this.title,
required this.value,
required this.accentColor,
this.icon,
});
final String title;
final String value;
final Color accentColor;
final IconData? icon;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: UIConstants.cardBorderRadius,
border: Border.all(color: AppColors.border),
),
child: ClipRRect(
borderRadius: UIConstants.cardBorderRadius,
child: Row(
children: [
Container(
width: 4,
height: 72,
color: accentColor,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.cardPadding,
vertical: UIConstants.spacing12,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textMuted,
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
value,
style: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
],
),
),
if (icon != null)
Icon(
icon,
size: 20,
color: accentColor,
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/repositories/program_repository.dart';
import 'package:trainhub_flutter/presentation/home/home_state.dart';
part 'home_controller.g.dart';
@riverpod
class HomeController extends _$HomeController {
@override
Future<HomeState> build() async {
final ProgramRepository programRepo = getIt<ProgramRepository>();
final programs = await programRepo.getAllPrograms();
if (programs.isEmpty) return const HomeState();
final activeProgram = programs.first;
final workouts = await programRepo.getWorkouts(activeProgram.id);
final completed = workouts.where((w) => w.completed).toList();
final next = workouts.where((w) => !w.completed).firstOrNull;
return HomeState(
activeProgramName: activeProgram.name,
completedWorkouts: completed.length,
totalWorkouts: workouts.length,
nextWorkoutName: next?.name,
recentActivity: completed.reversed.take(5).toList(),
);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => build());
}
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homeControllerHash() => r'9704c0237cda64f1c13b4fd6db4fbc6eca9988f8';
/// See also [HomeController].
@ProviderFor(HomeController)
final homeControllerProvider =
AutoDisposeAsyncNotifierProvider<HomeController, HomeState>.internal(
HomeController.new,
name: r'homeControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$homeControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomeController = AutoDisposeAsyncNotifier<HomeState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,699 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
import 'package:trainhub_flutter/presentation/common/widgets/app_empty_state.dart';
import 'package:trainhub_flutter/presentation/common/widgets/app_stat_card.dart';
import 'package:trainhub_flutter/presentation/home/home_controller.dart';
import 'package:trainhub_flutter/presentation/home/home_state.dart';
@RoutePage()
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncState = ref.watch(homeControllerProvider);
return asyncState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Padding(
padding: const EdgeInsets.all(UIConstants.pagePadding),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 48,
color: AppColors.destructive,
),
const SizedBox(height: UIConstants.spacing16),
Text(
'Something went wrong',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: UIConstants.spacing8),
Text(
'$e',
style: GoogleFonts.inter(
fontSize: 13,
color: AppColors.textMuted,
),
textAlign: TextAlign.center,
),
const SizedBox(height: UIConstants.spacing24),
FilledButton.icon(
onPressed: () =>
ref.read(homeControllerProvider.notifier).refresh(),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Retry'),
),
],
),
),
),
data: (data) {
if (data.activeProgramName == null) {
return AppEmptyState(
icon: Icons.calendar_today_outlined,
title: 'No active program',
subtitle:
'Head to Calendar to create or select a training program to get started.',
actionLabel: 'Go to Calendar',
onAction: () {
AutoTabsRouter.of(context).setActiveIndex(3);
},
);
}
return _HomeContent(data: data);
},
);
}
}
class _HomeContent extends StatelessWidget {
final HomeState data;
const _HomeContent({required this.data});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(UIConstants.pagePadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// -- Welcome header --
_WelcomeHeader(programName: data.activeProgramName!),
const SizedBox(height: UIConstants.spacing24),
// -- Stat cards row --
_StatCardsRow(
completed: data.completedWorkouts,
total: data.totalWorkouts,
),
const SizedBox(height: UIConstants.spacing24),
// -- Next workout banner --
if (data.nextWorkoutName != null) ...[
_NextWorkoutBanner(workoutName: data.nextWorkoutName!),
const SizedBox(height: UIConstants.spacing24),
],
// -- Quick actions --
_QuickActionsRow(),
const SizedBox(height: UIConstants.spacing32),
// -- Recent activity --
_RecentActivitySection(activity: data.recentActivity),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Welcome header
// ---------------------------------------------------------------------------
class _WelcomeHeader extends StatelessWidget {
final String programName;
const _WelcomeHeader({required this.programName});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome back',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textMuted,
),
),
const SizedBox(height: UIConstants.spacing4),
Row(
children: [
Expanded(
child: Text(
programName,
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.accentMuted,
borderRadius: UIConstants.smallCardBorderRadius,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.fitness_center,
size: 14,
color: AppColors.accent,
),
const SizedBox(width: 6),
Text(
'Active Program',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.accent,
),
),
],
),
),
],
),
],
);
}
}
// ---------------------------------------------------------------------------
// Stat cards row
// ---------------------------------------------------------------------------
class _StatCardsRow extends StatelessWidget {
final int completed;
final int total;
const _StatCardsRow({required this.completed, required this.total});
@override
Widget build(BuildContext context) {
final progress = total == 0 ? 0 : (completed / total * 100).round();
return Row(
children: [
Expanded(
child: AppStatCard(
title: 'Completed',
value: '$completed',
icon: Icons.check_circle_outline,
accentColor: AppColors.success,
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: AppStatCard(
title: 'Total Workouts',
value: '$total',
icon: Icons.list_alt,
accentColor: AppColors.info,
),
),
const SizedBox(width: UIConstants.spacing16),
Expanded(
child: AppStatCard(
title: 'Progress',
value: '$progress%',
icon: Icons.trending_up,
accentColor: AppColors.purple,
),
),
],
);
}
}
// ---------------------------------------------------------------------------
// Next workout banner
// ---------------------------------------------------------------------------
class _NextWorkoutBanner extends StatelessWidget {
final String workoutName;
const _NextWorkoutBanner({required this.workoutName});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UIConstants.cardPadding),
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: UIConstants.cardBorderRadius,
border: Border.all(color: AppColors.border),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.accentMuted,
borderRadius: UIConstants.smallCardBorderRadius,
),
child: const Icon(
Icons.play_arrow_rounded,
color: AppColors.accent,
size: 22,
),
),
const SizedBox(width: UIConstants.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Up Next',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textMuted,
),
),
const SizedBox(height: 2),
Text(
workoutName,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: AppColors.textMuted,
size: 20,
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Quick actions row
// ---------------------------------------------------------------------------
class _QuickActionsRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quick Actions',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: UIConstants.spacing12),
Row(
children: [
_QuickActionButton(
icon: Icons.play_arrow_rounded,
label: 'New Workout',
color: AppColors.accent,
onTap: () {
AutoTabsRouter.of(context).setActiveIndex(1);
},
),
const SizedBox(width: UIConstants.spacing12),
_QuickActionButton(
icon: Icons.description_outlined,
label: 'View Plans',
color: AppColors.info,
onTap: () {
AutoTabsRouter.of(context).setActiveIndex(1);
},
),
const SizedBox(width: UIConstants.spacing12),
_QuickActionButton(
icon: Icons.chat_bubble_outline,
label: 'AI Chat',
color: AppColors.purple,
onTap: () {
AutoTabsRouter.of(context).setActiveIndex(4);
},
),
],
),
],
);
}
}
class _QuickActionButton extends StatefulWidget {
final IconData icon;
final String label;
final Color color;
final VoidCallback onTap;
const _QuickActionButton({
required this.icon,
required this.label,
required this.color,
required this.onTap,
});
@override
State<_QuickActionButton> createState() => _QuickActionButtonState();
}
class _QuickActionButtonState extends State<_QuickActionButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: UIConstants.animationDuration,
decoration: BoxDecoration(
color: _isHovered
? widget.color.withValues(alpha: 0.08)
: Colors.transparent,
borderRadius: UIConstants.smallCardBorderRadius,
border: Border.all(
color: _isHovered ? widget.color.withValues(alpha: 0.4) : AppColors.border,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onTap,
borderRadius: UIConstants.smallCardBorderRadius,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.icon,
size: 18,
color: widget.color,
),
const SizedBox(width: UIConstants.spacing8),
Text(
widget.label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _isHovered
? widget.color
: AppColors.textSecondary,
),
),
],
),
),
),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Recent activity section
// ---------------------------------------------------------------------------
class _RecentActivitySection extends StatelessWidget {
final List<ProgramWorkoutEntity> activity;
const _RecentActivitySection({required this.activity});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Recent Activity',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
),
if (activity.isNotEmpty)
Text(
'${activity.length} workout${activity.length == 1 ? '' : 's'}',
style: GoogleFonts.inter(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
),
const SizedBox(height: UIConstants.spacing12),
if (activity.isEmpty)
_EmptyActivity()
else
_ActivityList(activity: activity),
],
);
}
}
class _EmptyActivity extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 40,
horizontal: UIConstants.spacing24,
),
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: UIConstants.cardBorderRadius,
border: Border.all(color: AppColors.border),
),
child: Column(
children: [
Icon(
Icons.history,
size: 32,
color: AppColors.textMuted,
),
const SizedBox(height: UIConstants.spacing12),
Text(
'No completed workouts yet',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: UIConstants.spacing4),
Text(
'Your recent workout history will appear here.',
style: GoogleFonts.inter(
fontSize: 13,
color: AppColors.textMuted,
),
),
],
),
);
}
}
class _ActivityList extends StatelessWidget {
final List<ProgramWorkoutEntity> activity;
const _ActivityList({required this.activity});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: UIConstants.cardBorderRadius,
border: Border.all(color: AppColors.border),
),
child: ClipRRect(
borderRadius: UIConstants.cardBorderRadius,
child: Column(
children: [
for (int i = 0; i < activity.length; i++) ...[
if (i > 0)
const Divider(
height: 1,
thickness: 1,
color: AppColors.border,
),
_ActivityItem(workout: activity[i]),
],
],
),
),
);
}
}
class _ActivityItem extends StatefulWidget {
final ProgramWorkoutEntity workout;
const _ActivityItem({required this.workout});
@override
State<_ActivityItem> createState() => _ActivityItemState();
}
class _ActivityItemState extends State<_ActivityItem> {
bool _isHovered = false;
Color get _typeColor {
switch (widget.workout.type.toLowerCase()) {
case 'strength':
return AppColors.accent;
case 'cardio':
return AppColors.info;
case 'flexibility':
case 'mobility':
return AppColors.purple;
case 'rest':
return AppColors.textMuted;
default:
return AppColors.success;
}
}
IconData get _typeIcon {
switch (widget.workout.type.toLowerCase()) {
case 'strength':
return Icons.fitness_center;
case 'cardio':
return Icons.directions_run;
case 'flexibility':
case 'mobility':
return Icons.self_improvement;
case 'rest':
return Icons.bedtime_outlined;
default:
return Icons.check_circle;
}
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: UIConstants.animationDuration,
color: _isHovered
? AppColors.surfaceContainerHigh.withValues(alpha: 0.5)
: Colors.transparent,
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.cardPadding,
vertical: UIConstants.spacing12,
),
child: Row(
children: [
// Leading icon with color coding
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: widget.workout.completed
? _typeColor.withValues(alpha: 0.15)
: AppColors.zinc800,
borderRadius: UIConstants.smallCardBorderRadius,
),
child: Icon(
widget.workout.completed ? _typeIcon : Icons.circle_outlined,
size: 18,
color: widget.workout.completed
? _typeColor
: AppColors.textMuted,
),
),
const SizedBox(width: UIConstants.spacing12),
// Workout info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.workout.name ?? 'Workout',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'Week ${widget.workout.weekId} · Day ${widget.workout.day}',
style: GoogleFonts.inter(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
),
),
// Type badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _typeColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
),
child: Text(
widget.workout.type,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: _typeColor,
),
),
),
const SizedBox(width: UIConstants.spacing12),
// Status indicator
if (widget.workout.completed)
const Icon(
Icons.check_circle,
size: 18,
color: AppColors.success,
)
else
const Icon(
Icons.radio_button_unchecked,
size: 18,
color: AppColors.textMuted,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
part 'home_state.freezed.dart';
@freezed
class HomeState with _$HomeState {
const factory HomeState({
String? activeProgramName,
@Default(0) int completedWorkouts,
@Default(0) int totalWorkouts,
String? nextWorkoutName,
@Default([]) List<ProgramWorkoutEntity> recentActivity,
}) = _HomeState;
}

View File

@@ -0,0 +1,261 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'home_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$HomeState {
String? get activeProgramName => throw _privateConstructorUsedError;
int get completedWorkouts => throw _privateConstructorUsedError;
int get totalWorkouts => throw _privateConstructorUsedError;
String? get nextWorkoutName => throw _privateConstructorUsedError;
List<ProgramWorkoutEntity> get recentActivity =>
throw _privateConstructorUsedError;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$HomeStateCopyWith<HomeState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomeStateCopyWith<$Res> {
factory $HomeStateCopyWith(HomeState value, $Res Function(HomeState) then) =
_$HomeStateCopyWithImpl<$Res, HomeState>;
@useResult
$Res call({
String? activeProgramName,
int completedWorkouts,
int totalWorkouts,
String? nextWorkoutName,
List<ProgramWorkoutEntity> recentActivity,
});
}
/// @nodoc
class _$HomeStateCopyWithImpl<$Res, $Val extends HomeState>
implements $HomeStateCopyWith<$Res> {
_$HomeStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeProgramName = freezed,
Object? completedWorkouts = null,
Object? totalWorkouts = null,
Object? nextWorkoutName = freezed,
Object? recentActivity = null,
}) {
return _then(
_value.copyWith(
activeProgramName: freezed == activeProgramName
? _value.activeProgramName
: activeProgramName // ignore: cast_nullable_to_non_nullable
as String?,
completedWorkouts: null == completedWorkouts
? _value.completedWorkouts
: completedWorkouts // ignore: cast_nullable_to_non_nullable
as int,
totalWorkouts: null == totalWorkouts
? _value.totalWorkouts
: totalWorkouts // ignore: cast_nullable_to_non_nullable
as int,
nextWorkoutName: freezed == nextWorkoutName
? _value.nextWorkoutName
: nextWorkoutName // ignore: cast_nullable_to_non_nullable
as String?,
recentActivity: null == recentActivity
? _value.recentActivity
: recentActivity // ignore: cast_nullable_to_non_nullable
as List<ProgramWorkoutEntity>,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$HomeStateImplCopyWith<$Res>
implements $HomeStateCopyWith<$Res> {
factory _$$HomeStateImplCopyWith(
_$HomeStateImpl value,
$Res Function(_$HomeStateImpl) then,
) = __$$HomeStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String? activeProgramName,
int completedWorkouts,
int totalWorkouts,
String? nextWorkoutName,
List<ProgramWorkoutEntity> recentActivity,
});
}
/// @nodoc
class __$$HomeStateImplCopyWithImpl<$Res>
extends _$HomeStateCopyWithImpl<$Res, _$HomeStateImpl>
implements _$$HomeStateImplCopyWith<$Res> {
__$$HomeStateImplCopyWithImpl(
_$HomeStateImpl _value,
$Res Function(_$HomeStateImpl) _then,
) : super(_value, _then);
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? activeProgramName = freezed,
Object? completedWorkouts = null,
Object? totalWorkouts = null,
Object? nextWorkoutName = freezed,
Object? recentActivity = null,
}) {
return _then(
_$HomeStateImpl(
activeProgramName: freezed == activeProgramName
? _value.activeProgramName
: activeProgramName // ignore: cast_nullable_to_non_nullable
as String?,
completedWorkouts: null == completedWorkouts
? _value.completedWorkouts
: completedWorkouts // ignore: cast_nullable_to_non_nullable
as int,
totalWorkouts: null == totalWorkouts
? _value.totalWorkouts
: totalWorkouts // ignore: cast_nullable_to_non_nullable
as int,
nextWorkoutName: freezed == nextWorkoutName
? _value.nextWorkoutName
: nextWorkoutName // ignore: cast_nullable_to_non_nullable
as String?,
recentActivity: null == recentActivity
? _value._recentActivity
: recentActivity // ignore: cast_nullable_to_non_nullable
as List<ProgramWorkoutEntity>,
),
);
}
}
/// @nodoc
class _$HomeStateImpl implements _HomeState {
const _$HomeStateImpl({
this.activeProgramName,
this.completedWorkouts = 0,
this.totalWorkouts = 0,
this.nextWorkoutName,
final List<ProgramWorkoutEntity> recentActivity = const [],
}) : _recentActivity = recentActivity;
@override
final String? activeProgramName;
@override
@JsonKey()
final int completedWorkouts;
@override
@JsonKey()
final int totalWorkouts;
@override
final String? nextWorkoutName;
final List<ProgramWorkoutEntity> _recentActivity;
@override
@JsonKey()
List<ProgramWorkoutEntity> get recentActivity {
if (_recentActivity is EqualUnmodifiableListView) return _recentActivity;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recentActivity);
}
@override
String toString() {
return 'HomeState(activeProgramName: $activeProgramName, completedWorkouts: $completedWorkouts, totalWorkouts: $totalWorkouts, nextWorkoutName: $nextWorkoutName, recentActivity: $recentActivity)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeStateImpl &&
(identical(other.activeProgramName, activeProgramName) ||
other.activeProgramName == activeProgramName) &&
(identical(other.completedWorkouts, completedWorkouts) ||
other.completedWorkouts == completedWorkouts) &&
(identical(other.totalWorkouts, totalWorkouts) ||
other.totalWorkouts == totalWorkouts) &&
(identical(other.nextWorkoutName, nextWorkoutName) ||
other.nextWorkoutName == nextWorkoutName) &&
const DeepCollectionEquality().equals(
other._recentActivity,
_recentActivity,
));
}
@override
int get hashCode => Object.hash(
runtimeType,
activeProgramName,
completedWorkouts,
totalWorkouts,
nextWorkoutName,
const DeepCollectionEquality().hash(_recentActivity),
);
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$HomeStateImplCopyWith<_$HomeStateImpl> get copyWith =>
__$$HomeStateImplCopyWithImpl<_$HomeStateImpl>(this, _$identity);
}
abstract class _HomeState implements HomeState {
const factory _HomeState({
final String? activeProgramName,
final int completedWorkouts,
final int totalWorkouts,
final String? nextWorkoutName,
final List<ProgramWorkoutEntity> recentActivity,
}) = _$HomeStateImpl;
@override
String? get activeProgramName;
@override
int get completedWorkouts;
@override
int get totalWorkouts;
@override
String? get nextWorkoutName;
@override
List<ProgramWorkoutEntity> get recentActivity;
/// Create a copy of HomeState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$HomeStateImplCopyWith<_$HomeStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,177 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/core/utils/id_generator.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/entities/exercise.dart';
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
import 'package:trainhub_flutter/domain/entities/training_section.dart';
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_state.dart';
part 'plan_editor_controller.g.dart';
@riverpod
class PlanEditorController extends _$PlanEditorController {
late final TrainingPlanRepository _planRepo;
@override
Future<PlanEditorState> build(String planId) async {
_planRepo = getIt<TrainingPlanRepository>();
final ExerciseRepository exerciseRepo = getIt<ExerciseRepository>();
final plan = await _planRepo.getById(planId);
final exercises = await exerciseRepo.getAll();
return PlanEditorState(plan: plan, availableExercises: exercises);
}
void updatePlanName(String name) {
final current = state.valueOrNull;
if (current == null) return;
state = AsyncValue.data(
current.copyWith(plan: current.plan.copyWith(name: name), isDirty: true),
);
}
void addSection() {
final current = state.valueOrNull;
if (current == null) return;
final newSection = TrainingSectionEntity(
id: IdGenerator.generate(),
name: 'New Section',
);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(
sections: [...current.plan.sections, newSection],
),
isDirty: true,
),
);
}
void deleteSection(int index) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
sections.removeAt(index);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
void updateSectionName(int sectionIndex, String name) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
sections[sectionIndex] = sections[sectionIndex].copyWith(name: name);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
void addExerciseToSection(int sectionIndex, ExerciseEntity exercise) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
final newExercise = TrainingExerciseEntity(
instanceId: IdGenerator.generate(),
exerciseId: exercise.id,
name: exercise.name,
);
sections[sectionIndex] = sections[sectionIndex].copyWith(
exercises: [...sections[sectionIndex].exercises, newExercise],
);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
void removeExerciseFromSection(int sectionIndex, int exerciseIndex) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
final exercises = List<TrainingExerciseEntity>.from(
sections[sectionIndex].exercises,
);
exercises.removeAt(exerciseIndex);
sections[sectionIndex] = sections[sectionIndex].copyWith(
exercises: exercises,
);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
void updateExerciseParams(
int sectionIndex,
int exerciseIndex, {
int? sets,
int? value,
bool? isTime,
int? rest,
}) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
final exercises = List<TrainingExerciseEntity>.from(
sections[sectionIndex].exercises,
);
exercises[exerciseIndex] = exercises[exerciseIndex].copyWith(
sets: sets ?? exercises[exerciseIndex].sets,
value: value ?? exercises[exerciseIndex].value,
isTime: isTime ?? exercises[exerciseIndex].isTime,
rest: rest ?? exercises[exerciseIndex].rest,
);
sections[sectionIndex] = sections[sectionIndex].copyWith(
exercises: exercises,
);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
void reorderExercise(int sectionIndex, int oldIndex, int newIndex) {
final current = state.valueOrNull;
if (current == null) return;
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
final exercises = List<TrainingExerciseEntity>.from(
sections[sectionIndex].exercises,
);
if (oldIndex < newIndex) newIndex -= 1;
final item = exercises.removeAt(oldIndex);
exercises.insert(newIndex, item);
sections[sectionIndex] = sections[sectionIndex].copyWith(
exercises: exercises,
);
state = AsyncValue.data(
current.copyWith(
plan: current.plan.copyWith(sections: sections),
isDirty: true,
),
);
}
Future<void> save() async {
final current = state.valueOrNull;
if (current == null) return;
await _planRepo.update(current.plan);
state = AsyncValue.data(current.copyWith(isDirty: false));
}
}

View File

@@ -0,0 +1,175 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plan_editor_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$planEditorControllerHash() =>
r'4045493829126f28b3a58695b68ade53519c1412';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$PlanEditorController
extends BuildlessAutoDisposeAsyncNotifier<PlanEditorState> {
late final String planId;
FutureOr<PlanEditorState> build(String planId);
}
/// See also [PlanEditorController].
@ProviderFor(PlanEditorController)
const planEditorControllerProvider = PlanEditorControllerFamily();
/// See also [PlanEditorController].
class PlanEditorControllerFamily extends Family<AsyncValue<PlanEditorState>> {
/// See also [PlanEditorController].
const PlanEditorControllerFamily();
/// See also [PlanEditorController].
PlanEditorControllerProvider call(String planId) {
return PlanEditorControllerProvider(planId);
}
@override
PlanEditorControllerProvider getProviderOverride(
covariant PlanEditorControllerProvider provider,
) {
return call(provider.planId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'planEditorControllerProvider';
}
/// See also [PlanEditorController].
class PlanEditorControllerProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PlanEditorController,
PlanEditorState
> {
/// See also [PlanEditorController].
PlanEditorControllerProvider(String planId)
: this._internal(
() => PlanEditorController()..planId = planId,
from: planEditorControllerProvider,
name: r'planEditorControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$planEditorControllerHash,
dependencies: PlanEditorControllerFamily._dependencies,
allTransitiveDependencies:
PlanEditorControllerFamily._allTransitiveDependencies,
planId: planId,
);
PlanEditorControllerProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.planId,
}) : super.internal();
final String planId;
@override
FutureOr<PlanEditorState> runNotifierBuild(
covariant PlanEditorController notifier,
) {
return notifier.build(planId);
}
@override
Override overrideWith(PlanEditorController Function() create) {
return ProviderOverride(
origin: this,
override: PlanEditorControllerProvider._internal(
() => create()..planId = planId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
planId: planId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<PlanEditorController, PlanEditorState>
createElement() {
return _PlanEditorControllerProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PlanEditorControllerProvider && other.planId == planId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, planId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PlanEditorControllerRef
on AutoDisposeAsyncNotifierProviderRef<PlanEditorState> {
/// The parameter `planId` of this provider.
String get planId;
}
class _PlanEditorControllerProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PlanEditorController,
PlanEditorState
>
with PlanEditorControllerRef {
_PlanEditorControllerProviderElement(super.provider);
@override
String get planId => (origin as PlanEditorControllerProvider).planId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,80 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_controller.dart';
import 'package:trainhub_flutter/presentation/plan_editor/widgets/plan_section_card.dart';
@RoutePage()
class PlanEditorPage extends ConsumerWidget {
final String planId;
const PlanEditorPage({super.key, @PathParam('planId') required this.planId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(planEditorControllerProvider(planId));
final controller = ref.read(planEditorControllerProvider(planId).notifier);
return Scaffold(
appBar: AppBar(
title: state.when(
data: (data) => TextField(
controller: TextEditingController(text: data.plan.name)
..selection = TextSelection.fromPosition(
TextPosition(offset: data.plan.name.length),
),
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Plan Name',
),
style: Theme.of(context).textTheme.titleLarge,
onChanged: controller.updatePlanName,
),
error: (_, __) => const Text('Error'),
loading: () => const Text('Loading...'),
),
actions: [
state.maybeWhen(
data: (data) => IconButton(
icon: Icon(
Icons.save,
color: data.isDirty ? Theme.of(context).primaryColor : null,
),
onPressed: data.isDirty ? () => controller.save() : null,
),
orElse: () => const SizedBox.shrink(),
),
],
),
body: state.when(
data: (data) => ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: data.plan.sections.length + 1,
itemBuilder: (context, index) {
if (index == data.plan.sections.length) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ElevatedButton.icon(
onPressed: controller.addSection,
icon: const Icon(Icons.add),
label: const Text('Add Section'),
),
),
);
}
final section = data.plan.sections[index];
return PlanSectionCard(
section: section,
sectionIndex: index,
plan: data.plan,
availableExercises: data.availableExercises,
);
},
),
error: (e, s) => Center(child: Text('Error: $e')),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
import 'package:trainhub_flutter/domain/entities/exercise.dart';
part 'plan_editor_state.freezed.dart';
@freezed
class PlanEditorState with _$PlanEditorState {
const factory PlanEditorState({
required TrainingPlanEntity plan,
@Default(false) bool isDirty,
@Default([]) List<ExerciseEntity> availableExercises,
}) = _PlanEditorState;
}

Some files were not shown because too many files have changed in this diff Show More