import 'dart:async'; import 'package:flutter/material.dart'; import '../app/app_theme.dart'; import '../l10n/l10n.dart'; import 'app_bottom_sheet.dart'; import 'app_busy_indicator.dart'; import 'surface_panel.dart'; Future showHabitDialog( BuildContext context, Future Function(String value) onSubmit, { String initialValue = '', }) async { final l10n = context.l10n; await showTextEntrySheet( context: context, title: initialValue.isEmpty ? l10n.addHabitTitle : l10n.renameHabitTitle, hintText: l10n.habitHint, initialValue: initialValue, icon: Icons.add_task_rounded, accentBuilder: (context) => FocusSeedVisuals.of(context).habit, onSubmit: onSubmit, ); } Future showIntentionDialog( BuildContext context, Future Function(String value) onSubmit, { String initialValue = '', }) async { final l10n = context.l10n; await showTextEntrySheet( context: context, title: initialValue.isEmpty ? l10n.setIntentionTitle : l10n.editIntentionTitle, hintText: l10n.intentionHint, initialValue: initialValue, icon: Icons.spa_rounded, maxLength: 90, maxLines: 3, accentBuilder: (context) => FocusSeedVisuals.of(context).action, onSubmit: onSubmit, ); } Future showTextEntrySheet({ required BuildContext context, required String title, required String hintText, required Future Function(String value) onSubmit, required IconData icon, required Color Function(BuildContext context) accentBuilder, String initialValue = '', int? maxLength, int minLines = 1, int maxLines = 1, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => _TextEntrySheet( title: title, hintText: hintText, initialValue: initialValue, maxLength: maxLength, minLines: minLines, maxLines: maxLines, icon: icon, accent: accentBuilder(context), onSubmit: onSubmit, ), ); } class _TextEntrySheet extends StatefulWidget { const _TextEntrySheet({ required this.title, required this.hintText, required this.initialValue, required this.icon, required this.accent, required this.onSubmit, this.maxLength, this.minLines = 1, this.maxLines = 1, }); final String title; final String hintText; final String initialValue; final IconData icon; final Color accent; final Future Function(String value) onSubmit; final int? maxLength; final int minLines; final int maxLines; @override State<_TextEntrySheet> createState() => _TextEntrySheetState(); } class _TextEntrySheetState extends State<_TextEntrySheet> { late final TextEditingController _controller; bool _saving = false; String? _errorText; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _submit() async { if (_saving) { return; } final value = _controller.text.trim(); if (value.isEmpty) { setState(() => _errorText = context.l10n.textEntryRequired); return; } setState(() { _saving = true; _errorText = null; }); final saved = await widget.onSubmit(value); if (!mounted) { return; } if (saved) { Navigator.pop(context); return; } setState(() { _saving = false; _errorText = context.l10n.saveFailed; }); } @override Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); final scheme = theme.colorScheme; final foreground = ThemeData.estimateBrightnessForColor(widget.accent) == Brightness.dark ? Colors.white : scheme.onSurface; return AppBottomSheetScaffold( title: widget.title, icon: widget.icon, accent: widget.accent, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SurfacePanel( radius: 18, tone: SurfacePanelTone.paper, padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: TextField( controller: _controller, autofocus: true, maxLength: widget.maxLength, minLines: widget.minLines, maxLines: widget.maxLines, textCapitalization: TextCapitalization.sentences, textInputAction: widget.maxLines == 1 ? TextInputAction.done : TextInputAction.newline, onChanged: (_) { if (_errorText == null) { setState(() {}); return; } setState(() => _errorText = null); }, onSubmitted: (_) { if (widget.maxLines == 1) { unawaited(_submit()); } }, decoration: InputDecoration( filled: false, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, hintText: widget.hintText, errorText: _errorText, ), ), ), const SizedBox(height: 14), Row( children: [ Expanded( child: TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.cancel), ), ), const SizedBox(width: 10), Expanded( flex: 2, child: FilledButton( onPressed: _saving || _controller.text.trim().isEmpty ? null : _submit, style: FilledButton.styleFrom( backgroundColor: widget.accent, foregroundColor: foreground, minimumSize: const Size.fromHeight(50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), textStyle: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), child: _saving ? AppBusyIndicator( semanticLabel: l10n.savingLabel, color: foreground, ) : Text(l10n.save), ), ), ], ), ], ), ); } } Future confirmDeleteHabit( BuildContext context, VoidCallback onConfirm, ) async { await showConfirmSheet( context: context, title: context.l10n.deleteHabitTitle, body: context.l10n.deleteHabitBody, confirmLabel: context.l10n.delete, icon: Icons.delete_outline_rounded, accentBuilder: (context) => Theme.of(context).colorScheme.error, onConfirm: onConfirm, ); } Future confirmClearToday( BuildContext context, VoidCallback onConfirm, ) async { await showConfirmSheet( context: context, title: context.l10n.clearTodayDataTitle, body: context.l10n.clearTodayDataBody, confirmLabel: context.l10n.clearToday, icon: Icons.refresh_rounded, accentBuilder: (context) => FocusSeedVisuals.of(context).warning, onConfirm: onConfirm, ); } Future confirmClearData( BuildContext context, VoidCallback onConfirm, ) async { await showConfirmSheet( context: context, title: context.l10n.clearAllDataTitle, body: context.l10n.clearAllDataBody, confirmLabel: context.l10n.deleteAll, icon: Icons.delete_forever_rounded, accentBuilder: (context) => Theme.of(context).colorScheme.error, onConfirm: onConfirm, ); } Future showConfirmSheet({ required BuildContext context, required String title, required String body, required String confirmLabel, required IconData icon, required Color Function(BuildContext context) accentBuilder, required VoidCallback onConfirm, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => _ConfirmSheet( title: title, body: body, confirmLabel: confirmLabel, icon: icon, accent: accentBuilder(context), onConfirm: onConfirm, ), ); } class _ConfirmSheet extends StatelessWidget { const _ConfirmSheet({ required this.title, required this.body, required this.confirmLabel, required this.icon, required this.accent, required this.onConfirm, }); final String title; final String body; final String confirmLabel; final IconData icon; final Color accent; final VoidCallback onConfirm; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; return AppBottomSheetScaffold( title: title, icon: icon, accent: accent, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SurfacePanel( radius: 18, padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), color: Color.alphaBlend( accent.withValues(alpha: 0.045), scheme.surfaceContainerLowest, ), child: Text( body, style: theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurface, height: 1.42, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 14), _ConfirmActions( accent: accent, confirmLabel: confirmLabel, onConfirm: onConfirm, ), ], ), ); } } void showPrivacySheet(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (context) => AppBottomSheetScaffold( title: context.l10n.privacyTitle, subtitle: context.l10n.privacySubtitle, icon: Icons.lock_outline_rounded, accent: FocusSeedVisuals.of(context).privacy, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _PrivacyPoint( icon: Icons.phone_android_rounded, text: context.l10n.privacyBodyOne, ), const SizedBox(height: 12), _PrivacyPoint( icon: Icons.cleaning_services_rounded, text: context.l10n.privacyBodyTwo, ), ], ), ), ); } class _PrivacyPoint extends StatelessWidget { const _PrivacyPoint({required this.icon, required this.text}); final IconData icon; final String text; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final visuals = FocusSeedVisuals.of(context); return SurfacePanel( radius: 18, padding: const EdgeInsets.fromLTRB(13, 13, 13, 13), color: Color.alphaBlend( visuals.privacy.withValues(alpha: 0.045), scheme.surfaceContainerLowest, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: visuals.privacy, size: 21), const SizedBox(width: 11), Expanded( child: Text( text, style: theme.textTheme.bodyMedium?.copyWith( color: scheme.onSurface, height: 1.42, fontWeight: FontWeight.w600, ), ), ), ], ), ); } } void showSnack(BuildContext context, String message) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(message), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.fromLTRB(16, 0, 16, 92), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), ); } class _ConfirmActions extends StatelessWidget { const _ConfirmActions({ required this.accent, required this.confirmLabel, required this.onConfirm, }); final Color accent; final String confirmLabel; final VoidCallback onConfirm; @override Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); final scheme = theme.colorScheme; final foreground = ThemeData.estimateBrightnessForColor(accent) == Brightness.dark ? Colors.white : scheme.onSurface; final confirmButton = FilledButton( style: FilledButton.styleFrom( backgroundColor: accent, foregroundColor: foreground, minimumSize: const Size.fromHeight(50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), textStyle: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), onPressed: () { onConfirm(); Navigator.pop(context); }, child: Text(confirmLabel, maxLines: 1, overflow: TextOverflow.ellipsis), ); final cancelButton = TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.cancel), ); return LayoutBuilder( builder: (context, constraints) { final textScale = MediaQuery.textScalerOf(context).scale(1); if (constraints.maxWidth < 330 || textScale > 1.25) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [confirmButton, const SizedBox(height: 8), cancelButton], ); } return Row( children: [ Expanded(child: cancelButton), const SizedBox(width: 10), Expanded(flex: 2, child: confirmButton), ], ); }, ); } }