full app done
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
/// 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user