Initial commit
This commit is contained in:
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(flutter pub get:*)",
|
||||
"Bash(flutter pub add:*)",
|
||||
"Bash(dart run build_runner:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(dir /s /b \"C:\\\\Users\\\\kaziu\\\\Desktop\\\\Trainhubv2\\\\trainhub_flutter\\\\lib\\\\presentation\\\\*.dart\")",
|
||||
"Bash(flutter build:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
/trainhub/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
30
.metadata
Normal file
30
.metadata
Normal file
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: windows
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
16
README.md
Normal file
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# trainhub_flutter
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
28
analysis_options.yaml
Normal file
28
analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
10
lib/core/constants/app_constants.dart
Normal file
10
lib/core/constants/app_constants.dart
Normal 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';
|
||||
}
|
||||
32
lib/core/constants/ui_constants.dart
Normal file
32
lib/core/constants/ui_constants.dart
Normal 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);
|
||||
}
|
||||
35
lib/core/extensions/context_extensions.dart
Normal file
35
lib/core/extensions/context_extensions.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/core/extensions/date_extensions.dart
Normal file
7
lib/core/extensions/date_extensions.dart
Normal 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();
|
||||
}
|
||||
15
lib/core/extensions/duration_extensions.dart
Normal file
15
lib/core/extensions/duration_extensions.dart
Normal 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')}';
|
||||
}
|
||||
}
|
||||
32
lib/core/router/app_router.dart
Normal file
32
lib/core/router/app_router.dart
Normal 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),
|
||||
];
|
||||
}
|
||||
191
lib/core/router/app_router.gr.dart
Normal file
191
lib/core/router/app_router.gr.dart
Normal 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}';
|
||||
}
|
||||
}
|
||||
41
lib/core/theme/app_colors.dart
Normal file
41
lib/core/theme/app_colors.dart
Normal 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);
|
||||
}
|
||||
209
lib/core/theme/app_theme.dart
Normal file
209
lib/core/theme/app_theme.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
103
lib/core/theme/app_typography.dart
Normal file
103
lib/core/theme/app_typography.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
9
lib/core/utils/id_generator.dart
Normal file
9
lib/core/utils/id_generator.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class IdGenerator {
|
||||
IdGenerator._();
|
||||
|
||||
static const Uuid _uuid = Uuid();
|
||||
|
||||
static String generate() => _uuid.v4();
|
||||
}
|
||||
20
lib/core/utils/json_utils.dart
Normal file
20
lib/core/utils/json_utils.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
152
lib/data/database/app_database.dart
Normal file
152
lib/data/database/app_database.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
6249
lib/data/database/app_database.g.dart
Normal file
6249
lib/data/database/app_database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
32
lib/data/database/daos/analysis_dao.dart
Normal file
32
lib/data/database/daos/analysis_dao.dart
Normal 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();
|
||||
}
|
||||
10
lib/data/database/daos/analysis_dao.g.dart
Normal file
10
lib/data/database/daos/analysis_dao.g.dart
Normal 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;
|
||||
}
|
||||
43
lib/data/database/daos/chat_dao.dart
Normal file
43
lib/data/database/daos/chat_dao.dart
Normal 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);
|
||||
}
|
||||
9
lib/data/database/daos/chat_dao.g.dart
Normal file
9
lib/data/database/daos/chat_dao.g.dart
Normal 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;
|
||||
}
|
||||
24
lib/data/database/daos/exercise_dao.dart
Normal file
24
lib/data/database/daos/exercise_dao.dart
Normal 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();
|
||||
}
|
||||
8
lib/data/database/daos/exercise_dao.g.dart
Normal file
8
lib/data/database/daos/exercise_dao.g.dart
Normal 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;
|
||||
}
|
||||
60
lib/data/database/daos/program_dao.dart
Normal file
60
lib/data/database/daos/program_dao.dart
Normal 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)));
|
||||
}
|
||||
10
lib/data/database/daos/program_dao.g.dart
Normal file
10
lib/data/database/daos/program_dao.g.dart
Normal 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;
|
||||
}
|
||||
24
lib/data/database/daos/training_plan_dao.dart
Normal file
24
lib/data/database/daos/training_plan_dao.dart
Normal 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();
|
||||
}
|
||||
8
lib/data/database/daos/training_plan_dao.g.dart
Normal file
8
lib/data/database/daos/training_plan_dao.g.dart
Normal 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;
|
||||
}
|
||||
28
lib/data/mappers/analysis_mapper.dart
Normal file
28
lib/data/mappers/analysis_mapper.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/data/mappers/chat_mapper.dart
Normal file
26
lib/data/mappers/chat_mapper.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/data/mappers/exercise_mapper.dart
Normal file
39
lib/data/mappers/exercise_mapper.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/data/mappers/program_mapper.dart
Normal file
68
lib/data/mappers/program_mapper.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/data/mappers/training_plan_mapper.dart
Normal file
87
lib/data/mappers/training_plan_mapper.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
90
lib/data/repositories/analysis_repository_impl.dart
Normal file
90
lib/data/repositories/analysis_repository_impl.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
84
lib/data/repositories/chat_repository_impl.dart
Normal file
84
lib/data/repositories/chat_repository_impl.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
53
lib/data/repositories/exercise_repository_impl.dart
Normal file
53
lib/data/repositories/exercise_repository_impl.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
151
lib/data/repositories/program_repository_impl.dart
Normal file
151
lib/data/repositories/program_repository_impl.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
40
lib/data/repositories/training_plan_repository_impl.dart
Normal file
40
lib/data/repositories/training_plan_repository_impl.dart
Normal 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
148
lib/database/database.dart
Normal 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
6242
lib/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
13
lib/domain/entities/analysis_session.dart
Normal file
13
lib/domain/entities/analysis_session.dart
Normal 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;
|
||||
}
|
||||
219
lib/domain/entities/analysis_session.freezed.dart
Normal file
219
lib/domain/entities/analysis_session.freezed.dart
Normal 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;
|
||||
}
|
||||
16
lib/domain/entities/annotation.dart
Normal file
16
lib/domain/entities/annotation.dart
Normal 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;
|
||||
}
|
||||
295
lib/domain/entities/annotation.freezed.dart
Normal file
295
lib/domain/entities/annotation.freezed.dart
Normal 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;
|
||||
}
|
||||
19
lib/domain/entities/chat_message.dart
Normal file
19
lib/domain/entities/chat_message.dart
Normal 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';
|
||||
}
|
||||
247
lib/domain/entities/chat_message.freezed.dart
Normal file
247
lib/domain/entities/chat_message.freezed.dart
Normal 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;
|
||||
}
|
||||
13
lib/domain/entities/chat_session.dart
Normal file
13
lib/domain/entities/chat_session.dart
Normal 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;
|
||||
}
|
||||
215
lib/domain/entities/chat_session.freezed.dart
Normal file
215
lib/domain/entities/chat_session.freezed.dart
Normal 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;
|
||||
}
|
||||
16
lib/domain/entities/exercise.dart
Normal file
16
lib/domain/entities/exercise.dart
Normal 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;
|
||||
}
|
||||
296
lib/domain/entities/exercise.freezed.dart
Normal file
296
lib/domain/entities/exercise.freezed.dart
Normal 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;
|
||||
}
|
||||
12
lib/domain/entities/program.dart
Normal file
12
lib/domain/entities/program.dart
Normal 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;
|
||||
}
|
||||
193
lib/domain/entities/program.freezed.dart
Normal file
193
lib/domain/entities/program.freezed.dart
Normal 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;
|
||||
}
|
||||
13
lib/domain/entities/program_week.dart
Normal file
13
lib/domain/entities/program_week.dart
Normal 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;
|
||||
}
|
||||
215
lib/domain/entities/program_week.freezed.dart
Normal file
215
lib/domain/entities/program_week.freezed.dart
Normal 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;
|
||||
}
|
||||
18
lib/domain/entities/program_workout.dart
Normal file
18
lib/domain/entities/program_workout.dart
Normal 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;
|
||||
}
|
||||
342
lib/domain/entities/program_workout.freezed.dart
Normal file
342
lib/domain/entities/program_workout.freezed.dart
Normal 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;
|
||||
}
|
||||
16
lib/domain/entities/training_exercise.dart
Normal file
16
lib/domain/entities/training_exercise.dart
Normal 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;
|
||||
}
|
||||
303
lib/domain/entities/training_exercise.freezed.dart
Normal file
303
lib/domain/entities/training_exercise.freezed.dart
Normal 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;
|
||||
}
|
||||
18
lib/domain/entities/training_plan.dart
Normal file
18
lib/domain/entities/training_plan.dart
Normal 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);
|
||||
}
|
||||
201
lib/domain/entities/training_plan.freezed.dart
Normal file
201
lib/domain/entities/training_plan.freezed.dart
Normal 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;
|
||||
}
|
||||
13
lib/domain/entities/training_section.dart
Normal file
13
lib/domain/entities/training_section.dart
Normal 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;
|
||||
}
|
||||
215
lib/domain/entities/training_section.freezed.dart
Normal file
215
lib/domain/entities/training_section.freezed.dart
Normal 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;
|
||||
}
|
||||
24
lib/domain/entities/workout_activity.dart
Normal file
24
lib/domain/entities/workout_activity.dart
Normal 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;
|
||||
}
|
||||
346
lib/domain/entities/workout_activity.freezed.dart
Normal file
346
lib/domain/entities/workout_activity.freezed.dart
Normal 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;
|
||||
}
|
||||
19
lib/domain/repositories/analysis_repository.dart
Normal file
19
lib/domain/repositories/analysis_repository.dart
Normal 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);
|
||||
}
|
||||
16
lib/domain/repositories/chat_repository.dart
Normal file
16
lib/domain/repositories/chat_repository.dart
Normal 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);
|
||||
}
|
||||
13
lib/domain/repositories/exercise_repository.dart
Normal file
13
lib/domain/repositories/exercise_repository.dart
Normal 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);
|
||||
}
|
||||
20
lib/domain/repositories/program_repository.dart
Normal file
20
lib/domain/repositories/program_repository.dart
Normal 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);
|
||||
}
|
||||
9
lib/domain/repositories/training_plan_repository.dart
Normal file
9
lib/domain/repositories/training_plan_repository.dart
Normal 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
50
lib/injection.dart
Normal 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
47
lib/main.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
107
lib/models/training_models.dart
Normal file
107
lib/models/training_models.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
87
lib/presentation/analysis/analysis_controller.dart
Normal file
87
lib/presentation/analysis/analysis_controller.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
30
lib/presentation/analysis/analysis_controller.g.dart
Normal file
30
lib/presentation/analysis/analysis_controller.g.dart
Normal 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
|
||||
232
lib/presentation/analysis/analysis_page.dart
Normal file
232
lib/presentation/analysis/analysis_page.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
14
lib/presentation/analysis/analysis_state.dart
Normal file
14
lib/presentation/analysis/analysis_state.dart
Normal 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;
|
||||
}
|
||||
244
lib/presentation/analysis/analysis_state.freezed.dart
Normal file
244
lib/presentation/analysis/analysis_state.freezed.dart
Normal 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;
|
||||
}
|
||||
40
lib/presentation/analysis/widgets/analysis_session_list.dart
Normal file
40
lib/presentation/analysis/widgets/analysis_session_list.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
470
lib/presentation/analysis/widgets/analysis_viewer.dart
Normal file
470
lib/presentation/analysis/widgets/analysis_viewer.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
lib/presentation/calendar/calendar_controller.dart
Normal file
125
lib/presentation/calendar/calendar_controller.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
30
lib/presentation/calendar/calendar_controller.g.dart
Normal file
30
lib/presentation/calendar/calendar_controller.g.dart
Normal 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
|
||||
116
lib/presentation/calendar/calendar_page.dart
Normal file
116
lib/presentation/calendar/calendar_page.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/presentation/calendar/calendar_state.dart
Normal file
20
lib/presentation/calendar/calendar_state.dart
Normal 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;
|
||||
}
|
||||
329
lib/presentation/calendar/calendar_state.freezed.dart
Normal file
329
lib/presentation/calendar/calendar_state.freezed.dart
Normal 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;
|
||||
}
|
||||
252
lib/presentation/calendar/widgets/program_selector.dart
Normal file
252
lib/presentation/calendar/widgets/program_selector.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
244
lib/presentation/calendar/widgets/program_week_view.dart
Normal file
244
lib/presentation/calendar/widgets/program_week_view.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/presentation/chat/chat_controller.dart
Normal file
109
lib/presentation/chat/chat_controller.dart
Normal 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?";
|
||||
}
|
||||
}
|
||||
26
lib/presentation/chat/chat_controller.g.dart
Normal file
26
lib/presentation/chat/chat_controller.g.dart
Normal 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
|
||||
766
lib/presentation/chat/chat_page.dart
Normal file
766
lib/presentation/chat/chat_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
lib/presentation/chat/chat_state.dart
Normal file
15
lib/presentation/chat/chat_state.dart
Normal 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;
|
||||
}
|
||||
261
lib/presentation/chat/chat_state.freezed.dart
Normal file
261
lib/presentation/chat/chat_state.freezed.dart
Normal 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;
|
||||
}
|
||||
105
lib/presentation/common/dialogs/confirm_dialog.dart
Normal file
105
lib/presentation/common/dialogs/confirm_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
143
lib/presentation/common/dialogs/text_input_dialog.dart
Normal file
143
lib/presentation/common/dialogs/text_input_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/presentation/common/widgets/app_empty_state.dart
Normal file
94
lib/presentation/common/widgets/app_empty_state.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/presentation/common/widgets/app_stat_card.dart
Normal file
85
lib/presentation/common/widgets/app_stat_card.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/presentation/home/home_controller.dart
Normal file
32
lib/presentation/home/home_controller.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
26
lib/presentation/home/home_controller.g.dart
Normal file
26
lib/presentation/home/home_controller.g.dart
Normal 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
|
||||
699
lib/presentation/home/home_page.dart
Normal file
699
lib/presentation/home/home_page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
lib/presentation/home/home_state.dart
Normal file
15
lib/presentation/home/home_state.dart
Normal 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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user