trainlog-egu5j / lib /src /presentation /screens /workouts_screen.dart
kuubson's picture
Zaprojektuj i wygeneruj kompletną aplikację Flutter (Dart) działającą na iOS, o nazwie TrainLog: dziennik treningu i śledzenie progresu siłowego/sylwetkowego. Aplikacja ma działać offline-first (bez b
afe1e75 verified
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:trainlog/src/data/models/workout.dart';
import 'package:trainlog/src/presentation/providers/workout_providers.dart';
class WorkoutsScreen extends ConsumerWidget {
const WorkoutsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final workoutsAsync = ref.watch(workoutsProvider);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Historia treningów'),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.add),
onPressed: () => context.go('/workout/new'),
),
),
child: workoutsAsync.when(
data: (workouts) => workouts.isEmpty
? const _EmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: workouts.length,
itemBuilder: (context, index) {
final workout = workouts[index];
return _WorkoutListItem(
workout: workout,
onTap: () => context.go('/workout/edit/${workout.id}'),
onDuplicate: () => _duplicateWorkout(context, ref, workout.id),
);
},
),
loading: () => const Center(child: CupertinoActivityIndicator()),
error: (err, _) => Center(child: Text('Błąd: $err')),
),
);
}
Future<void> _duplicateWorkout(BuildContext context, WidgetRef ref, int id) async {
final repo = ref.read(workoutRepositoryProvider);
try {
await repo.duplicateWorkout(id);
if (context.mounted) {
showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
title: const Text('Sukces'),
content: const Text('Trening został zduplikowany'),
actions: [
CupertinoDialogAction(
child: const Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
} catch (e) {
if (context.mounted) {
showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
title: const Text('Błąd'),
content: Text(e.toString()),
actions: [
CupertinoDialogAction(
child: const Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
}
}
}
class _WorkoutListItem extends StatelessWidget {
final Workout workout;
final VoidCallback onTap;
final VoidCallback onDuplicate;
const _WorkoutListItem({
required this.workout,
required this.onTap,
required this.onDuplicate,
});
@override
Widget build(BuildContext context) {
return Dismissible(
key: Key(workout.id.toString()),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
color: CupertinoColors.destructiveRed,
child: const Icon(CupertinoIcons.delete, color: CupertinoColors.white),
),
onDismissed: (_) => onDuplicate(),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
child: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemBackground.resolveFrom(context),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6.resolveFrom(context),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
workout.type.icon,
style: const TextStyle(fontSize: 20),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('d MMM yyyy, HH:mm', 'pl_PL').format(workout.date),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
workout.type.displayName,
style: TextStyle(
fontSize: 13,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
if (workout.note != null && workout.note!.isNotEmpty)
Text(
workout.note!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: CupertinoColors.tertiaryLabel.resolveFrom(context),
),
),
],
),
),
CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(
CupertinoIcons.doc_on_doc,
size: 20,
color: CupertinoColors.activeOrange,
),
onPressed: () {
showCupertinoModalPopup(
context: context,
builder: (context) => CupertinoActionSheet(
title: const Text('Opcje'),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
onDuplicate();
},
child: const Text('Duplikuj trening'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context),
isDefaultAction: true,
child: const Text('Anuluj'),
),
),
);
},
),
],
),
),
),
),
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.list_bullet,
size: 64,
color: CupertinoColors.systemGrey.resolveFrom(context),
),
const SizedBox(height: 16),
Text(
'Brak treningów',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
const SizedBox(height: 8),
Text(
'Naciśnij + aby dodać pierwszy trening',
style: TextStyle(
color: CupertinoColors.tertiaryLabel.resolveFrom(context),
),
),
],
),
);
}
}