Files
valuecards/lib/screens/sorted_overview_screen.dart
T
2026-09-18 00:16:49 +02:00

282 lines
8.6 KiB
Dart

/// Bird's-eye overview of all five piles with reorder actions.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/value_card.dart';
import '../models/value_pile.dart';
import '../providers/values_sort_provider.dart';
import '../widgets/practice_chrome.dart';
import '../widgets/value_card_tile.dart';
class SortedOverviewScreen extends StatefulWidget {
const SortedOverviewScreen({super.key});
@override
State<SortedOverviewScreen> createState() => _SortedOverviewScreenState();
}
class _SortedOverviewScreenState extends State<SortedOverviewScreen> {
String _query = '';
ValuePile? _filter;
@override
Widget build(BuildContext context) {
final sort = context.watch<ValuesSortController>();
final text = Theme.of(context).textTheme;
return PracticeScaffold(
header: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 0),
child: Row(
children: [
IconButton(
onPressed: sort.goToHub,
icon: const Icon(Icons.arrow_back),
),
Expanded(
child: Text(
'All Sorted Values',
style: text.labelMedium,
),
),
],
),
),
child: ListView(
children: [
TextField(
onChanged: (v) => setState(() => _query = v.trim().toLowerCase()),
decoration: const InputDecoration(
hintText: 'Find a value (e.g., Creativity)',
prefixIcon: Icon(Icons.search),
),
),
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_FilterChip(
label: 'All (${sort.totalCount})',
selected: _filter == null,
onTap: () => setState(() => _filter = null),
),
...ValuePile.values.map((pile) {
return _FilterChip(
label: '${pile.shortLabel} (${sort.countInPile(pile)})',
selected: _filter == pile,
accent: pile.accent,
onTap: () => setState(() => _filter = pile),
);
}),
],
),
),
const SizedBox(height: 18),
...ValuePile.values
.where((p) => _filter == null || _filter == p)
.map((pile) {
final cards = sort.cardsInPile(pile).where((c) {
if (_query.isEmpty) return true;
return c.title.toLowerCase().contains(_query) ||
c.description.toLowerCase().contains(_query);
}).toList();
return _PileSection(
pile: pile,
cards: cards,
onDemote: (card) => _showDemoteSheet(context, sort, card),
);
}),
const SizedBox(height: 8),
],
),
);
}
Future<void> _showDemoteSheet(
BuildContext context,
ValuesSortController sort,
ValueCard card,
) async {
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final text = Theme.of(ctx).textTheme;
final colors = Theme.of(ctx).colorScheme;
final maxHeight = MediaQuery.sizeOf(ctx).height * 0.75;
return SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
24,
8,
24,
MediaQuery.paddingOf(ctx).bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Reorder ${card.title}',
style: text.titleLarge,
),
),
IconButton(
onPressed: () => Navigator.pop(ctx),
icon: const Icon(Icons.close),
),
],
),
Text('Move to pile:', style: text.bodyMedium),
const SizedBox(height: 8),
...ValuePile.values.map((pile) {
final current = sort.assignments[card.id] == pile;
return ListTile(
contentPadding: EdgeInsets.zero,
enabled: !current,
leading: CircleAvatar(
backgroundColor: pile.accent.withValues(alpha: 0.2),
child: Text(
'${pile.rank}',
style: text.labelMedium?.copyWith(color: pile.accent),
),
),
title: Text(pile.label),
subtitle: Text(pile.subtitle),
trailing: current
? Icon(Icons.check, color: colors.primary)
: Icon(Icons.arrow_forward, color: colors.onSurfaceVariant),
onTap: current
? null
: () {
sort.moveCard(card.id, pile);
Navigator.pop(ctx);
},
);
}),
],
),
),
),
);
},
);
}
}
class _FilterChip extends StatelessWidget {
const _FilterChip({
required this.label,
required this.selected,
required this.onTap,
this.accent,
});
final String label;
final bool selected;
final VoidCallback onTap;
final Color? accent;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final labelColor = selected ? colors.onPrimary : colors.onSurface;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(label),
selected: selected,
onSelected: (_) => onTap(),
showCheckmark: true,
checkmarkColor: colors.onPrimary,
selectedColor: colors.primary,
backgroundColor: colors.surfaceContainer,
labelStyle: Theme.of(context).textTheme.labelMedium?.copyWith(
color: labelColor,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
),
side: BorderSide(
color: selected
? colors.primary
: (accent ?? colors.outlineVariant),
),
),
);
}
}
class _PileSection extends StatelessWidget {
const _PileSection({
required this.pile,
required this.cards,
required this.onDemote,
});
final ValuePile pile;
final List<ValueCard> cards;
final void Function(ValueCard card) onDemote;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
return Padding(
padding: const EdgeInsets.only(bottom: 22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.circle, size: 12, color: pile.accent),
const SizedBox(width: 8),
Expanded(
child: Text(
'${pile.label} · ${cards.length} cards',
style: text.titleLarge,
),
),
],
),
const SizedBox(height: 8),
if (pile == ValuePile.mostImportant)
...cards.map(
(card) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ValueCardTile(
card: card,
compact: true,
trailing: TextButton(
onPressed: () => onDemote(card),
child: const Text('Reorder'),
),
),
),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: cards
.map(
(c) => ValueChip(
title: c.title,
onTap: () => onDemote(c),
),
)
.toList(),
),
],
),
);
}
}