checkpoint: focus seed 1.0.1 localized dev switch
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppScaffold extends StatelessWidget {
|
||||
const AppScaffold({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.child,
|
||||
this.action,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget child;
|
||||
final Widget? action;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (action != null) action!,
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+103
-63
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/json_utils.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_state.dart';
|
||||
|
||||
Future<void> showHabitDialog(
|
||||
@@ -9,21 +10,26 @@ Future<void> showHabitDialog(
|
||||
ValueChanged<String> onSubmit, {
|
||||
String initialValue = '',
|
||||
}) async {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
var value = initialValue;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(initialValue.isEmpty ? 'Add habit' : 'Rename habit'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
title: Text(
|
||||
initialValue.isEmpty
|
||||
? context.l10n.addHabitTitle
|
||||
: context.l10n.renameHabitTitle,
|
||||
),
|
||||
content: TextFormField(
|
||||
initialValue: initialValue,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'Example: Read 10 pages',
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: context.l10n.habitHint,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
onChanged: (nextValue) => value = nextValue,
|
||||
onFieldSubmitted: (value) {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
@@ -31,19 +37,63 @@ Future<void> showHabitDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onSubmit(controller.text);
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
child: Text(context.l10n.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showIntentionDialog(
|
||||
BuildContext context,
|
||||
ValueChanged<String> onSubmit, {
|
||||
String initialValue = '',
|
||||
}) async {
|
||||
var value = initialValue;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text(
|
||||
initialValue.isEmpty
|
||||
? context.l10n.setIntentionTitle
|
||||
: context.l10n.editIntentionTitle,
|
||||
),
|
||||
content: TextFormField(
|
||||
initialValue: initialValue,
|
||||
autofocus: true,
|
||||
maxLength: 90,
|
||||
maxLines: 3,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(hintText: context.l10n.intentionHint),
|
||||
onChanged: (nextValue) => value = nextValue,
|
||||
onFieldSubmitted: (value) {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onSubmit(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
Future<void> confirmDeleteHabit(
|
||||
@@ -54,21 +104,19 @@ Future<void> confirmDeleteHabit(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Delete habit?'),
|
||||
content: const Text(
|
||||
'Past completion records for this habit will be removed.',
|
||||
),
|
||||
title: Text(context.l10n.deleteHabitTitle),
|
||||
content: Text(context.l10n.deleteHabitBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
child: Text(context.l10n.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -76,79 +124,78 @@ Future<void> confirmDeleteHabit(
|
||||
}
|
||||
|
||||
Future<void> showExportDialog(BuildContext context, AppState state) async {
|
||||
final controller = TextEditingController(text: prettyJson(state.toJson()));
|
||||
final scaffoldContext = context;
|
||||
final export = prettyJson(state.toJson());
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Export JSON'),
|
||||
title: Text(context.l10n.exportJson),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
minLines: 8,
|
||||
maxLines: 12,
|
||||
readOnly: true,
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(
|
||||
export,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(context.l10n.close),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: controller.text));
|
||||
final message = context.l10n.exportCopied;
|
||||
await Clipboard.setData(ClipboardData(text: export));
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
showSnack(context, 'Export copied');
|
||||
showSnack(scaffoldContext, message);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text('Copy'),
|
||||
label: Text(context.l10n.copy),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
Future<void> showImportDialog(
|
||||
BuildContext context,
|
||||
ValueChanged<String> onImport,
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
var value = '';
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Import JSON'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
title: Text(context.l10n.importJson),
|
||||
content: TextFormField(
|
||||
minLines: 8,
|
||||
maxLines: 12,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'Paste Focus Seed JSON export',
|
||||
onChanged: (nextValue) => value = nextValue,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: context.l10n.importHint,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onImport(controller.text);
|
||||
onImport(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Import'),
|
||||
child: Text(context.l10n.import),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
Future<void> confirmClearData(
|
||||
@@ -159,21 +206,19 @@ Future<void> confirmClearData(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Clear local data?'),
|
||||
content: const Text(
|
||||
'This resets habits, focus sessions, and reflections on this device.',
|
||||
),
|
||||
title: Text(context.l10n.clearDataTitle),
|
||||
content: Text(context.l10n.clearDataBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
child: Text(context.l10n.clear),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -189,23 +234,18 @@ void showPrivacySheet(BuildContext context) {
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: const [
|
||||
children: [
|
||||
Text(
|
||||
'Privacy',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Focus Seed stores your anchor task, habits, focus sessions, '
|
||||
'reflections, and settings locally on this device. V1 does not '
|
||||
'include account sign-in, cloud sync, ads, analytics SDKs, or '
|
||||
'billing SDKs.',
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'You can export your local data as JSON, import a previous '
|
||||
'export, or clear local data from Settings.',
|
||||
context.l10n.privacyTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(context.l10n.privacyBodyOne),
|
||||
const SizedBox(height: 12),
|
||||
Text(context.l10n.privacyBodyTwo),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'seed_card.dart';
|
||||
|
||||
class EmptyState extends StatelessWidget {
|
||||
const EmptyState({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.action,
|
||||
required this.onAction,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String action;
|
||||
final VoidCallback onAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SeedCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 42, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 12),
|
||||
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onAction,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(action),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class FocusSeedHeader extends StatelessWidget {
|
||||
const FocusSeedHeader({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.showBrand = true,
|
||||
this.isSaving = false,
|
||||
this.trailing,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool showBrand;
|
||||
final bool isSaving;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 14, 20, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showBrand) ...[
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale =
|
||||
MediaQuery.textScalerOf(context).scale(14) / 14;
|
||||
final compact = constraints.maxWidth < 340 || textScale > 1.3;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withValues(
|
||||
alpha: 0.12,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.spa_rounded,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 21,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.brandName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child:
|
||||
isSaving
|
||||
? _SaveStatus(
|
||||
key: const ValueKey('saving'),
|
||||
icon: Icons.sync_rounded,
|
||||
label: l10n.saving,
|
||||
compact: compact,
|
||||
)
|
||||
: _SaveStatus(
|
||||
key: const ValueKey('saved'),
|
||||
icon: Icons.smartphone_outlined,
|
||||
label: l10n.local,
|
||||
compact: compact,
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontSize: 24),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SaveStatus extends StatelessWidget {
|
||||
const _SaveStatus({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.compact,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Semantics(
|
||||
label: label,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 15, color: theme.colorScheme.primary),
|
||||
if (!compact) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: theme.textTheme.labelSmall),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/growth_stage.dart';
|
||||
|
||||
class GrowthObject extends StatelessWidget {
|
||||
const GrowthObject({
|
||||
required this.stage,
|
||||
this.size = 148,
|
||||
this.showAmbientHalo = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final GrowthStage stage;
|
||||
final double size;
|
||||
final bool showAmbientHalo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reduceMotion = MediaQuery.of(context).disableAnimations;
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final image = Image.asset(
|
||||
stage.assetPath,
|
||||
key: ValueKey(stage),
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
semanticLabel: localizedGrowthSemantic(context.l10n, stage),
|
||||
);
|
||||
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (showAmbientHalo)
|
||||
Container(
|
||||
width: size * 0.76,
|
||||
height: size * 0.76,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: visuals.ambientTint,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration:
|
||||
reduceMotion
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 420),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) {
|
||||
if (reduceMotion) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
}
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: Tween(begin: 0.94, end: 1.0).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: image,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/habit.dart';
|
||||
import 'seed_card.dart';
|
||||
|
||||
class HabitRow extends StatelessWidget {
|
||||
const HabitRow({
|
||||
required this.habit,
|
||||
required this.dateKey,
|
||||
required this.onToggle,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Habit habit;
|
||||
final String dateKey;
|
||||
final VoidCallback onToggle;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final done = habit.completedDates.contains(dateKey);
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: SeedCard(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
tooltip: done ? 'Mark not done' : 'Mark done',
|
||||
onPressed: onToggle,
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
child: Icon(
|
||||
done ? Icons.check : Icons.circle_outlined,
|
||||
key: ValueKey(done),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
habit.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
decoration: done ? TextDecoration.lineThrough : null,
|
||||
color:
|
||||
done
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onEdit != null)
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Habit options',
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
onEdit?.call();
|
||||
}
|
||||
if (value == 'delete') {
|
||||
onDelete?.call();
|
||||
}
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => const [
|
||||
PopupMenuItem(value: 'edit', child: Text('Rename')),
|
||||
PopupMenuItem(value: 'delete', child: Text('Delete')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InlineEditableText extends StatefulWidget {
|
||||
const InlineEditableText({
|
||||
required this.value,
|
||||
required this.emptyText,
|
||||
required this.onSubmitted,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final String emptyText;
|
||||
final ValueChanged<String> onSubmitted;
|
||||
|
||||
@override
|
||||
State<InlineEditableText> createState() => _InlineEditableTextState();
|
||||
}
|
||||
|
||||
class _InlineEditableTextState extends State<InlineEditableText> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.value);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant InlineEditableText oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.value != widget.value && _controller.text != widget.value) {
|
||||
_controller.text = widget.value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onTapOutside: (_) => widget.onSubmitted(_controller.text),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.emptyText,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Save anchor',
|
||||
onPressed: () => widget.onSubmitted(_controller.text),
|
||||
icon: const Icon(Icons.check),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'seed_card.dart';
|
||||
|
||||
class MetricTile extends StatelessWidget {
|
||||
const MetricTile({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.suffix,
|
||||
required this.icon,
|
||||
this.accent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String suffix;
|
||||
final IconData icon;
|
||||
final Color? accent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = accent ?? Theme.of(context).colorScheme.primary;
|
||||
return SeedCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: color),
|
||||
const SizedBox(height: 12),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
if (suffix.isNotEmpty) ...[
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
suffix,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SectionLabel extends StatelessWidget {
|
||||
const SectionLabel({required this.icon, required this.text, super.key});
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SeedCard extends StatelessWidget {
|
||||
const SeedCard({required this.child, this.margin, super.key});
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: margin,
|
||||
child: Padding(padding: const EdgeInsets.all(16), child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_theme.dart';
|
||||
|
||||
class SurfacePanel extends StatelessWidget {
|
||||
const SurfacePanel({
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.color,
|
||||
this.borderColor,
|
||||
this.elevated = false,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final Color? color;
|
||||
final Color? borderColor;
|
||||
final bool elevated;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final visuals = FocusSeedVisuals.of(context);
|
||||
final decoration = BoxDecoration(
|
||||
color: color ?? theme.colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: borderColor ?? theme.colorScheme.outlineVariant,
|
||||
),
|
||||
boxShadow:
|
||||
elevated
|
||||
? [
|
||||
BoxShadow(
|
||||
color: visuals.shadow,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
final content = Padding(padding: padding, child: child);
|
||||
return Container(
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child:
|
||||
onTap == null
|
||||
? content
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(onTap: onTap, child: content),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/date_keys.dart';
|
||||
import '../models/app_state.dart';
|
||||
import 'section_label.dart';
|
||||
import 'seed_card.dart';
|
||||
|
||||
class WeeklyProgress extends StatelessWidget {
|
||||
const WeeklyProgress({required this.state, super.key});
|
||||
|
||||
final AppState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final days = lastSevenDateKeys();
|
||||
return SeedCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionLabel(
|
||||
icon: Icons.insights_outlined,
|
||||
text: '7-day pulse',
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children:
|
||||
days.map((day) {
|
||||
final minutes = minutesForDate(state.sessions, day);
|
||||
final habits =
|
||||
state.habits
|
||||
.where((habit) => habit.completedDates.contains(day))
|
||||
.length;
|
||||
final hasReflection = state.reflections.any(
|
||||
(reflection) => reflection.dateKey == day,
|
||||
);
|
||||
final score =
|
||||
(minutes > 0 ? 1 : 0) +
|
||||
(habits > 0 ? 1 : 0) +
|
||||
(hasReflection ? 1 : 0);
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
height: 48 + (score * 8),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
score == 0
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(
|
||||
alpha: 0.35 + score * 0.15,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
day.substring(5),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user