108 lines
3.6 KiB
Dart
108 lines
3.6 KiB
Dart
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:provider/provider.dart';
|
||
|
|
|
||
|
|
import '../../design/page_header.dart';
|
||
|
|
import '../../design/zen_nav_bar.dart';
|
||
|
|
import '../../design/zen_style.dart';
|
||
|
|
import '../../localization/app_localizations.dart';
|
||
|
|
import '../../settings/settings_controller.dart';
|
||
|
|
import '../exercise.dart';
|
||
|
|
import '../exercise_labels.dart';
|
||
|
|
import '../exercise_library.dart';
|
||
|
|
|
||
|
|
/// The current Mode's Exercises, by name.
|
||
|
|
class LibraryView extends StatelessWidget {
|
||
|
|
const LibraryView({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final l10n = AppLocalizations.of(context);
|
||
|
|
final mode = context.watch<SettingsController>().mode;
|
||
|
|
final exercises = context.read<ExerciseLibrary>().forMode(mode);
|
||
|
|
final bottomInset = MediaQuery.paddingOf(context).bottom;
|
||
|
|
return ListView.builder(
|
||
|
|
key: PageStorageKey('library-${mode.name}'),
|
||
|
|
padding: EdgeInsets.only(bottom: ZenNavBar.height + 32 + bottomInset),
|
||
|
|
itemCount: exercises.length + 1,
|
||
|
|
itemBuilder: (context, i) => i == 0
|
||
|
|
? PageHeader(title: l10n.libraryTitle, subtitle: l10n.librarySubtitle(mode.name, exercises.length))
|
||
|
|
: Padding(
|
||
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
||
|
|
child: ExerciseTile(exercise: exercises[i - 1]),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ExerciseTile extends StatelessWidget {
|
||
|
|
const ExerciseTile({super.key, required this.exercise, this.onTap});
|
||
|
|
|
||
|
|
final Exercise exercise;
|
||
|
|
final VoidCallback? onTap;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final s = ZenStyle.of(context);
|
||
|
|
final text = Theme.of(context).textTheme;
|
||
|
|
final muscles = exercise.primaryMuscles.map(sentenceCase).join(', ');
|
||
|
|
return Card(
|
||
|
|
child: InkWell(
|
||
|
|
onTap: onTap,
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(10),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
ExerciseThumbnail(exercise: exercise, size: 64),
|
||
|
|
const SizedBox(width: 14),
|
||
|
|
Expanded(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(exercise.name, style: text.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
'$muscles · ${sentenceCase(exercise.level)}',
|
||
|
|
style: text.bodySmall?.copyWith(color: s.inkMuted),
|
||
|
|
maxLines: 1,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The start image in a soft well, or an icon for the few Exercises without images.
|
||
|
|
class ExerciseThumbnail extends StatelessWidget {
|
||
|
|
const ExerciseThumbnail({super.key, required this.exercise, required this.size});
|
||
|
|
|
||
|
|
final Exercise exercise;
|
||
|
|
final double size;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final s = ZenStyle.of(context);
|
||
|
|
final ratio = MediaQuery.devicePixelRatioOf(context);
|
||
|
|
return ClipRRect(
|
||
|
|
borderRadius: BorderRadius.circular((s.radius * 0.6).clamp(0, size / 2)),
|
||
|
|
child: Container(
|
||
|
|
width: size,
|
||
|
|
height: size,
|
||
|
|
color: s.surfaceSoft,
|
||
|
|
child: exercise.images.isEmpty
|
||
|
|
? Icon(Icons.image_not_supported_outlined, color: s.inkMuted)
|
||
|
|
: Image.asset(
|
||
|
|
exercise.images.first,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
cacheWidth: (size * ratio).round(),
|
||
|
|
errorBuilder: (_, _, _) => Icon(Icons.image_not_supported_outlined, color: s.inkMuted),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|