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