feat: enrich product visual depth
This commit is contained in:
@@ -4,6 +4,7 @@ import '../../application/focus_seed_controller.dart';
|
|||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../../models/focus_session.dart';
|
import '../../models/focus_session.dart';
|
||||||
import '../../models/reflection_entry.dart';
|
import '../../models/reflection_entry.dart';
|
||||||
|
import '../../widgets/seed_illustration.dart';
|
||||||
import '../../widgets/surface_panel.dart';
|
import '../../widgets/surface_panel.dart';
|
||||||
|
|
||||||
enum _HistoryView { focus, notes }
|
enum _HistoryView { focus, notes }
|
||||||
@@ -37,28 +38,11 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 28),
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 28),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
_HistorySwitch(
|
||||||
width: double.infinity,
|
value: _view,
|
||||||
child: SegmentedButton<_HistoryView>(
|
onChanged: (value) => setState(() => _view = value),
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: _HistoryView.focus,
|
|
||||||
icon: const Icon(Icons.timer_outlined),
|
|
||||||
label: Text(l10n.focusHistory),
|
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
const SizedBox(height: 10),
|
||||||
value: _HistoryView.notes,
|
|
||||||
icon: const Icon(Icons.edit_note_rounded),
|
|
||||||
label: Text(l10n.notesHistory),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {_view},
|
|
||||||
onSelectionChanged:
|
|
||||||
(selection) => setState(() => _view = selection.first),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 220),
|
duration: const Duration(milliseconds: 220),
|
||||||
switchInCurve: Curves.easeOutCubic,
|
switchInCurve: Curves.easeOutCubic,
|
||||||
@@ -68,10 +52,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
? _FocusHistoryList(
|
? _FocusHistoryList(
|
||||||
key: const ValueKey('focus-history'),
|
key: const ValueKey('focus-history'),
|
||||||
sessions: focusSessions,
|
sessions: focusSessions,
|
||||||
|
onStartToday: () => Navigator.of(context).pop(),
|
||||||
)
|
)
|
||||||
: _ReflectionHistoryList(
|
: _ReflectionHistoryList(
|
||||||
key: const ValueKey('reflection-history'),
|
key: const ValueKey('reflection-history'),
|
||||||
reflections: reflections,
|
reflections: reflections,
|
||||||
|
onStartToday: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -83,10 +69,133 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HistorySwitch extends StatelessWidget {
|
||||||
|
const _HistorySwitch({required this.value, required this.onChanged});
|
||||||
|
|
||||||
|
final _HistoryView value;
|
||||||
|
final ValueChanged<_HistoryView> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.surfaceContainerLow.withValues(alpha: 0.70),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: scheme.outlineVariant.withValues(alpha: 0.70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _HistorySwitchItem(
|
||||||
|
selected: value == _HistoryView.focus,
|
||||||
|
icon: Icons.timer_outlined,
|
||||||
|
label: l10n.focusHistory,
|
||||||
|
onTap: () => onChanged(_HistoryView.focus),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: _HistorySwitchItem(
|
||||||
|
selected: value == _HistoryView.notes,
|
||||||
|
icon: Icons.edit_note_rounded,
|
||||||
|
label: l10n.notesHistory,
|
||||||
|
onTap: () => onChanged(_HistoryView.notes),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistorySwitchItem extends StatelessWidget {
|
||||||
|
const _HistorySwitchItem({
|
||||||
|
required this.selected,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool selected;
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? scheme.primary.withValues(alpha: 0.16)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow:
|
||||||
|
selected
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: scheme.primary.withValues(alpha: 0.16),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 6),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 19,
|
||||||
|
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color:
|
||||||
|
selected ? scheme.onSurface : scheme.onSurfaceVariant,
|
||||||
|
fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FocusHistoryList extends StatelessWidget {
|
class _FocusHistoryList extends StatelessWidget {
|
||||||
const _FocusHistoryList({required this.sessions, super.key});
|
const _FocusHistoryList({
|
||||||
|
required this.sessions,
|
||||||
|
required this.onStartToday,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final List<FocusSession> sessions;
|
final List<FocusSession> sessions;
|
||||||
|
final VoidCallback onStartToday;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -95,38 +204,38 @@ class _FocusHistoryList extends StatelessWidget {
|
|||||||
return _EmptyHistory(
|
return _EmptyHistory(
|
||||||
title: l10n.emptyFocusHistoryTitle,
|
title: l10n.emptyFocusHistoryTitle,
|
||||||
body: l10n.emptyFocusHistoryBody,
|
body: l10n.emptyFocusHistoryBody,
|
||||||
|
tone: SeedIllustrationTone.archive,
|
||||||
|
onStartToday: onStartToday,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SurfacePanel(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
for (var index = 0; index < sessions.length; index++) ...[
|
for (var index = 0; index < sessions.length; index++) ...[
|
||||||
if (index > 0) const Divider(),
|
if (index > 0) const SizedBox(height: 8),
|
||||||
ListTile(
|
_HistoryTimelineTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
leading: Icons.timer_outlined,
|
||||||
leading: const Icon(Icons.timer_outlined),
|
title: l10n.focusMinutes(sessions[index].minutes),
|
||||||
title: Text(l10n.focusMinutes(sessions[index].minutes)),
|
subtitle: localizedReadableDate(
|
||||||
subtitle: Text(
|
context,
|
||||||
localizedReadableDate(context, sessions[index].completedAt),
|
sessions[index].completedAt,
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
localizedTime(context, sessions[index].completedAt),
|
|
||||||
),
|
),
|
||||||
|
trailing: localizedTime(context, sessions[index].completedAt),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReflectionHistoryList extends StatelessWidget {
|
class _ReflectionHistoryList extends StatelessWidget {
|
||||||
const _ReflectionHistoryList({required this.reflections, super.key});
|
const _ReflectionHistoryList({
|
||||||
|
required this.reflections,
|
||||||
|
required this.onStartToday,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final List<ReflectionEntry> reflections;
|
final List<ReflectionEntry> reflections;
|
||||||
|
final VoidCallback onStartToday;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -135,65 +244,166 @@ class _ReflectionHistoryList extends StatelessWidget {
|
|||||||
return _EmptyHistory(
|
return _EmptyHistory(
|
||||||
title: l10n.emptyNotesHistoryTitle,
|
title: l10n.emptyNotesHistoryTitle,
|
||||||
body: l10n.emptyNotesHistoryBody,
|
body: l10n.emptyNotesHistoryBody,
|
||||||
|
tone: SeedIllustrationTone.data,
|
||||||
|
onStartToday: onStartToday,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SurfacePanel(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
for (var index = 0; index < reflections.length; index++) ...[
|
for (var index = 0; index < reflections.length; index++) ...[
|
||||||
if (index > 0) const Divider(),
|
if (index > 0) const SizedBox(height: 8),
|
||||||
ListTile(
|
_HistoryTimelineTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
leading: reflections[index].mood.icon,
|
||||||
leading: Icon(reflections[index].mood.icon),
|
title: reflections[index].note,
|
||||||
title: Text(
|
subtitle: reflections[index].dateKey,
|
||||||
reflections[index].note,
|
|
||||||
maxLines: 3,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(reflections[index].dateKey),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmptyHistory extends StatelessWidget {
|
class _EmptyHistory extends StatelessWidget {
|
||||||
const _EmptyHistory({required this.title, required this.body});
|
const _EmptyHistory({
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
required this.tone,
|
||||||
|
required this.onStartToday,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String body;
|
final String body;
|
||||||
|
final SeedIllustrationTone tone;
|
||||||
|
final VoidCallback onStartToday;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return SurfacePanel(
|
final scheme = theme.colorScheme;
|
||||||
padding: const EdgeInsets.all(22),
|
final minHeight = MediaQuery.sizeOf(context).height * 0.62;
|
||||||
child: Column(
|
return SizedBox(
|
||||||
children: [
|
height: minHeight.clamp(430, 620).toDouble(),
|
||||||
Icon(
|
child: Center(
|
||||||
Icons.auto_graph_rounded,
|
child: DecoratedBox(
|
||||||
size: 38,
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primary,
|
gradient: RadialGradient(
|
||||||
|
center: Alignment.topCenter,
|
||||||
|
radius: 1.1,
|
||||||
|
colors: [
|
||||||
|
scheme.primary.withValues(alpha: 0.16),
|
||||||
|
scheme.surfaceContainerLowest.withValues(alpha: 0.76),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(18, 26, 18, 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SeedIllustration(size: 142, tone: tone, hero: true),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 8),
|
||||||
Text(
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 310),
|
||||||
|
child: Text(
|
||||||
body,
|
body,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: scheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: onStartToday,
|
||||||
|
icon: const Icon(Icons.arrow_back_rounded),
|
||||||
|
label: Text(context.l10n.navToday),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryTimelineTile extends StatelessWidget {
|
||||||
|
const _HistoryTimelineTile({
|
||||||
|
required this.leading,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.trailing,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData leading;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String? trailing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
return SurfacePanel(
|
||||||
|
padding: const EdgeInsets.fromLTRB(13, 12, 14, 12),
|
||||||
|
borderColor: scheme.outlineVariant.withValues(alpha: 0.60),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.primary.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 42,
|
||||||
|
child: Icon(leading, color: scheme.primary, size: 21),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (trailing != null) ...[
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
trailing!,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,17 +37,16 @@ class JourneyOverviewPanel extends StatelessWidget {
|
|||||||
return SurfacePanel(
|
return SurfacePanel(
|
||||||
elevated: true,
|
elevated: true,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
color: scheme.surfaceContainerLowest,
|
borderColor: scheme.outlineVariant.withValues(alpha: 0.45),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.fromLTRB(18, 16, 16, 18),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
scheme.primary.withValues(alpha: 0.12),
|
scheme.primary.withValues(alpha: 0.20),
|
||||||
scheme.surfaceContainerLowest,
|
scheme.surfaceContainerLowest.withValues(alpha: 0.96),
|
||||||
scheme.secondary.withValues(alpha: 0.07),
|
scheme.secondary.withValues(alpha: 0.14),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -64,23 +63,46 @@ class JourneyOverviewPanel extends StatelessWidget {
|
|||||||
onReflect: onReflect,
|
onReflect: onReflect,
|
||||||
);
|
);
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return Column(
|
return Stack(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
const Align(
|
const Positioned(
|
||||||
alignment: Alignment.centerRight,
|
right: -18,
|
||||||
child: SeedIllustration(size: 88),
|
top: -20,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.58,
|
||||||
|
child: SeedIllustration(size: 128, hero: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 17, 106, 18),
|
||||||
|
child: textContent,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
textContent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Row(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: textContent),
|
Positioned(
|
||||||
const SizedBox(width: 12),
|
right: -28,
|
||||||
const SeedIllustration(size: 112),
|
top: -22,
|
||||||
|
bottom: -18,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.96,
|
||||||
|
child: SeedIllustration(
|
||||||
|
size: constraints.maxWidth.clamp(146, 184).toDouble(),
|
||||||
|
hero: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 92,
|
||||||
|
bottom: 18,
|
||||||
|
child: _ProgressBadge(completed: completed),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(18, 17, 168, 18),
|
||||||
|
child: textContent,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -140,16 +162,9 @@ class _OverviewText extends StatelessWidget {
|
|||||||
color: scheme.onSurfaceVariant,
|
color: scheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 16),
|
||||||
ClipRRect(
|
_ProgressSeeds(completed: completed, progressValue: progressValue),
|
||||||
borderRadius: BorderRadius.circular(99),
|
const SizedBox(height: 12),
|
||||||
child: LinearProgressIndicator(
|
|
||||||
minHeight: 8,
|
|
||||||
value: progressValue,
|
|
||||||
backgroundColor: scheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -172,3 +187,86 @@ class _OverviewText extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ProgressSeeds extends StatelessWidget {
|
||||||
|
const _ProgressSeeds({required this.completed, required this.progressValue});
|
||||||
|
|
||||||
|
final int completed;
|
||||||
|
final double progressValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return Semantics(
|
||||||
|
value: '${(progressValue * 100).round()}%',
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(4, (index) {
|
||||||
|
final filled = index < completed;
|
||||||
|
return Expanded(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
height: 10,
|
||||||
|
margin: EdgeInsets.only(right: index == 3 ? 0 : 7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
filled
|
||||||
|
? scheme.primary
|
||||||
|
: scheme.surfaceContainerHigh.withValues(alpha: 0.82),
|
||||||
|
borderRadius: BorderRadius.circular(99),
|
||||||
|
boxShadow:
|
||||||
|
filled
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: scheme.primary.withValues(alpha: 0.28),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProgressBadge extends StatelessWidget {
|
||||||
|
const _ProgressBadge({required this.completed});
|
||||||
|
|
||||||
|
final int completed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.surfaceContainerLowest.withValues(alpha: 0.82),
|
||||||
|
borderRadius: BorderRadius.circular(99),
|
||||||
|
border: Border.all(
|
||||||
|
color: scheme.outlineVariant.withValues(alpha: 0.65),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.10),
|
||||||
|
blurRadius: 14,
|
||||||
|
offset: const Offset(0, 7),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 7),
|
||||||
|
child: Text(
|
||||||
|
'$completed/4',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: scheme.primary,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+450
-216
@@ -47,119 +47,16 @@ class MeScreen extends StatelessWidget {
|
|||||||
habits: state.habits.length,
|
habits: state.habits.length,
|
||||||
reflections: state.reflections.length,
|
reflections: state.reflections.length,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.preferencesTitle,
|
|
||||||
detail: l10n.preferencesDetail,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SurfacePanel(
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: SegmentedButton<AppearancePreference>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: AppearancePreference.system,
|
|
||||||
icon: const Icon(Icons.brightness_auto_outlined),
|
|
||||||
label: Text(l10n.system),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: AppearancePreference.light,
|
|
||||||
icon: const Icon(Icons.light_mode_outlined),
|
|
||||||
label: Text(l10n.light),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: AppearancePreference.dark,
|
|
||||||
icon: const Icon(Icons.dark_mode_outlined),
|
|
||||||
label: Text(l10n.dark),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {appearance},
|
|
||||||
onSelectionChanged:
|
|
||||||
(selection) => onAppearanceChanged(selection.first),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showLanguageTools) ...[
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_SectionHeader(
|
_PreferenceDeck(
|
||||||
title: l10n.language,
|
appearance: appearance,
|
||||||
detail: l10n.developmentOnly,
|
onAppearanceChanged: onAppearanceChanged,
|
||||||
),
|
language: language,
|
||||||
const SizedBox(height: 8),
|
showLanguageTools: showLanguageTools,
|
||||||
SurfacePanel(
|
onLanguageChanged: onLanguageChanged,
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: SegmentedButton<LanguagePreference>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: LanguagePreference.defaultLocale,
|
|
||||||
icon: const Icon(Icons.tune_rounded),
|
|
||||||
label: Text(l10n.languageDefault),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: LanguagePreference.english,
|
|
||||||
icon: const Icon(Icons.language_rounded),
|
|
||||||
label: Text(l10n.languageEnglish),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: LanguagePreference.chinese,
|
|
||||||
icon: const Icon(Icons.translate_rounded),
|
|
||||||
label: Text(l10n.languageChinese),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {language},
|
|
||||||
onSelectionChanged:
|
|
||||||
(selection) => onLanguageChanged(selection.first),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.localData,
|
|
||||||
detail: l10n.localDataDetail,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_ActionGrid(
|
|
||||||
children: [
|
|
||||||
_ActionCard(
|
|
||||||
icon: Icons.cleaning_services_outlined,
|
|
||||||
title: l10n.clearToday,
|
|
||||||
subtitle: l10n.clearTodaySubtitle,
|
|
||||||
onTap:
|
|
||||||
() => confirmClearToday(context, () async {
|
|
||||||
final cleared = await controller.clearToday();
|
|
||||||
if (context.mounted && cleared) {
|
|
||||||
showSnack(context, l10n.todayDataCleared);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
_ActionCard(
|
|
||||||
icon: Icons.privacy_tip_outlined,
|
|
||||||
title: l10n.privacyTitle,
|
|
||||||
subtitle: l10n.privacySubtitle,
|
|
||||||
onTap: () => showPrivacySheet(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_ActionCard(
|
|
||||||
icon: Icons.delete_forever_outlined,
|
|
||||||
title: l10n.deleteAllLocalData,
|
|
||||||
subtitle: l10n.deleteAllLocalDataSubtitle,
|
|
||||||
destructive: true,
|
|
||||||
fullWidth: true,
|
|
||||||
onTap:
|
|
||||||
() => confirmClearData(context, () async {
|
|
||||||
final cleared = await controller.clearData();
|
|
||||||
if (context.mounted && cleared) {
|
|
||||||
showSnack(context, l10n.localDataCleared);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_DataCarePanel(controller: controller),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Text(
|
Text(
|
||||||
'Focus Seed 1.0.2',
|
'Focus Seed 1.0.2',
|
||||||
@@ -195,24 +92,37 @@ class _PersonalGardenCard extends StatelessWidget {
|
|||||||
return SurfacePanel(
|
return SurfacePanel(
|
||||||
elevated: true,
|
elevated: true,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
borderColor: scheme.outlineVariant.withValues(alpha: 0.42),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 14, 16),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
scheme.primary.withValues(alpha: 0.10),
|
scheme.tertiary.withValues(alpha: 0.16),
|
||||||
scheme.surfaceContainerLowest,
|
scheme.surfaceContainerLowest,
|
||||||
scheme.tertiary.withValues(alpha: 0.07),
|
scheme.primary.withValues(alpha: 0.14),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const Positioned(
|
||||||
|
right: -24,
|
||||||
|
top: -24,
|
||||||
|
child: SeedIllustration(
|
||||||
|
size: 154,
|
||||||
|
tone: SeedIllustrationTone.data,
|
||||||
|
hero: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 17, 16, 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
ConstrainedBox(
|
||||||
children: [
|
constraints: const BoxConstraints(maxWidth: 250),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -220,11 +130,11 @@ class _PersonalGardenCard extends StatelessWidget {
|
|||||||
l10n.personalGardenTitle,
|
l10n.personalGardenTitle,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.titleLarge?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
l10n.personalGardenSubtitle,
|
l10n.personalGardenSubtitle,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
@@ -236,41 +146,31 @@ class _PersonalGardenCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(height: 28),
|
||||||
const SeedIllustration(
|
Wrap(
|
||||||
size: 88,
|
spacing: 8,
|
||||||
tone: SeedIllustrationTone.data,
|
runSpacing: 8,
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 14),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
_MetricPill(
|
||||||
child: _DataMetric(
|
|
||||||
icon: Icons.timer_outlined,
|
icon: Icons.timer_outlined,
|
||||||
label: l10n.focusDataLabel,
|
label: l10n.focusDataLabel,
|
||||||
value: focusSessions,
|
value: focusSessions,
|
||||||
),
|
),
|
||||||
),
|
_MetricPill(
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _DataMetric(
|
|
||||||
icon: Icons.checklist_rounded,
|
icon: Icons.checklist_rounded,
|
||||||
label: l10n.habitsDataLabel,
|
label: l10n.habitsDataLabel,
|
||||||
value: habits,
|
value: habits,
|
||||||
),
|
),
|
||||||
),
|
_MetricPill(
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: _DataMetric(
|
|
||||||
icon: Icons.edit_note_rounded,
|
icon: Icons.edit_note_rounded,
|
||||||
label: l10n.notesDataLabel,
|
label: l10n.notesDataLabel,
|
||||||
value: reflections,
|
value: reflections,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -278,8 +178,8 @@ class _PersonalGardenCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DataMetric extends StatelessWidget {
|
class _MetricPill extends StatelessWidget {
|
||||||
const _DataMetric({
|
const _MetricPill({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
@@ -293,21 +193,28 @@ class _DataMetric extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scheme = theme.colorScheme;
|
final scheme = theme.colorScheme;
|
||||||
return Container(
|
return DecoratedBox(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: scheme.surfaceContainerLowest.withValues(alpha: 0.72),
|
color: scheme.surfaceContainerLowest.withValues(alpha: 0.76),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(99),
|
||||||
border: Border.all(color: scheme.outlineVariant.withValues(alpha: 0.6)),
|
border: Border.all(
|
||||||
|
color: scheme.outlineVariant.withValues(alpha: 0.58),
|
||||||
),
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: scheme.primary, size: 18),
|
Icon(icon, color: scheme.primary, size: 17),
|
||||||
const SizedBox(width: 7),
|
const SizedBox(width: 7),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(end: value.toDouble()),
|
tween: Tween(end: value.toDouble()),
|
||||||
duration: const Duration(milliseconds: 260),
|
duration: const Duration(milliseconds: 260),
|
||||||
@@ -315,16 +222,288 @@ class _DataMetric extends StatelessWidget {
|
|||||||
builder:
|
builder:
|
||||||
(context, animatedValue, _) => Text(
|
(context, animatedValue, _) => Text(
|
||||||
animatedValue.round().toString(),
|
animatedValue.round().toString(),
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreferenceDeck extends StatelessWidget {
|
||||||
|
const _PreferenceDeck({
|
||||||
|
required this.appearance,
|
||||||
|
required this.onAppearanceChanged,
|
||||||
|
required this.language,
|
||||||
|
required this.showLanguageTools,
|
||||||
|
required this.onLanguageChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppearancePreference appearance;
|
||||||
|
final ValueChanged<AppearancePreference> onAppearanceChanged;
|
||||||
|
final LanguagePreference language;
|
||||||
|
final bool showLanguageTools;
|
||||||
|
final ValueChanged<LanguagePreference> onLanguageChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return SurfacePanel(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 15),
|
||||||
|
borderColor: scheme.outlineVariant.withValues(alpha: 0.55),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_SectionHeader(
|
||||||
|
title: l10n.preferencesTitle,
|
||||||
|
detail: l10n.preferencesDetail,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_PreferenceSwitcher<AppearancePreference>(
|
||||||
|
value: appearance,
|
||||||
|
onChanged: onAppearanceChanged,
|
||||||
|
choices: [
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: AppearancePreference.system,
|
||||||
|
icon: Icons.brightness_auto_outlined,
|
||||||
|
label: l10n.system,
|
||||||
|
),
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: AppearancePreference.light,
|
||||||
|
icon: Icons.light_mode_outlined,
|
||||||
|
label: l10n.light,
|
||||||
|
),
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: AppearancePreference.dark,
|
||||||
|
icon: Icons.dark_mode_outlined,
|
||||||
|
label: l10n.dark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (showLanguageTools) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Divider(color: scheme.outlineVariant.withValues(alpha: 0.62)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_SectionHeader(title: l10n.language, detail: l10n.developmentOnly),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_PreferenceSwitcher<LanguagePreference>(
|
||||||
|
value: language,
|
||||||
|
onChanged: onLanguageChanged,
|
||||||
|
choices: [
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: LanguagePreference.defaultLocale,
|
||||||
|
icon: Icons.tune_rounded,
|
||||||
|
label: l10n.languageDefault,
|
||||||
|
),
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: LanguagePreference.english,
|
||||||
|
icon: Icons.language_rounded,
|
||||||
|
label: l10n.languageEnglish,
|
||||||
|
),
|
||||||
|
_PreferenceChoice(
|
||||||
|
value: LanguagePreference.chinese,
|
||||||
|
icon: Icons.translate_rounded,
|
||||||
|
label: l10n.languageChinese,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreferenceChoice<T> {
|
||||||
|
const _PreferenceChoice({
|
||||||
|
required this.value,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
final T value;
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreferenceSwitcher<T> extends StatelessWidget {
|
||||||
|
const _PreferenceSwitcher({
|
||||||
|
required this.value,
|
||||||
|
required this.choices,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final T value;
|
||||||
|
final List<_PreferenceChoice<T>> choices;
|
||||||
|
final ValueChanged<T> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.surfaceContainerHigh.withValues(alpha: 0.48),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (final choice in choices) ...[
|
||||||
|
Expanded(
|
||||||
|
child: _PreferenceChip(
|
||||||
|
selected: choice.value == value,
|
||||||
|
icon: choice.icon,
|
||||||
|
label: choice.label,
|
||||||
|
onTap: () => onChanged(choice.value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (choice != choices.last) const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreferenceChip extends StatelessWidget {
|
||||||
|
const _PreferenceChip({
|
||||||
|
required this.selected,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool selected;
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? scheme.primary.withValues(alpha: 0.15)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border:
|
||||||
|
selected
|
||||||
|
? Border.all(color: scheme.primary.withValues(alpha: 0.28))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: selected ? scheme.primary : scheme.onSurfaceVariant,
|
||||||
|
size: 21,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: selected ? scheme.onSurface : scheme.onSurfaceVariant,
|
||||||
|
fontWeight: selected ? FontWeight.w900 : FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DataCarePanel extends StatelessWidget {
|
||||||
|
const _DataCarePanel({required this.controller});
|
||||||
|
|
||||||
|
final FocusSeedController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
return SurfacePanel(
|
||||||
|
elevated: true,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
borderColor: scheme.outlineVariant.withValues(alpha: 0.42),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
scheme.primary.withValues(alpha: 0.10),
|
||||||
|
scheme.surfaceContainerLowest,
|
||||||
|
scheme.error.withValues(alpha: 0.05),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.primary.withValues(alpha: 0.13),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 42,
|
||||||
|
child: Icon(
|
||||||
|
Icons.verified_user_outlined,
|
||||||
|
color: scheme.primary,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.localData,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
l10n.localDataDetail,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: scheme.onSurfaceVariant,
|
color: scheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -333,113 +512,168 @@ class _DataMetric extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
const SizedBox(height: 12),
|
||||||
|
_DataActionTile(
|
||||||
|
icon: Icons.cleaning_services_outlined,
|
||||||
|
title: l10n.clearToday,
|
||||||
|
subtitle: l10n.clearTodaySubtitle,
|
||||||
|
onTap:
|
||||||
|
() => confirmClearToday(context, () async {
|
||||||
|
final cleared = await controller.clearToday();
|
||||||
|
if (context.mounted && cleared) {
|
||||||
|
showSnack(context, l10n.todayDataCleared);
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_DataActionTile(
|
||||||
|
icon: Icons.privacy_tip_outlined,
|
||||||
|
title: l10n.privacyTitle,
|
||||||
|
subtitle: l10n.privacySubtitle,
|
||||||
|
onTap: () => showPrivacySheet(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_DangerActionTile(
|
||||||
|
title: l10n.deleteAllLocalData,
|
||||||
|
subtitle: l10n.deleteAllLocalDataSubtitle,
|
||||||
|
onTap:
|
||||||
|
() => confirmClearData(context, () async {
|
||||||
|
final cleared = await controller.clearData();
|
||||||
|
if (context.mounted && cleared) {
|
||||||
|
showSnack(context, l10n.localDataCleared);
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
class _ActionGrid extends StatelessWidget {
|
),
|
||||||
const _ActionGrid({required this.children});
|
|
||||||
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final compact = constraints.maxWidth < 350;
|
|
||||||
if (compact) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
for (var index = 0; index < children.length; index++) ...[
|
|
||||||
if (index > 0) const SizedBox(height: 10),
|
|
||||||
children[index],
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
return GridView.count(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
crossAxisSpacing: 10,
|
|
||||||
childAspectRatio: 1.22,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionCard extends StatelessWidget {
|
class _DataActionTile extends StatelessWidget {
|
||||||
const _ActionCard({
|
const _DataActionTile({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.destructive = false,
|
|
||||||
this.fullWidth = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final bool destructive;
|
|
||||||
final bool fullWidth;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scheme = theme.colorScheme;
|
final scheme = theme.colorScheme;
|
||||||
final accent = destructive ? scheme.error : scheme.primary;
|
return Material(
|
||||||
return SurfacePanel(
|
color: scheme.surfaceContainerLow.withValues(alpha: 0.72),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
padding: const EdgeInsets.all(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderColor: destructive ? scheme.error.withValues(alpha: 0.38) : null,
|
child: Padding(
|
||||||
color:
|
padding: const EdgeInsets.fromLTRB(12, 11, 12, 11),
|
||||||
destructive
|
child: Row(
|
||||||
? scheme.errorContainer.withValues(alpha: 0.22)
|
children: [
|
||||||
: scheme.surfaceContainerLowest,
|
Icon(icon, color: scheme.primary, size: 22),
|
||||||
child: SizedBox(
|
const SizedBox(width: 12),
|
||||||
width: fullWidth ? double.infinity : null,
|
Expanded(
|
||||||
height: fullWidth ? 108 : 118,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
|
||||||
width: 38,
|
|
||||||
height: 38,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: accent.withValues(alpha: 0.12),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(icon, color: accent, size: 21),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
color: destructive ? scheme.error : null,
|
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
maxLines: fullWidth ? 2 : 3,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color:
|
color: scheme.onSurfaceVariant,
|
||||||
destructive
|
|
||||||
? scheme.onErrorContainer
|
|
||||||
: scheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Icon(Icons.arrow_forward_ios_rounded, size: 15),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DangerActionTile extends StatelessWidget {
|
||||||
|
const _DangerActionTile({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
return Material(
|
||||||
|
color: scheme.errorContainer.withValues(alpha: 0.22),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 11, 12, 11),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.delete_forever_outlined,
|
||||||
|
color: scheme.error,
|
||||||
|
size: 23,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: scheme.error,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: scheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ class SeedIllustration extends StatelessWidget {
|
|||||||
const SeedIllustration({
|
const SeedIllustration({
|
||||||
this.tone = SeedIllustrationTone.garden,
|
this.tone = SeedIllustrationTone.garden,
|
||||||
this.size = 118,
|
this.size = 118,
|
||||||
|
this.hero = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SeedIllustrationTone tone;
|
final SeedIllustrationTone tone;
|
||||||
final double size;
|
final double size;
|
||||||
|
final bool hero;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -35,7 +37,7 @@ class SeedIllustration extends StatelessWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
gradient: RadialGradient(
|
gradient: RadialGradient(
|
||||||
colors: [
|
colors: [
|
||||||
softAccent,
|
softAccent.withValues(alpha: hero ? 0.92 : 1),
|
||||||
scheme.surfaceContainerHigh.withValues(alpha: 0.28),
|
scheme.surfaceContainerHigh.withValues(alpha: 0.28),
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
],
|
],
|
||||||
@@ -43,53 +45,87 @@ class SeedIllustration extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hero)
|
||||||
|
Positioned.fill(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _OrbitPainter(
|
||||||
|
color: accent.withValues(alpha: 0.30),
|
||||||
|
quietColor: scheme.outlineVariant.withValues(alpha: 0.30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hero)
|
||||||
|
Positioned(
|
||||||
|
left: size * 0.08,
|
||||||
|
top: size * 0.08,
|
||||||
|
child: _Sparkle(size: size * 0.13, color: accent),
|
||||||
|
),
|
||||||
|
if (hero)
|
||||||
|
Positioned(
|
||||||
|
right: size * 0.14,
|
||||||
|
bottom: size * 0.12,
|
||||||
|
child: _Sparkle(size: size * 0.10, color: scheme.tertiary),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: size * 0.16,
|
bottom: size * 0.16,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: size * 0.62,
|
width: size * (hero ? 0.72 : 0.62),
|
||||||
height: size * 0.28,
|
height: size * (hero ? 0.31 : 0.28),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: scheme.surfaceContainerHighest.withValues(alpha: 0.78),
|
color: scheme.surfaceContainerHighest.withValues(alpha: 0.78),
|
||||||
borderRadius: BorderRadius.circular(size),
|
borderRadius: BorderRadius.circular(size),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.10),
|
color: Colors.black.withValues(alpha: hero ? 0.16 : 0.10),
|
||||||
blurRadius: 18,
|
blurRadius: hero ? 26 : 18,
|
||||||
offset: const Offset(0, 8),
|
offset: Offset(0, hero ? 12 : 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: size * 0.18,
|
top: size * (hero ? 0.15 : 0.18),
|
||||||
left: size * 0.28,
|
left: size * (hero ? 0.24 : 0.28),
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: -0.62,
|
angle: -0.62,
|
||||||
child: _LeafBlob(
|
child: _LeafBlob(
|
||||||
width: size * 0.26,
|
width: size * (hero ? 0.30 : 0.26),
|
||||||
height: size * 0.42,
|
height: size * (hero ? 0.48 : 0.42),
|
||||||
color: accent.withValues(alpha: 0.78),
|
color: accent.withValues(alpha: 0.78),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: size * 0.16,
|
top: size * (hero ? 0.12 : 0.16),
|
||||||
right: size * 0.28,
|
right: size * (hero ? 0.24 : 0.28),
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: 0.62,
|
angle: 0.62,
|
||||||
child: _LeafBlob(
|
child: _LeafBlob(
|
||||||
width: size * 0.28,
|
width: size * (hero ? 0.33 : 0.28),
|
||||||
height: size * 0.45,
|
height: size * (hero ? 0.52 : 0.45),
|
||||||
color: scheme.primaryContainer.withValues(alpha: 0.86),
|
color: scheme.primaryContainer.withValues(alpha: 0.86),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hero)
|
||||||
|
Positioned(
|
||||||
|
top: size * 0.25,
|
||||||
|
right: size * 0.36,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: 0.10,
|
||||||
|
child: _LeafBlob(
|
||||||
|
width: size * 0.18,
|
||||||
|
height: size * 0.30,
|
||||||
|
color: scheme.tertiary.withValues(alpha: 0.62),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: size * 0.44,
|
top: size * 0.44,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: size * 0.08,
|
width: size * (hero ? 0.09 : 0.08),
|
||||||
height: size * 0.28,
|
height: size * (hero ? 0.32 : 0.28),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: scheme.primary.withValues(alpha: 0.78),
|
color: scheme.primary.withValues(alpha: 0.78),
|
||||||
borderRadius: BorderRadius.circular(99),
|
borderRadius: BorderRadius.circular(99),
|
||||||
@@ -101,8 +137,8 @@ class SeedIllustration extends StatelessWidget {
|
|||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: 0.18,
|
angle: 0.18,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: size * 0.18,
|
width: size * (hero ? 0.21 : 0.18),
|
||||||
height: size * 0.28,
|
height: size * (hero ? 0.31 : 0.28),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: scheme.tertiaryContainer.withValues(alpha: 0.90),
|
color: scheme.tertiaryContainer.withValues(alpha: 0.90),
|
||||||
borderRadius: BorderRadius.circular(99),
|
borderRadius: BorderRadius.circular(99),
|
||||||
@@ -198,3 +234,81 @@ class _FloatingDot extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _Sparkle extends StatelessWidget {
|
||||||
|
const _Sparkle({required this.size, required this.color});
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox.square(
|
||||||
|
dimension: size,
|
||||||
|
child: CustomPaint(painter: _SparklePainter(color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SparklePainter extends CustomPainter {
|
||||||
|
const _SparklePainter(this.color);
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()..color = color.withValues(alpha: 0.72);
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final path =
|
||||||
|
Path()
|
||||||
|
..moveTo(center.dx, 0)
|
||||||
|
..quadraticBezierTo(
|
||||||
|
center.dx * 1.18,
|
||||||
|
center.dy * 0.78,
|
||||||
|
size.width,
|
||||||
|
center.dy,
|
||||||
|
)
|
||||||
|
..quadraticBezierTo(
|
||||||
|
center.dx * 1.18,
|
||||||
|
center.dy * 1.18,
|
||||||
|
center.dx,
|
||||||
|
size.height,
|
||||||
|
)
|
||||||
|
..quadraticBezierTo(center.dx * 0.78, center.dy * 1.18, 0, center.dy)
|
||||||
|
..quadraticBezierTo(center.dx * 0.78, center.dy * 0.78, center.dx, 0);
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _SparklePainter oldDelegate) {
|
||||||
|
return oldDelegate.color != color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrbitPainter extends CustomPainter {
|
||||||
|
const _OrbitPainter({required this.color, required this.quietColor});
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
final Color quietColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final rect =
|
||||||
|
Offset(size.width * 0.08, size.height * 0.12) &
|
||||||
|
Size(size.width * 0.84, size.height * 0.72);
|
||||||
|
final paint =
|
||||||
|
Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = size.shortestSide * 0.018
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..color = quietColor;
|
||||||
|
canvas.drawArc(rect, -0.12, 4.8, false, paint);
|
||||||
|
paint.color = color;
|
||||||
|
canvas.drawArc(rect, 2.8, 1.25, false, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _OrbitPainter oldDelegate) {
|
||||||
|
return oldDelegate.color != color || oldDelegate.quietColor != quietColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:focus_seed/src/app/focus_seed_app.dart';
|
import 'package:focus_seed/src/app/focus_seed_app.dart';
|
||||||
import 'package:focus_seed/src/data/focus_seed_store.dart';
|
import 'package:focus_seed/src/data/focus_seed_store.dart';
|
||||||
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
|
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
|
||||||
import 'package:focus_seed/src/models/language_preference.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -143,11 +142,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('\u8bed\u8a00'), findsOneWidget);
|
expect(find.text('\u8bed\u8a00'), findsOneWidget);
|
||||||
final languageSegments = find.byType(SegmentedButton<LanguagePreference>);
|
await tester.tap(find.byIcon(Icons.language_rounded));
|
||||||
final widget = tester.widget<SegmentedButton<LanguagePreference>>(
|
|
||||||
languageSegments,
|
|
||||||
);
|
|
||||||
widget.onSelectionChanged?.call({LanguagePreference.english});
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Language'), findsOneWidget);
|
expect(find.text('Language'), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user