diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c52968cd..452771bb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,6 +1,9 @@ name: Windows Builder -on: tag +on: + push: + tags: + - '*.*.*' jobs: build-windows: @@ -14,7 +17,7 @@ jobs: # Install Flutter - uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.10' + flutter-version: '3.16.1' channel: 'stable' # Build Windows artifact diff --git a/lib/config/md_files.dart b/lib/config/md_files.dart deleted file mode 100644 index 8b137891..00000000 --- a/lib/config/md_files.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index aaaf0930..b722d464 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,8 +1,7 @@ import 'package:graphql/client.dart'; import 'package:intl/intl.dart'; -final DateFormat formatter = DateFormat('hh:mm'); - +/// TODO(misterfourtytwo): add equality override class Message { Message({this.text, this.severity = MessageSeverity.normal}) : time = DateTime.now(); @@ -13,7 +12,9 @@ class Message { final String? text; final DateTime time; final MessageSeverity severity; - String get timeString => formatter.format(time); + + static final DateFormat _formatter = DateFormat('hh:mm'); + String get timeString => _formatter.format(time); } enum MessageSeverity { diff --git a/lib/main.dart b/lib/main.dart index 83ca4708..13b05aad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -92,13 +92,15 @@ class SelfprivacyApp extends StatelessWidget { ? ThemeMode.dark : ThemeMode.light, builder: (final BuildContext context, final Widget? widget) { - Widget error = const Text('...rendering error...'); + Widget error = + const Center(child: Text('...rendering error...')); if (widget is Scaffold || widget is Navigator) { - error = Scaffold(body: Center(child: error)); + error = Scaffold(body: error); } ErrorWidget.builder = (final FlutterErrorDetails errorDetails) => error; - return widget!; + + return widget ?? error; }, ); }, diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index 3151aff7..56be04df 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -14,6 +14,7 @@ class BrandHeader extends StatelessWidget { @override Widget build(final BuildContext context) => AppBar( + centerTitle: true, title: Padding( padding: const EdgeInsets.only(top: 4.0), child: Text(title), @@ -25,8 +26,5 @@ class BrandHeader extends StatelessWidget { onBackButtonPressed ?? () => Navigator.of(context).pop(), ) : null, - actions: const [ - SizedBox.shrink(), - ], ); } diff --git a/lib/ui/components/buttons/brand_button.dart b/lib/ui/components/buttons/brand_button.dart index c381af43..bb2e722a 100644 --- a/lib/ui/components/buttons/brand_button.dart +++ b/lib/ui/components/buttons/brand_button.dart @@ -7,8 +7,9 @@ class BrandButton { final String? text, final Widget? child, }) { - assert(text == null || child == null, 'required title or child'); - assert(text != null || child != null, 'required title or child'); + assert((text ?? child) != null, 'either title or child must not be empty'); + assert(text != null || child != null, 'title or child must be provided'); + return ConstrainedBox( constraints: const BoxConstraints( minHeight: 48, @@ -28,8 +29,9 @@ class BrandButton { final String? text, final Widget? child, }) { - assert(text == null || child == null, 'required title or child'); - assert(text != null || child != null, 'required title or child'); + assert((text ?? child) != null, 'either title or child must not be empty'); + assert(text != null || child != null, 'title or child must be provided'); + return ConstrainedBox( constraints: const BoxConstraints( minWidth: double.infinity, diff --git a/lib/ui/components/buttons/buttons.dart b/lib/ui/components/buttons/buttons.dart new file mode 100644 index 00000000..49e0bb07 --- /dev/null +++ b/lib/ui/components/buttons/buttons.dart @@ -0,0 +1,2 @@ +export 'brand_button.dart'; +export 'sp_brand_button.dart'; diff --git a/lib/ui/components/buttons/sp_brand_button.dart b/lib/ui/components/buttons/sp_brand_button.dart new file mode 100644 index 00000000..036b30f2 --- /dev/null +++ b/lib/ui/components/buttons/sp_brand_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class SPBrandButton extends StatelessWidget { + const SPBrandButton({ + required this.child, + required this.onPressed, + super.key, + }); + + SPBrandButton.text({ + required final String title, + required this.onPressed, + super.key, + }) : child = Text(title); + + final Widget child; + final VoidCallback onPressed; + + @override + Widget build(final BuildContext context) => FilledButton( + // TODO(misterfourtytwo): move button styles to theme configuration + style: const ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size.fromHeight(48)), + ), + onPressed: onPressed, + child: child, + ); +} diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 1861bd0b..d1c00a24 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; class ProgressBar extends StatefulWidget { const ProgressBar({ @@ -21,41 +20,6 @@ class _ProgressBarState extends State { Widget build(final BuildContext context) { final double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); - final bool isDark = context.watch().state.isDarkModeOn; - final TextStyle style = - isDark ? progressTextStyleDark : progressTextStyleLight; - - final Iterable allSteps = widget.steps.asMap().map( - (final i, final step) { - final Container value = _stepTitle(index: i, style: style, step: step); - return MapEntry(i, value); - }, - ).values; - - final List odd = []; - final List even = []; - - int i = 0; - for (final Container step in allSteps) { - if (i.isEven) { - even.add(step); - } else { - odd.add(step); - } - i++; - } - - odd.insert( - 0, - const SizedBox( - width: 10, - ), - ); - odd.add( - const SizedBox( - width: 20, - ), - ); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -91,39 +55,4 @@ class _ProgressBarState extends State { ], ); } - - Container _stepTitle({ - required final int index, - TextStyle? style, - final String? step, - }) { - final bool isActive = index == widget.activeIndex; - - style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style; - return Container( - padding: const EdgeInsets.only(left: 10), - height: 20, - alignment: Alignment.center, - child: RichText( - textAlign: TextAlign.justify, - text: TextSpan( - style: progressTextStyleLight, - children: [ - TextSpan(text: '${index + 1}.', style: style), - TextSpan(text: step, style: style), - ], - ), - ), - ); - } } - -const TextStyle progressTextStyleLight = TextStyle( - fontSize: 11, - color: Colors.black, - height: 1.7, -); - -final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith( - color: Colors.white, -); diff --git a/lib/ui/pages/backups/snapshot_id_list_tile.dart b/lib/ui/pages/backups/snapshot_id_list_tile.dart index 802b90ee..22d35d34 100644 --- a/lib/ui/pages/backups/snapshot_id_list_tile.dart +++ b/lib/ui/pages/backups/snapshot_id_list_tile.dart @@ -17,7 +17,6 @@ class SnapshotIdListTile extends StatelessWidget { PlatformAdapter.setClipboard(snapshotId); getIt().showSnackBar( 'basis.copied_to_clipboard'.tr(), - behavior: SnackBarBehavior.floating, ); }, leading: Icon( diff --git a/lib/ui/pages/more/console.dart b/lib/ui/pages/more/console.dart index dc3801a7..94c06b4d 100644 --- a/lib/ui/pages/more/console.dart +++ b/lib/ui/pages/more/console.dart @@ -1,8 +1,7 @@ -import 'dart:collection'; - import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/ui/components/list_tiles/log_list_tile.dart'; @@ -16,25 +15,36 @@ class ConsolePage extends StatefulWidget { } class _ConsolePageState extends State { + bool paused = false; + @override void initState() { - getIt.get().addListener(update); - super.initState(); + + getIt().addListener(update); } @override void dispose() { getIt().removeListener(update); + super.dispose(); } - bool paused = false; - void update() { - if (!paused) { - setState(() => {}); - } + /// listener update could come at any time, like when widget is already + /// unmounted or during frame build, adding as postframe callback ensures + /// that element is marked for rebuild + WidgetsBinding.instance.addPostFrameCallback((final _) { + if (!paused && mounted) { + setState(() => {}); + } + }); + } + + void togglePause() { + paused ^= true; + setState(() {}); } @override @@ -51,7 +61,7 @@ class _ConsolePageState extends State { icon: Icon( paused ? Icons.play_arrow_outlined : Icons.pause_outlined, ), - onPressed: () => setState(() => paused = !paused), + onPressed: togglePause, ), ], ), @@ -69,12 +79,12 @@ class _ConsolePageState extends State { reverse: true, shrinkWrap: true, children: [ - const SizedBox(height: 20), - ...UnmodifiableListView( - messages - .map((final message) => LogListItem(message: message)) - .toList() - .reversed, + const Gap(20), + ...messages.reversed.map( + (final message) => LogListItem( + key: ValueKey(message), + message: message, + ), ), ], ); @@ -82,11 +92,10 @@ class _ConsolePageState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('console_page.waiting'.tr()), - const SizedBox( - height: 16, - ), + const Gap(16), const CircularProgressIndicator(), ], ); diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index c4075741..141c9463 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -1,8 +1,7 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/pages/onboarding/views/views.dart'; import 'package:selfprivacy/ui/router/router.dart'; @RoutePage() @@ -17,152 +16,35 @@ class _OnboardingPageState extends State { PageController pageController = PageController(); @override - void initState() { - super.initState(); + void dispose() { + pageController.dispose(); + super.dispose(); } + Future scrollTo(final int targetView) => pageController.animateToPage( + targetView, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutCubicEmphasized, + ); + @override - Widget build(final BuildContext context) => Scaffold( - body: PageView( + Widget build(final BuildContext context) => Material( + child: PageView( controller: pageController, children: [ - _withPadding(firstPage()), - _withPadding(secondPage()), - ], - ), - ); - - Widget _withPadding(final Widget child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - ), - child: child, - ); - - Widget firstPage() => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - children: [ - Expanded( - child: ListView( - children: [ - const SizedBox(height: 30), - Text( - 'onboarding.page1_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page1_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 32), - Center( - child: Image.asset( - _fileName( - context: context, - path: 'assets/images/onboarding', - fileExtention: 'png', - fileName: 'onboarding1', - ), - ), - ), - ], - ), + OnboardingFirstView( + onProceed: () => scrollTo(1), ), - BrandButton.rised( - onPressed: () { - pageController.animateToPage( - 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutCubicEmphasized, - ); - }, - text: 'basis.next'.tr(), - ), - const SizedBox(height: 30), - ], - ), - ); - - Widget secondPage() => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - children: [ - Expanded( - child: ListView( - children: [ - const SizedBox(height: 30), - Text( - 'onboarding.page2_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_server_provider_title'.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_server_provider_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_dns_provider_title'.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_dns_provider_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_backup_provider_title'.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Text( - 'onboarding.page2_backup_provider_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 16), - ], - ), - ), - BrandButton.rised( - onPressed: () { + OnboardingSecondView( + onProceed: () { context.read().turnOffOnboarding(); context.router.replaceAll([ const RootRoute(), const InitializingRoute(), ]); }, - text: 'basis.got_it'.tr(), ), - const SizedBox(height: 30), ], ), ); } - -String _fileName({ - required final BuildContext context, - required final String path, - required final String fileName, - required final String fileExtention, -}) { - final ThemeData theme = Theme.of(context); - final bool isDark = theme.brightness == Brightness.dark; - return '$path/$fileName${isDark ? '-dark' : '-light'}.$fileExtention'; -} diff --git a/lib/ui/pages/onboarding/views/onboarding_first_view.dart b/lib/ui/pages/onboarding/views/onboarding_first_view.dart new file mode 100644 index 00000000..fcef8ec5 --- /dev/null +++ b/lib/ui/pages/onboarding/views/onboarding_first_view.dart @@ -0,0 +1,50 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/ui/pages/onboarding/views/onboarding_view.dart'; + +class OnboardingFirstView extends StatelessWidget { + const OnboardingFirstView({ + required this.onProceed, + super.key, + }); + + final VoidCallback onProceed; + + String assetName({ + required final BuildContext context, + required final String path, + required final String fileName, + required final String fileExtension, + }) { + final String suffix = + Theme.of(context).brightness == Brightness.dark ? '-dark' : '-light'; + return '$path/$fileName$suffix.$fileExtension'; + } + + @override + Widget build(final BuildContext context) => OnboardingView( + onProceed: onProceed, + children: [ + Text( + 'onboarding.page1_title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const Gap(15), + Text( + 'onboarding.page1_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const Gap(30), + Image.asset( + assetName( + context: context, + path: 'assets/images/onboarding', + fileName: 'onboarding1', + fileExtension: 'png', + ), + fit: BoxFit.fitWidth, + ), + ], + ); +} diff --git a/lib/ui/pages/onboarding/views/onboarding_second_view.dart b/lib/ui/pages/onboarding/views/onboarding_second_view.dart new file mode 100644 index 00000000..8a996349 --- /dev/null +++ b/lib/ui/pages/onboarding/views/onboarding_second_view.dart @@ -0,0 +1,60 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/ui/pages/onboarding/views/onboarding_view.dart'; + +class OnboardingSecondView extends StatelessWidget { + const OnboardingSecondView({ + required this.onProceed, + super.key, + }); + + final VoidCallback onProceed; + + @override + Widget build(final BuildContext context) => OnboardingView( + buttonTitle: 'basis.got_it', + onProceed: onProceed, + children: [ + Text( + 'onboarding.page2_title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const Gap(16), + Text( + 'onboarding.page2_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const Gap(16), + Text( + 'onboarding.page2_server_provider_title'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const Gap(16), + Text( + 'onboarding.page2_server_provider_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const Gap(16), + Text( + 'onboarding.page2_dns_provider_title'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const Gap(16), + Text( + 'onboarding.page2_dns_provider_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const Gap(16), + Text( + 'onboarding.page2_backup_provider_title'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), + const Gap(16), + Text( + 'onboarding.page2_backup_provider_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ); +} diff --git a/lib/ui/pages/onboarding/views/onboarding_view.dart b/lib/ui/pages/onboarding/views/onboarding_view.dart new file mode 100644 index 00000000..6cf41b94 --- /dev/null +++ b/lib/ui/pages/onboarding/views/onboarding_view.dart @@ -0,0 +1,53 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/buttons/buttons.dart'; + +// base widget for onboarding view +class OnboardingView extends StatelessWidget { + const OnboardingView({ + required this.onProceed, + required this.children, + this.buttonTitle = 'basis.next', + super.key, + }); + + /// Proceed button title + final String buttonTitle; + + /// Proceed button callback + final VoidCallback onProceed; + + /// Current view content + final List children; + + @override + Widget build(final BuildContext context) => Scaffold( + body: Align( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ListView( + primary: true, + shrinkWrap: true, + padding: const EdgeInsets.all(15) + + const EdgeInsets.only(top: 15), + children: children, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15) + + const EdgeInsets.only(bottom: 30), + child: SPBrandButton.text( + title: buttonTitle.tr(), + onPressed: onProceed, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/lib/ui/pages/onboarding/views/views.dart b/lib/ui/pages/onboarding/views/views.dart new file mode 100644 index 00000000..b4ea6ffd --- /dev/null +++ b/lib/ui/pages/onboarding/views/views.dart @@ -0,0 +1,2 @@ +export 'onboarding_first_view.dart'; +export 'onboarding_second_view.dart'; diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart index b50f453b..6ae7607c 100644 --- a/lib/ui/pages/root_route.dart +++ b/lib/ui/pages/root_route.dart @@ -5,7 +5,6 @@ import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/layouts/root_scaffold_with_navigation.dart'; import 'package:selfprivacy/ui/router/root_destinations.dart'; - import 'package:selfprivacy/ui/router/router.dart'; @RoutePage() @@ -150,8 +149,3 @@ class MainScreenNavigationDrawer extends StatelessWidget { ); } } - -class ChangeTab { - ChangeTab(this.onPress); - final ValueChanged onPress; -} diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index 2fef5440..f94a353b 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -56,5 +56,3 @@ class _TempMessage extends StatelessWidget { ), ); } - -final DateFormat formatter = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/services/service_page.dart b/lib/ui/pages/services/service_page.dart index 37f9515c..b51edb31 100644 --- a/lib/ui/pages/services/service_page.dart +++ b/lib/ui/pages/services/service_page.dart @@ -170,83 +170,54 @@ class ServiceStatusCard extends StatelessWidget { @override Widget build(final BuildContext context) { + late IconData icon; + late String buttonTitle; + switch (status) { case ServiceStatus.active: - return FilledCard( - child: ListTile( - leading: const Icon( - Icons.check_circle_outline, - size: 24, - ), - title: Text('service_page.status.active'.tr()), - ), - ); + icon = Icons.check_circle_outline; + buttonTitle = 'service_page.status.active'; + break; + case ServiceStatus.inactive: - return FilledCard( - tertiary: true, - child: ListTile( - leading: const Icon( - Icons.stop_circle_outlined, - size: 24, - ), - title: Text('service_page.status.inactive'.tr()), - ), - ); + icon = Icons.stop_circle_outlined; + buttonTitle = 'service_page.status.inactive'; + break; + case ServiceStatus.failed: - return FilledCard( - error: true, - child: ListTile( - leading: const Icon( - Icons.error_outline, - size: 24, - ), - title: Text('service_page.status.failed'.tr()), - ), - ); + icon = Icons.error_outline; + buttonTitle = 'service_page.status.failed'; + break; + case ServiceStatus.off: - return FilledCard( - tertiary: true, - child: ListTile( - leading: const Icon( - Icons.power_settings_new, - size: 24, - ), - title: Text('service_page.status.off'.tr()), - ), - ); + icon = Icons.power_settings_new; + buttonTitle = 'service_page.status.off'; + break; + case ServiceStatus.activating: - return FilledCard( - tertiary: true, - child: ListTile( - leading: const Icon( - Icons.restart_alt_outlined, - size: 24, - ), - title: Text('service_page.status.activating'.tr()), - ), - ); + icon = Icons.restart_alt_outlined; + buttonTitle = 'service_page.status.activating'; + break; + case ServiceStatus.deactivating: - return FilledCard( - tertiary: true, - child: ListTile( - leading: const Icon( - Icons.restart_alt_outlined, - size: 24, - ), - title: Text('service_page.status.deactivating'.tr()), - ), - ); + icon = Icons.restart_alt_outlined; + buttonTitle = 'service_page.status.deactivating'; + break; + case ServiceStatus.reloading: - return FilledCard( - tertiary: true, - child: ListTile( - leading: const Icon( - Icons.restart_alt_outlined, - size: 24, - ), - title: Text('service_page.status.reloading'.tr()), - ), - ); + icon = Icons.restart_alt_outlined; + buttonTitle = 'service_page.status.reloading'; } + + return FilledCard( + tertiary: true, + child: ListTile( + leading: Icon( + icon, + size: 24, + ), + title: Text(buttonTitle.tr()), + ), + ); } } diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 2315bdb1..d35efbbd 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -89,7 +89,6 @@ class NewUserPage extends StatelessWidget { PlatformAdapter.setClipboard(currentPassword); getIt().showSnackBar( 'basis.copied_to_clipboard'.tr(), - behavior: SnackBarBehavior.floating, ); }, ), diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 9523ccf4..88727c7e 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -153,7 +153,6 @@ class _UserLogins extends StatelessWidget { PlatformAdapter.setClipboard(email); getIt().showSnackBar( 'basis.copied_to_clipboard'.tr(), - behavior: SnackBarBehavior.floating, ); }, title: email, diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart index 10f7419d..d7aec724 100644 --- a/lib/utils/ui_helpers.dart +++ b/lib/utils/ui_helpers.dart @@ -7,13 +7,13 @@ class UiHelpers { static String getDomainName(final ServerInstallationState config) => config.isDomainSelected ? config.serverDomain!.domainName : 'example.com'; + static final _formatter = NumberFormat()..minimumFractionDigits = 0; + static String formatWithPrecision( final double value, { final int fraction = 2, }) { - final NumberFormat formatter = NumberFormat(); - formatter.minimumFractionDigits = 0; - formatter.maximumFractionDigits = fraction; - return formatter.format(value); + _formatter.maximumFractionDigits = fraction; + return _formatter.format(value); } } diff --git a/pubspec.lock b/pubspec.lock index e1874330..a3ebff18 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -525,14 +525,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" get_it: dependency: "direct main" description: name: get_it - sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3 + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 url: "https://pub.dev" source: hosted - version: "7.6.4" + version: "7.6.7" glob: dependency: transitive description: @@ -673,10 +681,10 @@ packages: dependency: "direct main" description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c1641a6..32e0b41c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: flutter_markdown: ^0.6.18+2 flutter_secure_storage: ^9.0.0 flutter_svg: ^2.0.9 + gap: ^3.0.1 get_it: ^7.6.4 gql: ^1.0.0 graphql: ^5.1.3