diff --git a/lib/main.dart b/lib/main.dart index d9d48539..1c01e2d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:edumfa_authenticator/utils/globals.dart'; import 'package:edumfa_authenticator/utils/logger.dart'; @@ -78,8 +79,12 @@ class EduMFAAuthenticator extends ConsumerWidget { WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(appConstraintsProvider.notifier).state = constraints; }); - return DynamicColorBuilder( - builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + return PlatformProvider( + settings: PlatformSettingsData( + iosUsesMaterialWidgets: true + ), + builder: (context) => DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { ThemeData lightTheme = ThemeData(colorScheme: generateColorScheme( lightDynamic?.primary, brandColor @@ -100,34 +105,39 @@ class EduMFAAuthenticator extends ConsumerWidget { lightTheme = lightTheme.copyWith(navigationRailTheme: navigationRailThemeData); darkTheme = darkTheme.copyWith(navigationRailTheme: navigationRailThemeData); - return MaterialApp( - debugShowCheckedModeBanner: true, - navigatorKey: globalNavigatorKey, - localizationsDelegates: [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate - ], - supportedLocales: S.delegate.supportedLocales, - title: appName, - theme: lightTheme, - darkTheme: darkTheme, - scaffoldMessengerKey: globalSnackbarKey, // <= this + return PlatformTheme( themeMode: EasyDynamicTheme.of(context).themeMode, - initialRoute: SplashScreen.routeName, - routes: { - AboutSettingsView.routeName: (context) => const AboutSettingsView(), - AppearanceSettingsView.routeName: (context) => const AppearanceSettingsView(), - LicenseView.routeName: (context) => const LicenseView(), - MainView.routeName: (context) => const MainView(), - OnboardingView.routeName: (context) => const OnboardingView(), - PushTokenSettingsView.routeName: (context) => const PushTokenSettingsView(), - SettingsView.routeName: (context) => const SettingsView(), - SplashScreen.routeName: (context) => const SplashScreen(), - }, + materialLightTheme: lightTheme, + materialDarkTheme: darkTheme, + builder: (context) => PlatformApp( + debugShowCheckedModeBanner: true, + navigatorKey: globalNavigatorKey, + localizationsDelegates: [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate + ], + supportedLocales: S.delegate.supportedLocales, + title: appName, + initialRoute: SplashScreen.routeName, + routes: { + AboutSettingsView.routeName: (context) => const AboutSettingsView(), + AppearanceSettingsView.routeName: (context) => const AppearanceSettingsView(), + LicenseView.routeName: (context) => const LicenseView(), + MainView.routeName: (context) => const MainView(), + OnboardingView.routeName: (context) => const OnboardingView(), + PushTokenSettingsView.routeName: (context) => const PushTokenSettingsView(), + SettingsView.routeName: (context) => const SettingsView(), + SplashScreen.routeName: (context) => const SplashScreen(), + }, + material: (_, __) => MaterialAppData( + scaffoldMessengerKey: globalSnackbarKey + ), + ), ); - } + }, + ), ); }); } diff --git a/lib/views/tokens_view/tokens_view.dart b/lib/views/tokens_view/tokens_view.dart index a1616a88..0d5a04fc 100644 --- a/lib/views/tokens_view/tokens_view.dart +++ b/lib/views/tokens_view/tokens_view.dart @@ -8,7 +8,9 @@ import 'package:edumfa_authenticator/views/tokens_view/tokens_view_widgets/token import 'package:edumfa_authenticator/views/view_interface.dart'; import 'package:edumfa_authenticator/widgets/conditional_floating_action_button.dart'; import 'package:edumfa_authenticator/widgets/status_bar.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; @@ -28,21 +30,39 @@ class TokensViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: !isTablet(context) - ? ConditionalFloatingActionButton( - isExtended: ref.watch(tokenProvider).tokens.isEmpty, - label: S.of(context).addToken, - icon: Icons.add, + return PlatformScaffold( + material: (_, __) => MaterialScaffoldData( + resizeToAvoidBottomInset: false, + floatingActionButton: !isTablet(context) + ? ConditionalFloatingActionButton( + isExtended: ref.watch(tokenProvider).tokens.isEmpty, + label: S.of(context).addToken, + icon: Icons.add, + onPressed: () async => await showAddTokenSheet(context), + ) : null, + appBar: const PreferredSize( + preferredSize: Size.fromHeight(70), + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(left: 20, right: 20, top: 10), + child: TokenSearchBar(), + ), + ), + ), + ), + appBar: PlatformAppBar( + cupertino: (context, platform) => CupertinoNavigationBarData( + title: Text(S.of(context).tokens), + trailing: CupertinoButton( + child: const Icon(CupertinoIcons.add), onPressed: () async => await showAddTokenSheet(context), - ) : null, - appBar: const PreferredSize( - preferredSize: Size.fromHeight(70), - child: SafeArea( - child: Padding( - padding: EdgeInsets.only(left: 20, right: 20, top: 10), - child: TokenSearchBar(), + ), + bottom: const PreferredSize( + preferredSize: Size.fromHeight(35.0 + 8 * 2), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: TokenSearchBar(), + ) ), ), ), @@ -57,12 +77,22 @@ class TokensViewState extends ConsumerState { Future showAddTokenSheet(BuildContext context) async { await Haptics.vibrate(HapticsType.rigid); if (!context.mounted) return; - showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) => const AddTokenSheetWidget(), - ); + if (isMaterial(context)) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => const AddTokenSheetWidget(), + ); + } else { + showCupertinoSheet( + context: context, + pageBuilder: (_) => const CupertinoPageScaffold( + child: AddTokenSheetWidget(), + ), + ); + } + } } diff --git a/lib/views/tokens_view/tokens_view_widgets/token_add_widgets/upload_qr_code_button.dart b/lib/views/tokens_view/tokens_view_widgets/token_add_widgets/upload_qr_code_button.dart index 57bee979..33467551 100644 --- a/lib/views/tokens_view/tokens_view_widgets/token_add_widgets/upload_qr_code_button.dart +++ b/lib/views/tokens_view/tokens_view_widgets/token_add_widgets/upload_qr_code_button.dart @@ -1,5 +1,7 @@ import 'package:edumfa_authenticator/generated/l10n.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -14,7 +16,15 @@ class UploadQrCodeButton extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( width: double.infinity, - child: FilledButton.icon( + child: PlatformElevatedButton( + material: (_, platform) => MaterialElevatedButtonData( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), onPressed: () async { final XFile? image = await ImagePicker().pickImage( source: ImageSource.gallery, @@ -26,14 +36,18 @@ class UploadQrCodeButton extends StatelessWidget { ); handleBarcodes(barcodes); }, - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10.0, + children: [ + PlatformWidget( + material: (_, __) => const Icon(Icons.upload_file), + cupertino: (_, __) => const Icon(CupertinoIcons.arrow_up_doc), + ), + Text(S.of(context).uploadQrCodeButton) + ], ), - icon: const Icon(Icons.upload_file), - label: Text(S.of(context).uploadQrCodeButton), + ), ); } \ No newline at end of file diff --git a/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart b/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart index 00e3167a..e6a2e52f 100644 --- a/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart +++ b/lib/views/tokens_view/tokens_view_widgets/token_search_bar.dart @@ -1,7 +1,9 @@ import 'package:edumfa_authenticator/generated/l10n.dart'; import 'package:edumfa_authenticator/model/states/token_filter.dart'; import 'package:edumfa_authenticator/utils/riverpod_providers.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; @@ -31,60 +33,81 @@ class _TokenSearchBarState extends ConsumerState { @override Widget build(BuildContext context) { var tokenFilter = ref.read(tokenFilterProvider.notifier).state; - return SearchBar( - key: _searchBarKey, - controller: _searchController, - hintText: S.of(context).search, - textInputAction: TextInputAction.search, - focusNode: _searchFocusNode, - onTapOutside: (event) { - final RenderBox box = _searchBarKey.currentContext?.findRenderObject() as RenderBox; - final Rect rect = box.localToGlobal(Offset.zero) & box.size; - if (!rect.contains(event.position)) { - _searchFocusNode.unfocus(); - } - }, - onTap: () async => await Haptics.vibrate(HapticsType.soft), - onChanged: (value) { - ref.read(tokenFilterProvider.notifier).state = _searchController.text.isEmpty - ? null - : TokenFilter(searchQuery: value); - setState(() {}); - }, - leading: SizedBox( - width: 50, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: _searchFocusNode.hasFocus || tokenFilter != null - ? const Center(child: Icon(Icons.search)) - : SvgPicture.asset( - 'res/logo/app_icon.svg', - width: 32, - height: 32, - colorFilter: ColorFilter.mode( - Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white, - BlendMode.srcIn + return PlatformWidget( + material: + (_, __) => SearchBar( + key: _searchBarKey, + controller: _searchController, + hintText: S.of(context).search, + textInputAction: TextInputAction.search, + focusNode: _searchFocusNode, + onTapOutside: (event) { + final RenderBox box = + _searchBarKey.currentContext?.findRenderObject() as RenderBox; + final Rect rect = box.localToGlobal(Offset.zero) & box.size; + if (!rect.contains(event.position)) { + _searchFocusNode.unfocus(); + } + }, + onTap: () async => await Haptics.vibrate(HapticsType.soft), + onChanged: (value) { + ref.read(tokenFilterProvider.notifier).state = + _searchController.text.isEmpty + ? null + : TokenFilter(searchQuery: value); + setState(() {}); + }, + leading: SizedBox( + width: 50, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: + _searchFocusNode.hasFocus || tokenFilter != null + ? const Center(child: Icon(Icons.search)) + : SvgPicture.asset( + 'res/logo/app_icon.svg', + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + BlendMode.srcIn, + ), + ), + ), ), + trailing: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: + tokenFilter?.searchQuery.isNotEmpty ?? false + ? IconButton( + onPressed: () { + _searchController.clear(); + ref.read(tokenFilterProvider.notifier).state = null; + setState(() {}); + }, + icon: const Icon(Icons.close), + ) + : const SizedBox(), + ), + ], + constraints: const BoxConstraints(minHeight: double.infinity), + elevation: WidgetStateProperty.all(0), + ), + // TODO: We need to find a way to unfocus it when tapped outside + cupertino: + (_, __) => CupertinoSearchTextField( + controller: _searchController, + onChanged: (value) { + ref.read(tokenFilterProvider.notifier).state = + _searchController.text.isEmpty + ? null + : TokenFilter(searchQuery: value); + setState(() {}); + }, ), - ), - ), - trailing: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: tokenFilter?.searchQuery.isNotEmpty ?? false - ? IconButton( - onPressed: () { - _searchController.clear(); - ref.read(tokenFilterProvider.notifier).state = null; - setState(() {}); - }, - icon: const Icon(Icons.close) - ) - : const SizedBox() - ) - ], - constraints: const BoxConstraints(minHeight: double.infinity), - elevation: WidgetStateProperty.all(0), ); } diff --git a/pubspec.lock b/pubspec.lock index 42cbcc29..81144bce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -480,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_platform_widgets: + dependency: "direct main" + description: + name: flutter_platform_widgets + sha256: ba28f1a1ee7e46e2b7315405868c2d7126ba67e74e83e9a80538f8d5b5df7b21 + url: "https://pub.dev" + source: hosted + version: "8.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9a599082..adb7703f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: # UI cupertino_icons: ^1.0.8 easy_dynamic_theme: ^2.3.1 + flutter_platform_widgets: ^8.0.0 dynamic_color: ^1.7.0 haptic_feedback: ^0.5.1+1 path_provider: ^2.1.5