From 8cf43c76e212231e177558f090dce8c9f13c8bb6 Mon Sep 17 00:00:00 2001 From: Pete Gregory Date: Tue, 30 Dec 2025 11:55:46 +0000 Subject: [PATCH] re add calculator page --- lib/components/pages/settings.dart | 184 +++++++++++++++-------------- lib/ui/app_shell.dart | 104 +++++++++------- pubspec.yaml | 2 +- test/app_shell_test.dart | 7 +- 4 files changed, 160 insertions(+), 137 deletions(-) diff --git a/lib/components/pages/settings.dart b/lib/components/pages/settings.dart index 30d70c7..dab0537 100644 --- a/lib/components/pages/settings.dart +++ b/lib/components/pages/settings.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; +import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/endpoint_service.dart'; import 'package:mileograph_flutter/services/data_service.dart'; @@ -174,6 +175,9 @@ class _SettingsPageState extends State { @override Widget build(BuildContext context) { final endpointService = context.watch(); + final loggedIn = context.select( + (auth) => auth.isLoggedIn, + ); if (!endpointService.isLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), @@ -251,97 +255,99 @@ class _SettingsPageState extends State { 'Current: ${endpointService.baseUrl}', style: Theme.of(context).textTheme.labelSmall, ), - const SizedBox(height: 32), - Text( - 'Account', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 8), - Text( - 'Change your password for this account.', - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 12), - Form( - key: _passwordFormKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - controller: _currentPasswordController, - decoration: const InputDecoration( - labelText: 'Current password', - border: OutlineInputBorder(), + if (loggedIn) ...[ + const SizedBox(height: 32), + Text( + 'Account', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - autofillHints: const [AutofillHints.password], - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your current password.'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _newPasswordController, - decoration: const InputDecoration( - labelText: 'New password', - border: OutlineInputBorder(), - ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - autofillHints: const [AutofillHints.newPassword], - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a new password.'; - } - return null; - }, - ), - const SizedBox(height: 12), - TextFormField( - controller: _confirmPasswordController, - decoration: const InputDecoration( - labelText: 'Confirm new password', - border: OutlineInputBorder(), - ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - autofillHints: const [AutofillHints.newPassword], - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please confirm the new password.'; - } - if (value != _newPasswordController.text) { - return 'New passwords do not match.'; - } - return null; - }, - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: _changingPassword ? null : _changePassword, - icon: _changingPassword - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.lock_reset), - label: Text( - _changingPassword ? 'Updating...' : 'Change password', - ), - ), - ], ), - ), + const SizedBox(height: 8), + Text( + 'Change your password for this account.', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 12), + Form( + key: _passwordFormKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _currentPasswordController, + decoration: const InputDecoration( + labelText: 'Current password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.password], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your current password.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _newPasswordController, + decoration: const InputDecoration( + labelText: 'New password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.newPassword], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a new password.'; + } + return null; + }, + ), + const SizedBox(height: 12), + TextFormField( + controller: _confirmPasswordController, + decoration: const InputDecoration( + labelText: 'Confirm new password', + border: OutlineInputBorder(), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.newPassword], + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please confirm the new password.'; + } + if (value != _newPasswordController.text) { + return 'New passwords do not match.'; + } + return null; + }, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: _changingPassword ? null : _changePassword, + icon: _changingPassword + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.lock_reset), + label: Text( + _changingPassword ? 'Updating...' : 'Change password', + ), + ), + ], + ), + ), + ], ], ), ), diff --git a/lib/ui/app_shell.dart b/lib/ui/app_shell.dart index a0e8b67..ecd6426 100644 --- a/lib/ui/app_shell.dart +++ b/lib/ui/app_shell.dart @@ -3,6 +3,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; +import 'package:mileograph_flutter/components/pages/calculator.dart'; +import 'package:mileograph_flutter/components/pages/calculator_details.dart'; import 'package:mileograph_flutter/components/login/login.dart'; import 'package:mileograph_flutter/components/pages/dashboard.dart'; import 'package:mileograph_flutter/components/pages/loco_legs.dart'; @@ -19,10 +21,12 @@ import 'package:mileograph_flutter/services/data_service.dart'; import 'package:mileograph_flutter/services/navigation_guard.dart'; import 'package:provider/provider.dart'; -final GlobalKey _shellNavigatorKey = GlobalKey(); +final GlobalKey _shellNavigatorKey = + GlobalKey(); const List _contentPages = [ "/dashboard", + "/calculator", "/logbook", "/traction", "/add", @@ -31,13 +35,14 @@ const List _contentPages = [ const List _defaultTabDestinations = [ "/dashboard", + "/calculator", "/logbook/entries", "/traction", "/add", "/more", ]; -const int _addTabIndex = 3; +const int _addTabIndex = 4; class _NavItem { final String label; @@ -47,6 +52,7 @@ class _NavItem { const List<_NavItem> _navItems = [ _NavItem("Home", Icons.home), + _NavItem("Calculator", Icons.route), _NavItem("Logbook", Icons.menu_book), _NavItem("Traction", Icons.train), _NavItem("Add", Icons.add), @@ -112,10 +118,7 @@ class _MyAppState extends State { return null; }, routes: [ - GoRoute( - path: '/', - redirect: (context, state) => '/dashboard', - ), + GoRoute(path: '/', redirect: (context, state) => '/dashboard'), ShellRoute( navigatorKey: _shellNavigatorKey, builder: (context, state, child) => MyHomePage(child: child), @@ -124,6 +127,17 @@ class _MyAppState extends State { path: '/dashboard', builder: (context, state) => const Dashboard(), ), + GoRoute( + path: '/calculator', + builder: (context, state) => const CalculatorPage(), + routes: [ + GoRoute( + path: 'details', + builder: (context, state) => + CalculatorDetailsPage(result: state.extra), + ), + ], + ), GoRoute( path: '/logbook', redirect: (context, state) => '/logbook/entries', @@ -212,7 +226,10 @@ class _MyAppState extends State { ), ], ), - GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), GoRoute( path: '/settings', builder: (context, state) => const SettingsPage(), @@ -373,7 +390,10 @@ class _MyHomePageState extends State { TextSpan( children: const [ TextSpan(text: "Mile"), - TextSpan(text: "O", style: TextStyle(color: Colors.red)), + TextSpan( + text: "O", + style: TextStyle(color: Colors.red), + ), TextSpan(text: "graph"), ], style: const TextStyle( @@ -390,7 +410,10 @@ class _MyHomePageState extends State { onPressed: () => context.go('/more/settings'), icon: const Icon(Icons.settings), ), - IconButton(onPressed: auth.logout, icon: const Icon(Icons.logout)), + IconButton( + onPressed: auth.logout, + icon: const Icon(Icons.logout), + ), ], ), bottomNavigationBar: isWide @@ -448,7 +471,8 @@ class _MyHomePageState extends State { return Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.browserBack): const _BackIntent(), - LogicalKeySet(LogicalKeyboardKey.browserForward): const _ForwardIntent(), + LogicalKeySet(LogicalKeyboardKey.browserForward): + const _ForwardIntent(), }, child: Actions( actions: { @@ -474,7 +498,10 @@ class _MyHomePageState extends State { canPop: false, onPopInvokedWithResult: (didPop, _) async { if (didPop) return; - await _handleBackNavigation(allowExit: true, recordForward: false); + await _handleBackNavigation( + allowExit: true, + recordForward: false, + ); }, child: scaffold, ), @@ -494,7 +521,9 @@ class _MyHomePageState extends State { } Widget _buildRailToggleButton(bool railExtended) { - final collapseIcon = railExtended ? Icons.chevron_left : Icons.chevron_right; + final collapseIcon = railExtended + ? Icons.chevron_left + : Icons.chevron_right; final collapseLabel = railExtended ? 'Collapse' : 'Expand'; if (railExtended) { @@ -587,8 +616,9 @@ class _MyHomePageState extends State { final data = context.watch(); final notifications = data.notifications; final loading = data.isNotificationsLoading; - final listHeight = - isWide ? 380.0 : MediaQuery.of(context).size.height * 0.6; + final listHeight = isWide + ? 380.0 + : MediaQuery.of(context).size.height * 0.6; Widget body; if (loading && notifications.isEmpty) { @@ -628,9 +658,7 @@ class _MyHomePageState extends State { item.title.isNotEmpty ? item.title : 'Notification', - style: Theme.of(context) - .textTheme - .titleMedium + style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 4), @@ -642,18 +670,15 @@ class _MyHomePageState extends State { const SizedBox(height: 6), Text( _formatNotificationTime(item.createdAt!), - style: Theme.of(context) - .textTheme - .bodySmall + style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: () { - final baseColor = Theme.of(context) - .textTheme - .bodySmall - ?.color; + final baseColor = Theme.of( + context, + ).textTheme.bodySmall?.color; if (baseColor == null) return null; - final newAlpha = - (baseColor.a * 0.7).clamp(0.0, 1.0); + final newAlpha = (baseColor.a * 0.7) + .clamp(0.0, 1.0); return baseColor.withValues( alpha: newAlpha, ); @@ -666,10 +691,8 @@ class _MyHomePageState extends State { ), const SizedBox(width: 8), TextButton( - onPressed: () => _dismissNotifications( - context, - [item.id], - ), + onPressed: () => + _dismissNotifications(context, [item.id]), child: const Text('Dismiss'), ), ], @@ -695,19 +718,18 @@ class _MyHomePageState extends State { children: [ Text( 'Notifications', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const Spacer(), TextButton( onPressed: notifications.isEmpty ? null : () => _dismissNotifications( - context, - notifications.map((e) => e.id).toList(), - ), + context, + notifications.map((e) => e.id).toList(), + ), child: const Text('Dismiss all'), ), ], @@ -729,9 +751,7 @@ class _MyHomePageState extends State { try { await context.read().dismissNotifications(ids); } catch (e) { - messenger?.showSnackBar( - SnackBar(content: Text('Failed to dismiss: $e')), - ); + messenger?.showSnackBar(SnackBar(content: Text('Failed to dismiss: $e'))); } } @@ -751,9 +771,7 @@ class _MyHomePageState extends State { color: Colors.redAccent, borderRadius: BorderRadius.circular(10), ), - constraints: const BoxConstraints( - minWidth: 20, - ), + constraints: const BoxConstraints(minWidth: 20), child: Text( label, style: const TextStyle( diff --git a/pubspec.yaml b/pubspec.yaml index 83c97c1..ae41d9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.4.3+1 +version: 0.4.4+1 environment: sdk: ^3.8.1 diff --git a/test/app_shell_test.dart b/test/app_shell_test.dart index 1f4d523..c2cd04d 100644 --- a/test/app_shell_test.dart +++ b/test/app_shell_test.dart @@ -8,13 +8,12 @@ void main() { expect(tabIndexForPath('/calculator/details'), 1); expect(tabIndexForPath('/legs'), 2); expect(tabIndexForPath('/traction/12/timeline'), 3); - expect(tabIndexForPath('/trips'), 4); - expect(tabIndexForPath('/add'), 5); + expect(tabIndexForPath('/trips'), 2); + expect(tabIndexForPath('/add'), 4); }); test('tabIndexForPath ignores query when parsing uri', () { - expect(tabIndexForPath(Uri.parse('/trips?sort=desc').path), 4); + expect(tabIndexForPath(Uri.parse('/trips?sort=desc').path), 2); expect(tabIndexForPath(Uri.parse('/calculator/details?x=1').path), 1); }); } -