Initial commit
This commit is contained in:
72
lib/presentation/workout_session/widgets/activity_card.dart
Normal file
72
lib/presentation/workout_session/widgets/activity_card.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
|
||||
|
||||
class ActivityCard extends StatelessWidget {
|
||||
final WorkoutActivityEntity activity;
|
||||
|
||||
const ActivityCard({super.key, required this.activity});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isRest = activity.type == 'rest';
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
activity.name,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!isRest) ...[
|
||||
Text(
|
||||
"${activity.sectionName} • Set ${activity.setIndex}/${activity.totalSets}",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (activity.originalExercise != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildInfo(
|
||||
context,
|
||||
"Sets",
|
||||
"${activity.originalExercise!.sets}",
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_buildInfo(
|
||||
context,
|
||||
activity.originalExercise!.isTime ? "Secs" : "Reps",
|
||||
"${activity.originalExercise!.value}",
|
||||
),
|
||||
],
|
||||
),
|
||||
] else
|
||||
Text(
|
||||
"Resting...",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfo(BuildContext context, String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(value, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SessionControls extends StatelessWidget {
|
||||
final bool isRunning;
|
||||
final bool isFinished;
|
||||
final VoidCallback onPause;
|
||||
final VoidCallback onPlay;
|
||||
final VoidCallback onNext;
|
||||
final VoidCallback onPrevious;
|
||||
|
||||
const SessionControls({
|
||||
super.key,
|
||||
required this.isRunning,
|
||||
required this.isFinished,
|
||||
required this.onPause,
|
||||
required this.onPlay,
|
||||
required this.onNext,
|
||||
required this.onPrevious,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isFinished) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Finish Workout'),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
onPressed: onPrevious,
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
iconSize: 32,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
IconButton.filled(
|
||||
onPressed: isRunning ? onPause : onPlay,
|
||||
icon: Icon(isRunning ? Icons.pause : Icons.play_arrow),
|
||||
iconSize: 48,
|
||||
style: IconButton.styleFrom(padding: const EdgeInsets.all(16)),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
IconButton.filledTonal(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(Icons.skip_next),
|
||||
iconSize: 32,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SessionProgressBar extends StatelessWidget {
|
||||
final double progress;
|
||||
|
||||
const SessionProgressBar({super.key, required this.progress});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearProgressIndicator(
|
||||
value: progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
);
|
||||
}
|
||||
}
|
||||
182
lib/presentation/workout_session/workout_session_controller.dart
Normal file
182
lib/presentation/workout_session/workout_session_controller.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
|
||||
import 'package:trainhub_flutter/presentation/workout_session/workout_session_state.dart';
|
||||
import 'package:trainhub_flutter/injection.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
|
||||
|
||||
part 'workout_session_controller.g.dart';
|
||||
|
||||
@riverpod
|
||||
class WorkoutSessionController extends _$WorkoutSessionController {
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
Future<WorkoutSessionState> build(String planId) async {
|
||||
final planRepo = getIt<TrainingPlanRepository>();
|
||||
final plan = await planRepo.getById(planId);
|
||||
|
||||
final activities = _buildSequence(plan);
|
||||
ref.onDispose(() => _timer?.cancel());
|
||||
|
||||
final initialState = WorkoutSessionState(activities: activities);
|
||||
|
||||
if (activities.isNotEmpty) {
|
||||
final first = activities.first;
|
||||
return initialState.copyWith(timeRemaining: first.duration);
|
||||
}
|
||||
return initialState;
|
||||
}
|
||||
|
||||
List<WorkoutActivityEntity> _buildSequence(TrainingPlanEntity plan) {
|
||||
final List<WorkoutActivityEntity> seq = [];
|
||||
for (final section in plan.sections) {
|
||||
for (final ex in section.exercises) {
|
||||
for (int s = 1; s <= ex.sets; s++) {
|
||||
seq.add(
|
||||
WorkoutActivityEntity(
|
||||
id: '${ex.instanceId}-s$s-work',
|
||||
name: ex.name,
|
||||
type: 'work',
|
||||
duration: ex.isTime ? ex.value : 0,
|
||||
originalExercise: ex,
|
||||
sectionName: section.name,
|
||||
setIndex: s,
|
||||
totalSets: ex.sets,
|
||||
),
|
||||
);
|
||||
final bool isLastOfWorkout =
|
||||
s == ex.sets &&
|
||||
section.exercises.last == ex &&
|
||||
plan.sections.last == section;
|
||||
if (ex.rest > 0 && !isLastOfWorkout) {
|
||||
seq.add(
|
||||
WorkoutActivityEntity(
|
||||
id: '${ex.instanceId}-s$s-rest',
|
||||
name: 'Rest',
|
||||
type: 'rest',
|
||||
duration: ex.rest,
|
||||
sectionName: section.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
void startTimer() {
|
||||
if (_timer != null && _timer!.isActive) return;
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), _tick);
|
||||
final currentState = state.value;
|
||||
if (currentState != null) {
|
||||
state = AsyncValue.data(currentState.copyWith(isRunning: true));
|
||||
}
|
||||
}
|
||||
|
||||
void pauseTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
final currentState = state.value;
|
||||
if (currentState != null) {
|
||||
state = AsyncValue.data(currentState.copyWith(isRunning: false));
|
||||
}
|
||||
}
|
||||
|
||||
void _tick(Timer timer) {
|
||||
if (state.value?.isFinished ?? true) return;
|
||||
var currentState = state.value!;
|
||||
|
||||
var newState = currentState.copyWith(
|
||||
totalTimeElapsed: currentState.totalTimeElapsed + 1,
|
||||
);
|
||||
final activity = newState.currentActivity;
|
||||
|
||||
if (activity != null && activity.duration > 0 && newState.isRunning) {
|
||||
if (newState.timeRemaining > 0) {
|
||||
newState = newState.copyWith(timeRemaining: newState.timeRemaining - 1);
|
||||
} else {
|
||||
state = AsyncValue.data(newState); // update interim state before next
|
||||
_goNext(newState);
|
||||
return;
|
||||
}
|
||||
}
|
||||
state = AsyncValue.data(newState);
|
||||
}
|
||||
|
||||
void next() {
|
||||
final currentState = state.value;
|
||||
if (currentState != null) _goNext(currentState);
|
||||
}
|
||||
|
||||
void _goNext(WorkoutSessionState currentState) {
|
||||
if (currentState.currentIndex < currentState.activities.length - 1) {
|
||||
final nextIndex = currentState.currentIndex + 1;
|
||||
final nextActivity = currentState.activities[nextIndex];
|
||||
|
||||
final newState = currentState.copyWith(
|
||||
currentIndex: nextIndex,
|
||||
timeRemaining: nextActivity.duration,
|
||||
);
|
||||
|
||||
state = AsyncValue.data(newState);
|
||||
|
||||
if (nextActivity.isRest) {
|
||||
startTimer();
|
||||
} else {
|
||||
pauseTimer();
|
||||
}
|
||||
} else {
|
||||
_finish();
|
||||
}
|
||||
}
|
||||
|
||||
void previous() {
|
||||
final currentState = state.value;
|
||||
if (currentState != null && currentState.currentIndex > 0) {
|
||||
final prevIndex = currentState.currentIndex - 1;
|
||||
final prevActivity = currentState.activities[prevIndex];
|
||||
|
||||
state = AsyncValue.data(
|
||||
currentState.copyWith(
|
||||
currentIndex: prevIndex,
|
||||
timeRemaining: prevActivity.duration,
|
||||
),
|
||||
);
|
||||
|
||||
pauseTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void jumpTo(int index) {
|
||||
final currentState = state.value;
|
||||
if (currentState != null &&
|
||||
index >= 0 &&
|
||||
index < currentState.activities.length) {
|
||||
final activity = currentState.activities[index];
|
||||
|
||||
state = AsyncValue.data(
|
||||
currentState.copyWith(
|
||||
currentIndex: index,
|
||||
timeRemaining: activity.duration,
|
||||
),
|
||||
);
|
||||
|
||||
if (activity.isRest) {
|
||||
startTimer();
|
||||
} else {
|
||||
pauseTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
pauseTimer();
|
||||
final currentState = state.value;
|
||||
if (currentState != null) {
|
||||
state = AsyncValue.data(currentState.copyWith(isFinished: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'workout_session_controller.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$workoutSessionControllerHash() =>
|
||||
r'd3f53d72c80963634c6edaeb44aa5b04c9ffba6d';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$WorkoutSessionController
|
||||
extends BuildlessAutoDisposeAsyncNotifier<WorkoutSessionState> {
|
||||
late final String planId;
|
||||
|
||||
FutureOr<WorkoutSessionState> build(String planId);
|
||||
}
|
||||
|
||||
/// See also [WorkoutSessionController].
|
||||
@ProviderFor(WorkoutSessionController)
|
||||
const workoutSessionControllerProvider = WorkoutSessionControllerFamily();
|
||||
|
||||
/// See also [WorkoutSessionController].
|
||||
class WorkoutSessionControllerFamily
|
||||
extends Family<AsyncValue<WorkoutSessionState>> {
|
||||
/// See also [WorkoutSessionController].
|
||||
const WorkoutSessionControllerFamily();
|
||||
|
||||
/// See also [WorkoutSessionController].
|
||||
WorkoutSessionControllerProvider call(String planId) {
|
||||
return WorkoutSessionControllerProvider(planId);
|
||||
}
|
||||
|
||||
@override
|
||||
WorkoutSessionControllerProvider getProviderOverride(
|
||||
covariant WorkoutSessionControllerProvider provider,
|
||||
) {
|
||||
return call(provider.planId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'workoutSessionControllerProvider';
|
||||
}
|
||||
|
||||
/// See also [WorkoutSessionController].
|
||||
class WorkoutSessionControllerProvider
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderImpl<
|
||||
WorkoutSessionController,
|
||||
WorkoutSessionState
|
||||
> {
|
||||
/// See also [WorkoutSessionController].
|
||||
WorkoutSessionControllerProvider(String planId)
|
||||
: this._internal(
|
||||
() => WorkoutSessionController()..planId = planId,
|
||||
from: workoutSessionControllerProvider,
|
||||
name: r'workoutSessionControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$workoutSessionControllerHash,
|
||||
dependencies: WorkoutSessionControllerFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
WorkoutSessionControllerFamily._allTransitiveDependencies,
|
||||
planId: planId,
|
||||
);
|
||||
|
||||
WorkoutSessionControllerProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.planId,
|
||||
}) : super.internal();
|
||||
|
||||
final String planId;
|
||||
|
||||
@override
|
||||
FutureOr<WorkoutSessionState> runNotifierBuild(
|
||||
covariant WorkoutSessionController notifier,
|
||||
) {
|
||||
return notifier.build(planId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(WorkoutSessionController Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: WorkoutSessionControllerProvider._internal(
|
||||
() => create()..planId = planId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
planId: planId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
WorkoutSessionController,
|
||||
WorkoutSessionState
|
||||
>
|
||||
createElement() {
|
||||
return _WorkoutSessionControllerProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is WorkoutSessionControllerProvider && other.planId == planId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, planId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin WorkoutSessionControllerRef
|
||||
on AutoDisposeAsyncNotifierProviderRef<WorkoutSessionState> {
|
||||
/// The parameter `planId` of this provider.
|
||||
String get planId;
|
||||
}
|
||||
|
||||
class _WorkoutSessionControllerProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<
|
||||
WorkoutSessionController,
|
||||
WorkoutSessionState
|
||||
>
|
||||
with WorkoutSessionControllerRef {
|
||||
_WorkoutSessionControllerProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get planId => (origin as WorkoutSessionControllerProvider).planId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
595
lib/presentation/workout_session/workout_session_page.dart
Normal file
595
lib/presentation/workout_session/workout_session_page.dart
Normal file
@@ -0,0 +1,595 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
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/presentation/workout_session/workout_session_controller.dart';
|
||||
import 'package:trainhub_flutter/presentation/workout_session/workout_session_state.dart';
|
||||
import 'package:trainhub_flutter/presentation/workout_session/widgets/activity_card.dart';
|
||||
import 'package:trainhub_flutter/presentation/workout_session/widgets/session_controls.dart';
|
||||
import 'package:trainhub_flutter/presentation/workout_session/widgets/session_progress_bar.dart';
|
||||
|
||||
@RoutePage()
|
||||
class WorkoutSessionPage extends ConsumerWidget {
|
||||
final String planId;
|
||||
|
||||
const WorkoutSessionPage({
|
||||
super.key,
|
||||
@PathParam('planId') required this.planId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncState = ref.watch(workoutSessionControllerProvider(planId));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.zinc950,
|
||||
body: asyncState.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accent,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
error: (err, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: AppColors.destructive,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Failed to load workout',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
'$err',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (state) {
|
||||
final controller = ref.read(
|
||||
workoutSessionControllerProvider(planId).notifier,
|
||||
);
|
||||
|
||||
if (state.isFinished) {
|
||||
return _CompletionScreen(
|
||||
totalTimeElapsed: state.totalTimeElapsed,
|
||||
);
|
||||
}
|
||||
|
||||
final isRest = state.currentActivity?.isRest ?? false;
|
||||
|
||||
return _ActiveSessionView(
|
||||
state: state,
|
||||
isRest: isRest,
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Active session view (gradient background + timer + controls)
|
||||
// ---------------------------------------------------------------------------
|
||||
class _ActiveSessionView extends StatelessWidget {
|
||||
final WorkoutSessionState state;
|
||||
final bool isRest;
|
||||
final WorkoutSessionController controller;
|
||||
|
||||
const _ActiveSessionView({
|
||||
required this.state,
|
||||
required this.isRest,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Compute the time progress for the circular ring.
|
||||
final activity = state.currentActivity;
|
||||
final double timeProgress;
|
||||
if (activity != null && activity.duration > 0) {
|
||||
timeProgress = 1.0 - (state.timeRemaining / activity.duration);
|
||||
} else {
|
||||
timeProgress = 0.0;
|
||||
}
|
||||
|
||||
final accentTint = isRest
|
||||
? AppColors.info.withValues(alpha: 0.06)
|
||||
: AppColors.accent.withValues(alpha: 0.06);
|
||||
final ringColor = isRest ? AppColors.info : AppColors.accent;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppColors.zinc950,
|
||||
accentTint,
|
||||
AppColors.zinc950,
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// -- Top progress bar --
|
||||
SessionProgressBar(progress: state.progress),
|
||||
|
||||
// -- Elapsed time badge --
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: UIConstants.spacing16,
|
||||
right: UIConstants.spacing24,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc800.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.border.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
size: 14,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDuration(state.totalTimeElapsed),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// -- Central content --
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Activity info card
|
||||
if (activity != null)
|
||||
ActivityCard(activity: activity),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing32),
|
||||
|
||||
// Circular progress ring + timer
|
||||
_CircularTimerDisplay(
|
||||
timeRemaining: state.timeRemaining,
|
||||
progress: timeProgress,
|
||||
ringColor: ringColor,
|
||||
isRunning: state.isRunning,
|
||||
isTimeBased: activity?.isTimeBased ?? false,
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// "Up next" pill
|
||||
if (state.nextActivity != null)
|
||||
_UpNextPill(
|
||||
nextActivityName: state.nextActivity!.name,
|
||||
isNextRest: state.nextActivity!.isRest,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// -- Bottom controls --
|
||||
SessionControls(
|
||||
isRunning: state.isRunning,
|
||||
isFinished: state.isFinished,
|
||||
onPause: controller.pauseTimer,
|
||||
onPlay: controller.startTimer,
|
||||
onNext: controller.next,
|
||||
onPrevious: controller.previous,
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final m = seconds ~/ 60;
|
||||
final s = seconds % 60;
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Circular timer with arc progress ring
|
||||
// ---------------------------------------------------------------------------
|
||||
class _CircularTimerDisplay extends StatelessWidget {
|
||||
final int timeRemaining;
|
||||
final double progress;
|
||||
final Color ringColor;
|
||||
final bool isRunning;
|
||||
final bool isTimeBased;
|
||||
|
||||
const _CircularTimerDisplay({
|
||||
required this.timeRemaining,
|
||||
required this.progress,
|
||||
required this.ringColor,
|
||||
required this.isRunning,
|
||||
required this.isTimeBased,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double size = 220;
|
||||
const double strokeWidth = 6.0;
|
||||
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background track
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
value: 1.0,
|
||||
strokeWidth: strokeWidth,
|
||||
color: AppColors.zinc800.withValues(alpha: 0.5),
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
|
||||
// Progress arc
|
||||
if (isTimeBased)
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress.clamp(0.0, 1.0),
|
||||
strokeWidth: strokeWidth,
|
||||
color: ringColor,
|
||||
backgroundColor: Colors.transparent,
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
|
||||
// Glow behind the timer text
|
||||
Container(
|
||||
width: size * 0.7,
|
||||
height: size * 0.7,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ringColor.withValues(alpha: 0.08),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Timer text
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(timeRemaining),
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 52,
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 2,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (!isTimeBased)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'UNTIMED',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isTimeBased && !isRunning)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'PAUSED',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(int seconds) {
|
||||
final m = seconds ~/ 60;
|
||||
final s = seconds % 60;
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// "Up next" pill
|
||||
// ---------------------------------------------------------------------------
|
||||
class _UpNextPill extends StatelessWidget {
|
||||
final String nextActivityName;
|
||||
final bool isNextRest;
|
||||
|
||||
const _UpNextPill({
|
||||
required this.nextActivityName,
|
||||
required this.isNextRest,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pillColor = isNextRest
|
||||
? AppColors.info.withValues(alpha: 0.12)
|
||||
: AppColors.accent.withValues(alpha: 0.12);
|
||||
final pillBorderColor = isNextRest
|
||||
? AppColors.info.withValues(alpha: 0.25)
|
||||
: AppColors.accent.withValues(alpha: 0.25);
|
||||
final labelColor = isNextRest ? AppColors.info : AppColors.accent;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing16,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: pillColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: pillBorderColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'UP NEXT',
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.textMuted,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
nextActivityName,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Completion screen
|
||||
// ---------------------------------------------------------------------------
|
||||
class _CompletionScreen extends StatelessWidget {
|
||||
final int totalTimeElapsed;
|
||||
|
||||
const _CompletionScreen({required this.totalTimeElapsed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
AppColors.zinc950,
|
||||
AppColors.success.withValues(alpha: 0.06),
|
||||
AppColors.zinc950,
|
||||
],
|
||||
stops: const [0.0, 0.45, 1.0],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Checkmark circle
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.success.withValues(alpha: 0.12),
|
||||
border: Border.all(
|
||||
color: AppColors.success.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.success.withValues(alpha: 0.15),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_rounded,
|
||||
color: AppColors.success,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
const Text(
|
||||
'Workout Complete',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
|
||||
Text(
|
||||
'Great job! You crushed it.',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing32),
|
||||
|
||||
// Total time card
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(UIConstants.borderRadius),
|
||||
border: Border.all(
|
||||
color: AppColors.border.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'TOTAL TIME',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Text(
|
||||
_formatDuration(totalTimeElapsed),
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing32),
|
||||
|
||||
// Finish button
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 48,
|
||||
child: FilledButton(
|
||||
onPressed: () => context.router.maybePop(),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.success,
|
||||
foregroundColor: AppColors.zinc950,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: const Text('Finish'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final h = seconds ~/ 3600;
|
||||
final m = (seconds % 3600) ~/ 60;
|
||||
final s = seconds % 60;
|
||||
if (h > 0) {
|
||||
return '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
29
lib/presentation/workout_session/workout_session_state.dart
Normal file
29
lib/presentation/workout_session/workout_session_state.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/workout_activity.dart';
|
||||
|
||||
part 'workout_session_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class WorkoutSessionState with _$WorkoutSessionState {
|
||||
const factory WorkoutSessionState({
|
||||
required List<WorkoutActivityEntity> activities,
|
||||
@Default(0) int currentIndex,
|
||||
@Default(0) int timeRemaining,
|
||||
@Default(0) int totalTimeElapsed,
|
||||
@Default(false) bool isRunning,
|
||||
@Default(false) bool isFinished,
|
||||
}) = _WorkoutSessionState;
|
||||
|
||||
const WorkoutSessionState._();
|
||||
|
||||
WorkoutActivityEntity? get currentActivity =>
|
||||
currentIndex < activities.length ? activities[currentIndex] : null;
|
||||
|
||||
WorkoutActivityEntity? get nextActivity =>
|
||||
currentIndex + 1 < activities.length
|
||||
? activities[currentIndex + 1]
|
||||
: null;
|
||||
|
||||
double get progress =>
|
||||
activities.isEmpty ? 0.0 : currentIndex / activities.length;
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
// 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_session_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 _$WorkoutSessionState {
|
||||
List<WorkoutActivityEntity> get activities =>
|
||||
throw _privateConstructorUsedError;
|
||||
int get currentIndex => throw _privateConstructorUsedError;
|
||||
int get timeRemaining => throw _privateConstructorUsedError;
|
||||
int get totalTimeElapsed => throw _privateConstructorUsedError;
|
||||
bool get isRunning => throw _privateConstructorUsedError;
|
||||
bool get isFinished => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of WorkoutSessionState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$WorkoutSessionStateCopyWith<WorkoutSessionState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $WorkoutSessionStateCopyWith<$Res> {
|
||||
factory $WorkoutSessionStateCopyWith(
|
||||
WorkoutSessionState value,
|
||||
$Res Function(WorkoutSessionState) then,
|
||||
) = _$WorkoutSessionStateCopyWithImpl<$Res, WorkoutSessionState>;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<WorkoutActivityEntity> activities,
|
||||
int currentIndex,
|
||||
int timeRemaining,
|
||||
int totalTimeElapsed,
|
||||
bool isRunning,
|
||||
bool isFinished,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$WorkoutSessionStateCopyWithImpl<$Res, $Val extends WorkoutSessionState>
|
||||
implements $WorkoutSessionStateCopyWith<$Res> {
|
||||
_$WorkoutSessionStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of WorkoutSessionState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? activities = null,
|
||||
Object? currentIndex = null,
|
||||
Object? timeRemaining = null,
|
||||
Object? totalTimeElapsed = null,
|
||||
Object? isRunning = null,
|
||||
Object? isFinished = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
activities: null == activities
|
||||
? _value.activities
|
||||
: activities // ignore: cast_nullable_to_non_nullable
|
||||
as List<WorkoutActivityEntity>,
|
||||
currentIndex: null == currentIndex
|
||||
? _value.currentIndex
|
||||
: currentIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
timeRemaining: null == timeRemaining
|
||||
? _value.timeRemaining
|
||||
: timeRemaining // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
totalTimeElapsed: null == totalTimeElapsed
|
||||
? _value.totalTimeElapsed
|
||||
: totalTimeElapsed // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isRunning: null == isRunning
|
||||
? _value.isRunning
|
||||
: isRunning // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isFinished: null == isFinished
|
||||
? _value.isFinished
|
||||
: isFinished // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$WorkoutSessionStateImplCopyWith<$Res>
|
||||
implements $WorkoutSessionStateCopyWith<$Res> {
|
||||
factory _$$WorkoutSessionStateImplCopyWith(
|
||||
_$WorkoutSessionStateImpl value,
|
||||
$Res Function(_$WorkoutSessionStateImpl) then,
|
||||
) = __$$WorkoutSessionStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
List<WorkoutActivityEntity> activities,
|
||||
int currentIndex,
|
||||
int timeRemaining,
|
||||
int totalTimeElapsed,
|
||||
bool isRunning,
|
||||
bool isFinished,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$WorkoutSessionStateImplCopyWithImpl<$Res>
|
||||
extends _$WorkoutSessionStateCopyWithImpl<$Res, _$WorkoutSessionStateImpl>
|
||||
implements _$$WorkoutSessionStateImplCopyWith<$Res> {
|
||||
__$$WorkoutSessionStateImplCopyWithImpl(
|
||||
_$WorkoutSessionStateImpl _value,
|
||||
$Res Function(_$WorkoutSessionStateImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of WorkoutSessionState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? activities = null,
|
||||
Object? currentIndex = null,
|
||||
Object? timeRemaining = null,
|
||||
Object? totalTimeElapsed = null,
|
||||
Object? isRunning = null,
|
||||
Object? isFinished = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$WorkoutSessionStateImpl(
|
||||
activities: null == activities
|
||||
? _value._activities
|
||||
: activities // ignore: cast_nullable_to_non_nullable
|
||||
as List<WorkoutActivityEntity>,
|
||||
currentIndex: null == currentIndex
|
||||
? _value.currentIndex
|
||||
: currentIndex // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
timeRemaining: null == timeRemaining
|
||||
? _value.timeRemaining
|
||||
: timeRemaining // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
totalTimeElapsed: null == totalTimeElapsed
|
||||
? _value.totalTimeElapsed
|
||||
: totalTimeElapsed // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isRunning: null == isRunning
|
||||
? _value.isRunning
|
||||
: isRunning // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isFinished: null == isFinished
|
||||
? _value.isFinished
|
||||
: isFinished // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$WorkoutSessionStateImpl extends _WorkoutSessionState {
|
||||
const _$WorkoutSessionStateImpl({
|
||||
required final List<WorkoutActivityEntity> activities,
|
||||
this.currentIndex = 0,
|
||||
this.timeRemaining = 0,
|
||||
this.totalTimeElapsed = 0,
|
||||
this.isRunning = false,
|
||||
this.isFinished = false,
|
||||
}) : _activities = activities,
|
||||
super._();
|
||||
|
||||
final List<WorkoutActivityEntity> _activities;
|
||||
@override
|
||||
List<WorkoutActivityEntity> get activities {
|
||||
if (_activities is EqualUnmodifiableListView) return _activities;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_activities);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final int currentIndex;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int timeRemaining;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int totalTimeElapsed;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isRunning;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isFinished;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WorkoutSessionState(activities: $activities, currentIndex: $currentIndex, timeRemaining: $timeRemaining, totalTimeElapsed: $totalTimeElapsed, isRunning: $isRunning, isFinished: $isFinished)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$WorkoutSessionStateImpl &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other._activities,
|
||||
_activities,
|
||||
) &&
|
||||
(identical(other.currentIndex, currentIndex) ||
|
||||
other.currentIndex == currentIndex) &&
|
||||
(identical(other.timeRemaining, timeRemaining) ||
|
||||
other.timeRemaining == timeRemaining) &&
|
||||
(identical(other.totalTimeElapsed, totalTimeElapsed) ||
|
||||
other.totalTimeElapsed == totalTimeElapsed) &&
|
||||
(identical(other.isRunning, isRunning) ||
|
||||
other.isRunning == isRunning) &&
|
||||
(identical(other.isFinished, isFinished) ||
|
||||
other.isFinished == isFinished));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_activities),
|
||||
currentIndex,
|
||||
timeRemaining,
|
||||
totalTimeElapsed,
|
||||
isRunning,
|
||||
isFinished,
|
||||
);
|
||||
|
||||
/// Create a copy of WorkoutSessionState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$WorkoutSessionStateImplCopyWith<_$WorkoutSessionStateImpl> get copyWith =>
|
||||
__$$WorkoutSessionStateImplCopyWithImpl<_$WorkoutSessionStateImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _WorkoutSessionState extends WorkoutSessionState {
|
||||
const factory _WorkoutSessionState({
|
||||
required final List<WorkoutActivityEntity> activities,
|
||||
final int currentIndex,
|
||||
final int timeRemaining,
|
||||
final int totalTimeElapsed,
|
||||
final bool isRunning,
|
||||
final bool isFinished,
|
||||
}) = _$WorkoutSessionStateImpl;
|
||||
const _WorkoutSessionState._() : super._();
|
||||
|
||||
@override
|
||||
List<WorkoutActivityEntity> get activities;
|
||||
@override
|
||||
int get currentIndex;
|
||||
@override
|
||||
int get timeRemaining;
|
||||
@override
|
||||
int get totalTimeElapsed;
|
||||
@override
|
||||
bool get isRunning;
|
||||
@override
|
||||
bool get isFinished;
|
||||
|
||||
/// Create a copy of WorkoutSessionState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$WorkoutSessionStateImplCopyWith<_$WorkoutSessionStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
Reference in New Issue
Block a user