290 lines
8.9 KiB
Dart
290 lines
8.9 KiB
Dart
/// Bird's-eye overview of all five piles with demote / refine 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;
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
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: [
|
|
Text('All Sorted Values', style: text.displaySmall),
|
|
const SizedBox(height: 16),
|
|
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: 12),
|
|
Text(
|
|
'Tip: refine any pile before ranking your core values.',
|
|
style: text.bodySmall?.copyWith(color: colors.onSurfaceVariant),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|