448 lines
15 KiB
Dart
448 lines
15 KiB
Dart
/// Post-sort home — landscape summary and gateways (Stitch Practice Hub).
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../models/value_card.dart';
|
|
import '../providers/values_sort_provider.dart';
|
|
import '../widgets/practice_chrome.dart';
|
|
|
|
class PracticeHubScreen extends StatelessWidget {
|
|
const PracticeHubScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final sort = context.watch<ValuesSortController>();
|
|
final text = Theme.of(context).textTheme;
|
|
final colors = Theme.of(context).colorScheme;
|
|
final answered = sort.totalAnsweredReflections;
|
|
final slots = sort.totalReflectionSlots;
|
|
final progress = slots == 0 ? 0.0 : answered / slots;
|
|
final core = sort.rankedCards.take(5).toList(growable: false);
|
|
|
|
return PracticeScaffold(
|
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
|
|
footer: FilledButton.tonal(
|
|
onPressed: () => _confirmReset(context, sort),
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(52),
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
foregroundColor: colors.onSurface,
|
|
shape: const StadiumBorder(),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.restart_alt, size: 18, color: colors.outline),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Finish Practice (Reset All)',
|
|
style: text.labelLarge?.copyWith(color: colors.onSurface),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
child: ListView(
|
|
children: [
|
|
Text(
|
|
'Your Values Landscape',
|
|
style: text.displaySmall?.copyWith(color: colors.onSurface),
|
|
),
|
|
const SizedBox(height: 18),
|
|
_CoreValuesCard(
|
|
values: core,
|
|
onValueTap: (card) => sort.goToReflection(valueId: card.id),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_GatewayCard(
|
|
icon: Icons.layers_outlined,
|
|
iconColor: colors.primary,
|
|
title: 'All Sorted Values',
|
|
onTap: sort.goToOverview,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_GatewayCard(
|
|
icon: Icons.format_list_numbered,
|
|
iconColor: colors.secondary,
|
|
title: 'Order Top Values',
|
|
onTap: sort.goToRanking,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_GatewayCard(
|
|
icon: Icons.edit_note,
|
|
iconColor: colors.tertiary,
|
|
title: 'Reflect & Inquire',
|
|
meta: slots == 0
|
|
? 'Rank top values first'
|
|
: '$answered of $slots Answered (${(progress * 100).round()}%)',
|
|
metaBackground: colors.tertiaryContainer.withValues(alpha: 0.45),
|
|
metaForeground: colors.onTertiaryContainer,
|
|
progress: slots == 0 ? null : progress,
|
|
onTap: sort.coreValues.isEmpty ? null : sort.goToReflection,
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _confirmReset(
|
|
BuildContext context,
|
|
ValuesSortController sort,
|
|
) async {
|
|
final ok = await showModalBottomSheet<bool>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (ctx) {
|
|
final text = Theme.of(ctx).textTheme;
|
|
final c = Theme.of(ctx).colorScheme;
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: c.errorContainer.withValues(alpha: 0.5),
|
|
child: Icon(Icons.hourglass_empty, color: c.error),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Reset Entire Practice?', style: text.titleLarge),
|
|
Text(
|
|
'Clean slate contemplation',
|
|
style: text.labelSmall,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
'This deliberate action will clear all 5 categorized piles, '
|
|
'your prioritized hierarchy, and stored reflection notes. '
|
|
'You will begin this mindful exploration from the initial deck.',
|
|
style: text.bodyMedium,
|
|
),
|
|
const SizedBox(height: 14),
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: c.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.lightbulb_outline,
|
|
size: 18, color: c.secondary),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Tip: You can pause here and revisit your values whenever you need clarity in daily choices without resetting.',
|
|
style: text.bodySmall,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
PrimaryPillButton(
|
|
label: 'Yes, Reset All ${sort.totalCount} Cards',
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
),
|
|
const SizedBox(height: 8),
|
|
GhostPillButton(
|
|
label: 'Keep My Current Landscape',
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
if (ok == true) sort.restart();
|
|
}
|
|
}
|
|
|
|
class _CoreValuesCard extends StatelessWidget {
|
|
const _CoreValuesCard({
|
|
required this.values,
|
|
required this.onValueTap,
|
|
});
|
|
|
|
final List<ValueCard> values;
|
|
final void Function(ValueCard card) onValueTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final text = Theme.of(context).textTheme;
|
|
final colors = Theme.of(context).colorScheme;
|
|
final accent = colors.secondary;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerLow,
|
|
borderRadius: BorderRadius.circular(22),
|
|
border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.7)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 28,
|
|
height: 28,
|
|
decoration: BoxDecoration(
|
|
color: accent.withValues(alpha: 0.18),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(Icons.star, size: 15, color: accent),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Your Core Values',
|
|
style: text.titleLarge?.copyWith(
|
|
color: colors.onSurface,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
if (values.isEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: Text(
|
|
'Rank your Most Important values to distill your core set here.',
|
|
style: text.bodyMedium?.copyWith(
|
|
color: colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
)
|
|
else
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final gap = 8.0;
|
|
final chipWidth = (constraints.maxWidth - gap) / 2;
|
|
return Wrap(
|
|
spacing: gap,
|
|
runSpacing: gap,
|
|
children: [
|
|
for (var i = 0; i < values.length; i++)
|
|
SizedBox(
|
|
width: chipWidth,
|
|
child: _CoreValueChip(
|
|
rank: i + 1,
|
|
title: values[i].title,
|
|
accent: accent,
|
|
onTap: () => onValueTap(values[i]),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CoreValueChip extends StatelessWidget {
|
|
const _CoreValueChip({
|
|
required this.rank,
|
|
required this.title,
|
|
required this.accent,
|
|
required this.onTap,
|
|
});
|
|
|
|
final int rank;
|
|
final String title;
|
|
final Color accent;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final text = Theme.of(context).textTheme;
|
|
final colors = Theme.of(context).colorScheme;
|
|
|
|
return Material(
|
|
color: colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 22,
|
|
height: 22,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: accent.withValues(alpha: 0.18),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Text(
|
|
'$rank',
|
|
style: text.labelSmall?.copyWith(
|
|
color: accent,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: text.labelMedium?.copyWith(
|
|
color: colors.onSurface,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GatewayCard extends StatelessWidget {
|
|
const _GatewayCard({
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.title,
|
|
required this.onTap,
|
|
this.meta,
|
|
this.metaBackground,
|
|
this.metaForeground,
|
|
this.progress,
|
|
});
|
|
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final String title;
|
|
final String? meta;
|
|
final Color? metaBackground;
|
|
final Color? metaForeground;
|
|
final VoidCallback? onTap;
|
|
final double? progress;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final text = Theme.of(context).textTheme;
|
|
final colors = Theme.of(context).colorScheme;
|
|
final enabled = onTap != null;
|
|
final hasMeta = meta != null && meta!.isNotEmpty;
|
|
|
|
return Opacity(
|
|
opacity: enabled ? 1 : 0.55,
|
|
child: Material(
|
|
color: colors.surfaceContainerLow,
|
|
elevation: 0,
|
|
shadowColor: colors.shadow.withValues(alpha: 0.08),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHigh,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: iconColor, size: 22),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: text.titleLarge?.copyWith(
|
|
color: colors.onSurface,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
if (hasMeta) ...[
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 3,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: metaBackground ?? colors.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Text(
|
|
meta!,
|
|
style: text.labelSmall?.copyWith(
|
|
color: metaForeground ?? colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.arrow_forward,
|
|
color: colors.primary,
|
|
size: 22,
|
|
),
|
|
],
|
|
),
|
|
if (progress != null) ...[
|
|
const SizedBox(height: 14),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(999),
|
|
child: LinearProgressIndicator(
|
|
value: progress,
|
|
minHeight: 6,
|
|
backgroundColor: colors.surfaceContainerHighest,
|
|
color: colors.tertiary,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|