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