diff --git a/analysis_options.yaml b/analysis_options.yaml index baf162b..8e56bae 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,12 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + errors: + invalid_annotation_target: ignore + plugins: + - custom_lint + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` @@ -23,12 +29,5 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - - errors: - invalid_annotation_target: ignore - - - plugins: - - custom_lint # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/integration_test/simple_test.dart b/integration_test/simple_test.dart index b1a5075..c15d7a8 100644 --- a/integration_test/simple_test.dart +++ b/integration_test/simple_test.dart @@ -1,13 +1,27 @@ +import 'package:abawo_bt_app/util/sharedPrefs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:abawo_bt_app/main.dart'; import 'package:abawo_bt_app/src/rust/frb_generated.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async => await RustLib.init()); - testWidgets('Can call rust function', (WidgetTester tester) async { - await tester.pumpWidget(const MyApp()); - expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget); + + testWidgets('App launches to devices tab', (WidgetTester tester) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + + await tester.pumpWidget( + ProviderScope( + overrides: [sharedPreferencesProvider.overrideWithValue(prefs)], + child: const AbawoBtApp(), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Manage and connect your hardware.'), findsOneWidget); }); } diff --git a/lib/database/database.dart b/lib/database/database.dart index c330622..10ca118 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -10,13 +10,13 @@ part 'database.g.dart'; class NConnectedDevices extends _$NConnectedDevices { @override Future> build() async { - final db = await ref.watch(databaseProvider); - return await db.getAllConnectedDevices(); + final db = ref.watch(databaseProvider); + return db.getAllConnectedDevices(); } Future> addConnectedDevice( ConnectedDevicesCompanion device) async { - final db = await ref.watch(databaseProvider); + final db = ref.watch(databaseProvider); final res = await db.addConnectedDevice(device); if (res.isOk()) { ref.invalidateSelf(); @@ -25,7 +25,7 @@ class NConnectedDevices extends _$NConnectedDevices { } Future> deleteConnectedDevice(int id) async { - final db = await ref.watch(databaseProvider); + final db = ref.watch(databaseProvider); final res = await db.deleteConnectedDevice(id); if (res.isOk()) { ref.invalidateSelf(); diff --git a/lib/main.dart b/lib/main.dart index f2e1c26..37f0d5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,19 +1,21 @@ import 'package:abawo_bt_app/pages/devices_page.dart'; +import 'package:abawo_bt_app/pages/devices_tab_page.dart'; import 'package:abawo_bt_app/src/rust/frb_generated.dart'; +import 'package:abawo_bt_app/theme/app_theme.dart'; import 'package:abawo_bt_app/util/sharedPrefs.dart'; +import 'package:abawo_bt_app/widgets/app_shell.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:nb_utils/nb_utils.dart'; -import 'pages/home_page.dart'; import 'pages/settings_page.dart'; import 'package:abawo_bt_app/pages/device_details_page.dart'; Future main() async { Logger.root.level = Level.ALL; // defaults to Level.INFO Logger.root.onRecord.listen((record) { - print('${record.level.name}: ${record.time}: ${record.message}'); + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); }); await RustLib.init(); WidgetsFlutterBinding.ensureInitialized(); @@ -27,24 +29,22 @@ Future main() async { ], child: const AbawoBtApp())); } -class AbawoBtApp extends StatelessWidget { +class AbawoBtApp extends ConsumerWidget { const AbawoBtApp({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final themePreference = ref.watch(appThemePreferenceProvider); + return MaterialApp.router( title: 'Abawo BT App', - theme: ThemeData( - primarySwatch: Colors.blue, - useMaterial3: true, - brightness: Brightness.light, - ), - darkTheme: ThemeData( - primarySwatch: Colors.blue, - useMaterial3: true, - brightness: Brightness.dark, - ), - themeMode: ThemeMode.system, + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), + themeMode: switch (themePreference) { + AppThemePreference.light => ThemeMode.light, + AppThemePreference.dark => ThemeMode.dark, + AppThemePreference.system => ThemeMode.system, + }, routerConfig: _router, debugShowCheckedModeBanner: false, ); @@ -54,22 +54,28 @@ class AbawoBtApp extends StatelessWidget { // Configure GoRouter final _router = GoRouter( navigatorKey: navigatorKey, - initialLocation: '/', + initialLocation: '/devices', routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomePage(), + ShellRoute( + builder: (context, state, child) => AppShell( + currentLocation: state.uri.path, + child: child, + ), routes: [ GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), + path: '/devices', + builder: (context, state) => const DevicesTabPage(), ), GoRoute( - path: 'connect_device', - builder: (context, state) => const ConnectDevicePage(), - ) + path: '/settings', + builder: (context, state) => const SettingsPage(), + ), ], ), + GoRoute( + path: '/connect_device', + builder: (context, state) => const ConnectDevicePage(), + ), GoRoute( path: '/device/:deviceAddress', builder: (context, state) { diff --git a/lib/pages/device_details_page.dart b/lib/pages/device_details_page.dart index b1e315b..0852450 100644 --- a/lib/pages/device_details_page.dart +++ b/lib/pages/device_details_page.dart @@ -587,7 +587,7 @@ class _DeviceDetailsPageState extends ConsumerState { } toast(toastMessage); - context.replace('/'); + context.replace('/devices'); } Future _cancelReconnect() async { @@ -599,7 +599,7 @@ class _DeviceDetailsPageState extends ConsumerState { if (!mounted) { return; } - context.replace('/'); + context.replace('/devices'); } void _showStatusHistory() { diff --git a/lib/pages/devices_page.dart b/lib/pages/devices_page.dart index b8291c1..bec64c7 100644 --- a/lib/pages/devices_page.dart +++ b/lib/pages/devices_page.dart @@ -65,7 +65,7 @@ class _ConnectDevicePageState extends ConsumerState title: const Text('Connect Device'), leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => context.go('/'), + onPressed: () => context.go('/devices'), ), actions: [ Padding( @@ -188,7 +188,10 @@ class _ConnectDevicePageState extends ConsumerState return; } else { final res = await controller.connect(device); - print('res: $res'); + if (!mounted) { + return; + } + switch (res) { case Ok(): // trigger pairing/permission prompt if needed @@ -231,6 +234,9 @@ class _ConnectDevicePageState extends ConsumerState } break; case Err(:final v): + if (!context.mounted) { + break; + } ScaffoldMessenger.of(context) .showSnackBar(SnackBar( content: Text( @@ -239,7 +245,6 @@ class _ConnectDevicePageState extends ConsumerState break; } } - print('Tapped on ${device.id}'); }, child: DeviceListItem( deviceName: deviceName, diff --git a/lib/pages/devices_tab_page.dart b/lib/pages/devices_tab_page.dart new file mode 100644 index 0000000..c7816a2 --- /dev/null +++ b/lib/pages/devices_tab_page.dart @@ -0,0 +1,67 @@ +import 'package:abawo_bt_app/pages/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DevicesTabPage extends StatelessWidget { + const DevicesTabPage({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Devices', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 6), + Text( + 'Manage and connect your hardware.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.68), + ), + ), + ], + ), + ), + IconButton.filledTonal( + tooltip: 'Connect a device', + onPressed: () => context.go('/connect_device'), + icon: const Icon(Icons.add), + ), + ], + ), + const SizedBox(height: 20), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Saved devices', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + const DevicesList(), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 0cfe003..4a62369 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -181,6 +181,10 @@ class _DevicesListState extends ConsumerState { timeout: const Duration(seconds: 10), ); + if (!context.mounted) { + return; + } + if (result.isOk()) { context.go('/device/${device.deviceAddress}'); } else { @@ -193,13 +197,18 @@ class _DevicesListState extends ConsumerState { ); } } catch (e) { + if (!context.mounted) { + return; + } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${e.toString()}')), ); } finally { - setState(() { - _connectingDeviceId = null; - }); + if (context.mounted) { + setState(() { + _connectingDeviceId = null; + }); + } } }, child: DeviceListItem( diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 2bc3cb9..a6e3266 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,50 +1,54 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.go('/'), + return ListView( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + children: [ + Text( + 'Settings', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), - ), - body: ListView( - children: [ - ListTile( - leading: const Icon(Icons.brightness_6), - title: const Text('Theme'), - subtitle: const Text('Change app theme'), - trailing: const Icon(Icons.chevron_right), - onTap: () { - // Theme settings functionality - }, + const SizedBox(height: 6), + Text( + 'Theme, Bluetooth, and app details.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.68), + ), + ), + const SizedBox(height: 20), + Card( + child: Column( + children: const [ + ListTile( + leading: Icon(Icons.brightness_6), + title: Text('Theme'), + subtitle: Text('Theme controls arrive in the next phase'), + ), + Divider(height: 1), + ListTile( + leading: Icon(Icons.bluetooth), + title: Text('Bluetooth Settings'), + subtitle: Text('Configure Bluetooth connections'), + ), + Divider(height: 1), + ListTile( + leading: Icon(Icons.info), + title: Text('About'), + subtitle: Text('App information'), + ), + ], ), - ListTile( - leading: const Icon(Icons.bluetooth), - title: const Text('Bluetooth Settings'), - subtitle: const Text('Configure Bluetooth connections'), - trailing: const Icon(Icons.chevron_right), - onTap: () { - // Bluetooth settings functionality - }, - ), - ListTile( - leading: const Icon(Icons.info), - title: const Text('About'), - subtitle: const Text('App information'), - trailing: const Icon(Icons.chevron_right), - onTap: () { - // About screen functionality - }, - ), - ], - ), + ), + ], ); } } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..84816a0 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static const Color brandBlue = Color(0xFF2F7BFF); + static const Color darkBackground = Color(0xFF071A2E); + static const Color darkSurface = Color(0xFF10253A); + static const Color darkSurfaceAlt = Color(0xFF173149); + static const Color lightBackground = Color(0xFFF4F8FC); + static const Color lightSurface = Color(0xFFFFFFFF); + static const Color lightSurfaceAlt = Color(0xFFEAF1F8); + + static ThemeData light() { + return _buildTheme( + brightness: Brightness.light, + colorScheme: ColorScheme.fromSeed( + seedColor: brandBlue, + brightness: Brightness.light, + primary: brandBlue, + surface: lightSurface, + ), + scaffoldBackgroundColor: lightBackground, + panelColor: lightSurface, + panelAltColor: lightSurfaceAlt, + ); + } + + static ThemeData dark() { + return _buildTheme( + brightness: Brightness.dark, + colorScheme: ColorScheme.fromSeed( + seedColor: brandBlue, + brightness: Brightness.dark, + primary: brandBlue, + surface: darkSurface, + ), + scaffoldBackgroundColor: darkBackground, + panelColor: darkSurface, + panelAltColor: darkSurfaceAlt, + ); + } + + static ThemeData _buildTheme({ + required Brightness brightness, + required ColorScheme colorScheme, + required Color scaffoldBackgroundColor, + required Color panelColor, + required Color panelAltColor, + }) { + final isDark = brightness == Brightness.dark; + final onSurfaceMuted = colorScheme.onSurface.withValues(alpha: 0.68); + final baseChipTheme = ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: colorScheme, + ).chipTheme; + + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: colorScheme, + scaffoldBackgroundColor: scaffoldBackgroundColor, + dividerColor: colorScheme.outlineVariant.withValues(alpha: 0.42), + appBarTheme: AppBarTheme( + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + foregroundColor: colorScheme.onSurface, + centerTitle: false, + titleTextStyle: TextStyle( + color: colorScheme.onSurface, + fontSize: 28, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + ), + ), + cardTheme: CardThemeData( + elevation: 0, + color: panelColor, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + side: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.55), + ), + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: panelColor, + indicatorColor: colorScheme.primary.withValues(alpha: isDark ? 0.20 : 0.14), + labelTextStyle: WidgetStateProperty.resolveWith( + (states) => TextStyle( + fontSize: 12, + fontWeight: states.contains(WidgetState.selected) + ? FontWeight.w700 + : FontWeight.w500, + ), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + minimumSize: const Size.fromHeight(52), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.onSurface, + minimumSize: const Size.fromHeight(48), + side: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.8), + ), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + ), + listTileTheme: ListTileThemeData( + iconColor: colorScheme.primary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + chipTheme: baseChipTheme.copyWith( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999), + ), + side: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.6), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: panelAltColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + hintStyle: TextStyle(color: onSurfaceMuted), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.55), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide(color: colorScheme.primary, width: 1.2), + ), + ), + dialogTheme: DialogThemeData( + backgroundColor: panelColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: panelColor, + modalBackgroundColor: panelColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: panelAltColor, + contentTextStyle: TextStyle(color: colorScheme.onSurface), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + ), + ); + } +} diff --git a/lib/util/sharedPrefs.dart b/lib/util/sharedPrefs.dart index 5edce16..917af90 100644 --- a/lib/util/sharedPrefs.dart +++ b/lib/util/sharedPrefs.dart @@ -1,3 +1,5 @@ +// ignore_for_file: file_names + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -21,43 +23,61 @@ class SharedPrefNotifier extends StateNotifier { // Helper to get the value with proper typing static T _getValue(SharedPreferences prefs, String key, T defaultValue) { - switch (T) { - case String: - return prefs.getString(key) as T ?? defaultValue; - case bool: - return prefs.getBool(key) as T ?? defaultValue; - case int: - return prefs.getInt(key) as T ?? defaultValue; - case double: - return prefs.getDouble(key) as T ?? defaultValue; - case const (List): - return prefs.getStringList(key) as T ?? defaultValue; - default: - throw UnsupportedError('Type $T not supported by SharedPreferences'); + if (T == String) { + return prefs.getString(key) as T? ?? defaultValue; } + + if (T == bool) { + return prefs.getBool(key) as T? ?? defaultValue; + } + + if (T == int) { + return prefs.getInt(key) as T? ?? defaultValue; + } + + if (T == double) { + return prefs.getDouble(key) as T? ?? defaultValue; + } + + if (T == List) { + return prefs.getStringList(key) as T? ?? defaultValue; + } + + throw UnsupportedError('Type $T not supported by SharedPreferences'); } Future update(T value) async { - switch (T) { - case String: - await prefs.setString(key, value as String); - break; - case bool: - await prefs.setBool(key, value as bool); - break; - case int: - await prefs.setInt(key, value as int); - break; - case double: - await prefs.setDouble(key, value as double); - break; - case const (List): - await prefs.setStringList(key, value as List); - break; - default: - throw UnsupportedError('Type $T not supported by SharedPreferences'); + if (T == String) { + await prefs.setString(key, value as String); + state = value; + return; } - state = value; + + if (T == bool) { + await prefs.setBool(key, value as bool); + state = value; + return; + } + + if (T == int) { + await prefs.setInt(key, value as int); + state = value; + return; + } + + if (T == double) { + await prefs.setDouble(key, value as double); + state = value; + return; + } + + if (T == List) { + await prefs.setStringList(key, value as List); + state = value; + return; + } + + throw UnsupportedError('Type $T not supported by SharedPreferences'); } } @@ -70,3 +90,37 @@ final isDarkModeProvider = defaultValue: false, ); }); + +enum AppThemePreference { + system, + light, + dark, +} + +class ThemePreferenceNotifier extends StateNotifier { + ThemePreferenceNotifier(this._prefs) + : super(_themePreferenceFromStorage(_prefs.getString(_themeModeKey))); + + static const String _themeModeKey = 'app_theme_preference'; + + final SharedPreferences _prefs; + + Future update(AppThemePreference value) async { + await _prefs.setString(_themeModeKey, value.name); + state = value; + } +} + +AppThemePreference _themePreferenceFromStorage(String? value) { + return switch (value) { + 'light' => AppThemePreference.light, + 'dark' => AppThemePreference.dark, + _ => AppThemePreference.system, + }; +} + +final appThemePreferenceProvider = + StateNotifierProvider((ref) { + final prefs = ref.watch(sharedPreferencesProvider); + return ThemePreferenceNotifier(prefs); +}); diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart new file mode 100644 index 0000000..855bcd5 --- /dev/null +++ b/lib/widgets/app_shell.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AppShell extends StatelessWidget { + const AppShell({ + required this.child, + required this.currentLocation, + super.key, + }); + + final Widget child; + final String currentLocation; + + @override + Widget build(BuildContext context) { + final selectedIndex = currentLocation.startsWith('/settings') ? 1 : 0; + + return Scaffold( + body: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).colorScheme.surface, + ], + ), + ), + child: SafeArea(bottom: false, child: child), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: NavigationBar( + selectedIndex: selectedIndex, + onDestinationSelected: (index) { + switch (index) { + case 0: + context.go('/devices'); + break; + case 1: + context.go('/settings'); + break; + } + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.bluetooth_searching_outlined), + selectedIcon: Icon(Icons.bluetooth_searching), + label: 'Devices', + ), + NavigationDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: 'Settings', + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/horizontal_scanning_animation.dart b/lib/widgets/horizontal_scanning_animation.dart index 23de97e..de8729c 100644 --- a/lib/widgets/horizontal_scanning_animation.dart +++ b/lib/widgets/horizontal_scanning_animation.dart @@ -12,7 +12,7 @@ class HorizontalScanningAnimation extends StatefulWidget { this.height = 50.0, }); @override - _HorizontalScanningAnimationState createState() => + State createState() => _HorizontalScanningAnimationState(); } @@ -117,8 +117,6 @@ class _HorizontalWavePainter extends CustomPainter { double currentWidth = width * easedProgress * 0.8; // Max 80% width expansion double startX = (width / 2) - (currentWidth / 2); - double endX = (width / 2) + (currentWidth / 2); - // Calculate opacity based on progress (fade in and out) double opacity; if (waveProgress < 0.1) { @@ -130,8 +128,9 @@ class _HorizontalWavePainter extends CustomPainter { } opacity = max(0.0, opacity); // Clamp opacity - if (opacity <= 0.0 || currentWidth < 5) - continue; // Skip drawing if invisible or too small + if (opacity <= 0.0 || currentWidth < 5) { + continue; + } // Create the wave path final path = Path(); diff --git a/lib/widgets/scanning_animation.dart b/lib/widgets/scanning_animation.dart index 4005a1a..b5daa61 100644 --- a/lib/widgets/scanning_animation.dart +++ b/lib/widgets/scanning_animation.dart @@ -16,7 +16,7 @@ class ScanningWaveAnimation extends StatefulWidget { }); @override - _ScanningWaveAnimationState createState() => _ScanningWaveAnimationState(); + State createState() => _ScanningWaveAnimationState(); } class _ScanningWaveAnimationState extends State {