This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/core/utils/id_generator.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/program_week.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/program_workout.dart';
|
||||
@@ -31,12 +33,12 @@ class ProgramWeekView extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Week ${week.position}",
|
||||
'Week ${week.position}',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Divider(),
|
||||
SizedBox(
|
||||
height: 500, // Fixed height for the week grid, or make it dynamic
|
||||
height: 500,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: List.generate(7, (dayIndex) {
|
||||
@@ -46,120 +48,14 @@ class ProgramWeekView extends StatelessWidget {
|
||||
.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
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _DayColumn(
|
||||
dayNum: dayNum,
|
||||
dayIndex: dayIndex,
|
||||
dayWorkouts: dayWorkouts,
|
||||
availablePlans: availablePlans,
|
||||
week: week,
|
||||
onAddWorkout: onAddWorkout,
|
||||
onDeleteWorkout: onDeleteWorkout,
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -170,75 +66,399 @@ class ProgramWeekView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DayColumn extends StatelessWidget {
|
||||
final int dayNum;
|
||||
final int dayIndex;
|
||||
final List<ProgramWorkoutEntity> dayWorkouts;
|
||||
final List<TrainingPlanEntity> availablePlans;
|
||||
final ProgramWeekEntity week;
|
||||
final Function(ProgramWorkoutEntity) onAddWorkout;
|
||||
final Function(String) onDeleteWorkout;
|
||||
|
||||
const _DayColumn({
|
||||
required this.dayNum,
|
||||
required this.dayIndex,
|
||||
required this.dayWorkouts,
|
||||
required this.availablePlans,
|
||||
required this.week,
|
||||
required this.onAddWorkout,
|
||||
required this.onDeleteWorkout,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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),
|
||||
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(
|
||||
(workout) => _WorkoutCard(
|
||||
workout: workout,
|
||||
onDelete: () => onDeleteWorkout(workout.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getDayName(int day) {
|
||||
const days = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday',
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat',
|
||||
'Sun',
|
||||
];
|
||||
if (day >= 1 && day <= 7) return days[day - 1];
|
||||
return 'Day $day';
|
||||
}
|
||||
|
||||
void _showAddWorkoutSheet(BuildContext context, int dayNum) {
|
||||
void _showAddWorkoutSheet(BuildContext context) {
|
||||
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(),
|
||||
],
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _AddWorkoutSheet(
|
||||
availablePlans: availablePlans,
|
||||
week: week,
|
||||
dayNum: dayNum,
|
||||
onAddWorkout: (workout) {
|
||||
onAddWorkout(workout);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkoutCard extends StatelessWidget {
|
||||
final ProgramWorkoutEntity workout;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _WorkoutCard({required this.workout, required this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isNote = workout.type == 'note';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
color: isNote
|
||||
? AppColors.info.withValues(alpha: 0.08)
|
||||
: null,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: Icon(
|
||||
isNote ? Icons.sticky_note_2_outlined : Icons.fitness_center,
|
||||
size: 14,
|
||||
color: isNote ? AppColors.info : AppColors.textMuted,
|
||||
),
|
||||
title: Text(
|
||||
workout.name ?? 'Untitled',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: workout.description != null && workout.description!.isNotEmpty
|
||||
? Text(
|
||||
workout.description!,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: null,
|
||||
trailing: InkWell(
|
||||
onTap: onDelete,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Icon(Icons.close, size: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddWorkoutSheet extends StatefulWidget {
|
||||
final List<TrainingPlanEntity> availablePlans;
|
||||
final ProgramWeekEntity week;
|
||||
final int dayNum;
|
||||
final Function(ProgramWorkoutEntity) onAddWorkout;
|
||||
|
||||
const _AddWorkoutSheet({
|
||||
required this.availablePlans,
|
||||
required this.week,
|
||||
required this.dayNum,
|
||||
required this.onAddWorkout,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AddWorkoutSheet> createState() => _AddWorkoutSheetState();
|
||||
}
|
||||
|
||||
class _AddWorkoutSheetState extends State<_AddWorkoutSheet>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Text(
|
||||
'Add to Schedule',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.fitness_center, size: 16),
|
||||
text: 'Training Plan',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.sticky_note_2_outlined, size: 16),
|
||||
text: 'Note',
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_PlanPickerTab(
|
||||
availablePlans: widget.availablePlans,
|
||||
week: widget.week,
|
||||
dayNum: widget.dayNum,
|
||||
onAddWorkout: widget.onAddWorkout,
|
||||
),
|
||||
_NoteTab(
|
||||
week: widget.week,
|
||||
dayNum: widget.dayNum,
|
||||
onAddWorkout: widget.onAddWorkout,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanPickerTab extends StatelessWidget {
|
||||
final List<TrainingPlanEntity> availablePlans;
|
||||
final ProgramWeekEntity week;
|
||||
final int dayNum;
|
||||
final Function(ProgramWorkoutEntity) onAddWorkout;
|
||||
|
||||
const _PlanPickerTab({
|
||||
required this.availablePlans,
|
||||
required this.week,
|
||||
required this.dayNum,
|
||||
required this.onAddWorkout,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (availablePlans.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text('No training plans available. Create one first!'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: availablePlans.length,
|
||||
itemBuilder: (context, index) {
|
||||
final plan = availablePlans[index];
|
||||
return 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteTab extends StatefulWidget {
|
||||
final ProgramWeekEntity week;
|
||||
final int dayNum;
|
||||
final Function(ProgramWorkoutEntity) onAddWorkout;
|
||||
|
||||
const _NoteTab({
|
||||
required this.week,
|
||||
required this.dayNum,
|
||||
required this.onAddWorkout,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_NoteTab> createState() => _NoteTabState();
|
||||
}
|
||||
|
||||
class _NoteTabState extends State<_NoteTab> {
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UIConstants.spacing16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _titleController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Title',
|
||||
hintText: 'e.g. Active Recovery',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
TextField(
|
||||
controller: _contentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Note (optional)',
|
||||
hintText: 'Additional details...',
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
if (_titleController.text.isEmpty) return;
|
||||
final newWorkout = ProgramWorkoutEntity(
|
||||
id: IdGenerator.generate(),
|
||||
programId: widget.week.programId,
|
||||
weekId: widget.week.id,
|
||||
day: widget.dayNum.toString(),
|
||||
type: 'note',
|
||||
name: _titleController.text,
|
||||
description: _contentController.text.isEmpty
|
||||
? null
|
||||
: _contentController.text,
|
||||
completed: false,
|
||||
);
|
||||
widget.onAddWorkout(newWorkout);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Note'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user