From 04863ff008068163b0d1e02acab2d04155e310ab Mon Sep 17 00:00:00 2001 From: m3mo Date: Tue, 3 Feb 2026 14:21:58 +0100 Subject: [PATCH] Add setup and onboarding screens - Create SetupPage for choosing local or online mode - Create OnboardingPage with 3-slide tutorial - Explain Eisenhower Method and ALPEN planning approach --- .../presentation/pages/onboarding_page.dart | 198 ++++++++++++++++++ .../presentation/pages/setup_page.dart | 159 ++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 lib/features/onboarding/presentation/pages/onboarding_page.dart create mode 100644 lib/features/onboarding/presentation/pages/setup_page.dart diff --git a/lib/features/onboarding/presentation/pages/onboarding_page.dart b/lib/features/onboarding/presentation/pages/onboarding_page.dart new file mode 100644 index 0000000..f0b800d --- /dev/null +++ b/lib/features/onboarding/presentation/pages/onboarding_page.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../../../../l10n/app_localizations.dart'; +import '../../../settings/presentation/viewmodels/settings_viewmodel.dart'; + +class OnboardingPage extends StatefulWidget { + const OnboardingPage({super.key}); + + @override + State createState() => _OnboardingPageState(); +} + +class _OnboardingPageState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + Future _completeOnboarding() async { + final settingsVm = context.read(); + await settingsVm.setOnboardingShown(true); + + if (mounted) { + // Navigate based on app mode + if (settingsVm.isOnlineMode) { + context.go('/login'); + } else { + context.go('/'); + } + } + } + + void _nextPage() { + if (_currentPage < 2) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + _completeOnboarding(); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + final pages = [ + _OnboardingSlide( + icon: Icons.schedule, + title: l10n.onboardingTitle1, + description: l10n.onboardingDesc1, + ), + _OnboardingSlide( + icon: Icons.psychology, + title: l10n.onboardingTitle2, + description: l10n.onboardingDesc2, + ), + _OnboardingSlide( + icon: Icons.rocket_launch, + title: l10n.onboardingTitle3, + description: l10n.onboardingDesc3, + ), + ]; + + return Scaffold( + body: SafeArea( + child: Column( + children: [ + // Skip button + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: _completeOnboarding, + child: Text(l10n.skip), + ), + ), + ), + // Page content + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: (page) { + setState(() { + _currentPage = page; + }); + }, + children: pages, + ), + ), + // Page indicator + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + pages.length, + (index) => AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: _currentPage == index ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: _currentPage == index + ? theme.colorScheme.primary + : theme.colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + // Next/Get Started button + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 32), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _nextPage, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + _currentPage == 2 ? l10n.getStarted : l10n.next, + style: const TextStyle(fontSize: 16), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _OnboardingSlide extends StatelessWidget { + final IconData icon; + final String title; + final String description; + + const _OnboardingSlide({ + required this.icon, + required this.title, + required this.description, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: 64, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 48), + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + description, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/features/onboarding/presentation/pages/setup_page.dart b/lib/features/onboarding/presentation/pages/setup_page.dart new file mode 100644 index 0000000..6539578 --- /dev/null +++ b/lib/features/onboarding/presentation/pages/setup_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../../../../l10n/app_localizations.dart'; +import '../../../settings/domain/enums/app_mode.dart'; +import '../../../settings/presentation/viewmodels/settings_viewmodel.dart'; + +class SetupPage extends StatelessWidget { + const SetupPage({super.key}); + + Future _selectMode(BuildContext context, AppMode mode) async { + final settingsVm = context.read(); + await settingsVm.setAppMode(mode); + await settingsVm.setSetupCompleted(true); + + if (context.mounted) { + context.go('/onboarding'); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Scaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.task_alt, + size: 80, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + l10n.setupTitle, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + l10n.setupSubtitle, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + _ModeCard( + icon: Icons.smartphone, + title: l10n.useLocally, + description: l10n.useLocallyDesc, + onTap: () => _selectMode(context, AppMode.local), + ), + const SizedBox(height: 16), + _ModeCard( + icon: Icons.cloud_sync, + title: l10n.syncOnline, + description: l10n.syncOnlineDesc, + onTap: () => _selectMode(context, AppMode.online), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _ModeCard extends StatelessWidget { + final IconData icon; + final String title; + final String description; + final VoidCallback onTap; + + const _ModeCard({ + required this.icon, + required this.title, + required this.description, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: theme.colorScheme.outlineVariant, + ), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 28, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + } +}