From 183ed3f1d70757f1accb3a6a6f3d57d90fe0fc4d Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 22 Aug 2024 17:35:55 +0530 Subject: [PATCH] [mob][photos] Refactor + UI tweaks on home app bar --- .../lib/ui/components/home_header_widget.dart | 100 ------ .../ui/home/error_warning_header_widget.dart | 102 ++++++ mobile/lib/ui/home/header_widget.dart | 4 +- mobile/lib/ui/home/home_app_bar_widget.dart | 198 +++++++++++- mobile/lib/ui/home/status_bar_widget.dart | 292 ------------------ 5 files changed, 296 insertions(+), 400 deletions(-) delete mode 100644 mobile/lib/ui/components/home_header_widget.dart create mode 100644 mobile/lib/ui/home/error_warning_header_widget.dart delete mode 100644 mobile/lib/ui/home/status_bar_widget.dart diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart deleted file mode 100644 index 7f2519a190..0000000000 --- a/mobile/lib/ui/components/home_header_widget.dart +++ /dev/null @@ -1,100 +0,0 @@ -import "dart:async"; -import "dart:io"; - -import 'package:flutter/material.dart'; -import "package:logging/logging.dart"; -import "package:photo_manager/photo_manager.dart"; -import "package:photos/generated/l10n.dart"; -import "package:photos/services/local_sync_service.dart"; -import 'package:photos/ui/components/buttons/icon_button_widget.dart'; -import "package:photos/ui/settings/backup/backup_folder_selection_page.dart"; -import "package:photos/utils/dialog_util.dart"; -import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/photo_manager_util.dart"; - -class HomeHeaderWidget extends StatefulWidget { - final Widget centerWidget; - const HomeHeaderWidget({required this.centerWidget, Key? key}) - : super(key: key); - - @override - State createState() => _HomeHeaderWidgetState(); -} - -class _HomeHeaderWidgetState extends State { - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButtonWidget( - iconButtonType: IconButtonType.primary, - icon: Icons.menu_outlined, - onTap: () { - Scaffold.of(context).openDrawer(); - }, - ), - ], - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: widget.centerWidget, - ), - IconButtonWidget( - icon: Icons.add_photo_alternate_outlined, - iconButtonType: IconButtonType.primary, - onTap: () async { - try { - final PermissionState state = - await requestPhotoMangerPermissions(); - await LocalSyncService.instance.onUpdatePermission(state); - } on Exception catch (e) { - Logger("HomeHeaderWidget").severe( - "Failed to request permission: ${e.toString()}", - e, - ); - } - if (!LocalSyncService.instance.hasGrantedFullPermission()) { - if (Platform.isAndroid) { - await PhotoManager.openSetting(); - } else { - final bool hasGrantedLimit = - LocalSyncService.instance.hasGrantedLimitedPermissions(); - // ignore: unawaited_futures - showChoiceActionSheet( - context, - title: S.of(context).preserveMore, - body: S.of(context).grantFullAccessPrompt, - firstButtonLabel: S.of(context).openSettings, - firstButtonOnTap: () async { - await PhotoManager.openSetting(); - }, - secondButtonLabel: hasGrantedLimit - ? S.of(context).selectMorePhotos - : S.of(context).cancel, - secondButtonOnTap: () async { - if (hasGrantedLimit) { - await PhotoManager.presentLimited(); - } - }, - ); - } - } else { - unawaited( - routeToPage( - context, - BackupFolderSelectionPage( - buttonText: S.of(context).backup, - ), - ), - ); - } - }, - ), - ], - ); - } -} diff --git a/mobile/lib/ui/home/error_warning_header_widget.dart b/mobile/lib/ui/home/error_warning_header_widget.dart new file mode 100644 index 0000000000..ffda3f4c42 --- /dev/null +++ b/mobile/lib/ui/home/error_warning_header_widget.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; +import 'package:photos/core/event_bus.dart'; +import 'package:photos/events/notification_event.dart'; +import 'package:photos/events/sync_status_update_event.dart'; +import "package:photos/generated/l10n.dart"; +import 'package:photos/services/user_remote_flag_service.dart'; +import "package:photos/theme/ente_theme.dart"; +import 'package:photos/ui/account/verify_recovery_page.dart'; +import 'package:photos/ui/components/notification_widget.dart'; +import 'package:photos/ui/home/header_error_widget.dart'; +import 'package:photos/utils/navigation_util.dart'; + +const double kContainerHeight = 36; + +class ErrorWarningHeader extends StatefulWidget { + const ErrorWarningHeader({Key? key}) : super(key: key); + + @override + State createState() => _ErrorWarningHeaderState(); +} + +class _ErrorWarningHeaderState extends State { + static final _logger = Logger("StatusBarWidget"); + + late StreamSubscription _subscription; + late StreamSubscription _notificationSubscription; + bool _showErrorBanner = false; + Error? _syncError; + + @override + void initState() { + super.initState(); + + _subscription = Bus.instance.on().listen((event) { + _logger.info("Received event " + event.status.toString()); + if (event.status == SyncStatus.error) { + setState(() { + _syncError = event.error; + _showErrorBanner = true; + }); + } else { + setState(() { + _syncError = null; + _showErrorBanner = false; + }); + } + }); + _notificationSubscription = + Bus.instance.on().listen((event) { + if (mounted) { + setState(() {}); + } + }); + } + + @override + void dispose() { + _subscription.cancel(); + _notificationSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _showErrorBanner + ? Divider( + height: 8, + color: getEnteColorScheme(context).strokeFaint, + ) + : const SizedBox.shrink(), + _showErrorBanner + ? HeaderErrorWidget(error: _syncError) + : const SizedBox.shrink(), + UserRemoteFlagService.instance.shouldShowRecoveryVerification() && + !_showErrorBanner + ? Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), + child: NotificationWidget( + startIcon: Icons.error_outline, + actionIcon: Icons.arrow_forward, + text: S.of(context).confirmYourRecoveryKey, + type: NotificationType.banner, + onTap: () async => { + await routeToPage( + context, + const VerifyRecoveryPage(), + forceCustomPageRoute: true, + ), + }, + ), + ) + : const SizedBox.shrink(), + ], + ); + } +} diff --git a/mobile/lib/ui/home/header_widget.dart b/mobile/lib/ui/home/header_widget.dart index a322382f14..e4cf9057a7 100644 --- a/mobile/lib/ui/home/header_widget.dart +++ b/mobile/lib/ui/home/header_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; +import "package:photos/ui/home/error_warning_header_widget.dart"; import "package:photos/ui/home/memories/memories_widget.dart"; -import 'package:photos/ui/home/status_bar_widget.dart'; class HeaderWidget extends StatelessWidget { const HeaderWidget({ @@ -14,7 +14,7 @@ class HeaderWidget extends StatelessWidget { return const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StatusBarWidget(), + ErrorWarningHeader(), MemoriesWidget(), ], ); diff --git a/mobile/lib/ui/home/home_app_bar_widget.dart b/mobile/lib/ui/home/home_app_bar_widget.dart index 5cda20e593..0f535de5fe 100644 --- a/mobile/lib/ui/home/home_app_bar_widget.dart +++ b/mobile/lib/ui/home/home_app_bar_widget.dart @@ -2,15 +2,20 @@ import "dart:async"; import "dart:io"; import "package:flutter/material.dart"; +import "package:intl/intl.dart"; import "package:logging/logging.dart"; import "package:photo_manager/photo_manager.dart"; import "package:photos/core/event_bus.dart"; +import "package:photos/ente_theme_data.dart"; import "package:photos/events/sync_status_update_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/services/local_sync_service.dart"; +import "package:photos/services/sync_service.dart"; +import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; +import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; -import "package:photos/ui/home/status_bar_widget.dart"; +import "package:photos/ui/home/error_warning_header_widget.dart"; import "package:photos/ui/settings/backup/backup_folder_selection_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; @@ -33,6 +38,7 @@ class _HomeAppBarWidgetState extends State { @override void initState() { super.initState(); + _subscription = Bus.instance.on().listen((event) { _logger.info("Received event " + event.status.toString()); @@ -72,11 +78,21 @@ class _HomeAppBarWidgetState extends State { AppBar build(BuildContext context) { return AppBar( centerTitle: true, - title: _showStatus - ? _showErrorBanner - ? const Text("ente", style: brandStyleMedium) - : const SyncStatusWidget() - : const Text("ente", style: brandStyleMedium), + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOutQuad, + switchOutCurve: Curves.easeInQuad, + child: _showStatus + ? AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOutQuad, + switchOutCurve: Curves.easeInQuad, + child: _showErrorBanner + ? const Text("ente", style: brandStyleMedium) + : const SyncStatusWidget(), + ) + : const Text("ente", style: brandStyleMedium), + ), actions: [ IconButtonWidget( icon: Icons.add_photo_alternate_outlined, @@ -133,3 +149,173 @@ class _HomeAppBarWidgetState extends State { ); } } + +class SyncStatusWidget extends StatefulWidget { + const SyncStatusWidget({Key? key}) : super(key: key); + + @override + State createState() => _SyncStatusWidgetState(); +} + +class _SyncStatusWidgetState extends State { + static const Duration kSleepDuration = Duration(milliseconds: 3000); + + SyncStatusUpdate? _event; + late StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + + _subscription = Bus.instance.on().listen((event) { + setState(() { + _event = event; + }); + }); + _event = SyncService.instance.getLastSyncStatusEvent(); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isNotOutdatedEvent = _event != null && + (_event!.status == SyncStatus.completedBackup || + _event!.status == SyncStatus.completedFirstGalleryImport) && + (DateTime.now().microsecondsSinceEpoch - _event!.timestamp > + kSleepDuration.inMicroseconds); + if (_event == null || + isNotOutdatedEvent || + //sync error cases are handled in StatusBarWidget + _event!.status == SyncStatus.error) { + return const SizedBox.shrink(); + } + if (_event!.status == SyncStatus.completedBackup) { + return const AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOutQuad, + switchOutCurve: Curves.easeInQuad, + child: SyncStatusCompletedWidget(), + ); + } + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeOutQuad, + switchOutCurve: Curves.easeInQuad, + child: RefreshIndicatorWidget(_event), + ); + } +} + +class RefreshIndicatorWidget extends StatelessWidget { + final SyncStatusUpdate? event; + + const RefreshIndicatorWidget(this.event, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: kContainerHeight, + alignment: Alignment.center, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + EnteLoadingWidget( + color: getEnteColorScheme(context).primary400, + ), + const SizedBox(width: 12), + Text( + _getRefreshingText(context), + style: getEnteTextTheme(context).small, + ), + ], + ), + ], + ), + ), + ); + } + + String _getRefreshingText(BuildContext context) { + if (event!.status == SyncStatus.startedFirstGalleryImport || + event!.status == SyncStatus.completedFirstGalleryImport) { + return S.of(context).loadingGallery; + } + if (event!.status == SyncStatus.applyingRemoteDiff) { + return S.of(context).syncing; + } + if (event!.status == SyncStatus.preparingForUpload) { + return S.of(context).encryptingBackup; + } + if (event!.status == SyncStatus.inProgress) { + final format = NumberFormat(); + return S.of(context).syncProgress( + format.format(event!.completed!), + format.format(event!.total!), + ); + } + if (event!.status == SyncStatus.paused) { + return event!.reason; + } + if (event!.status == SyncStatus.error) { + return event!.reason; + } + if (event!.status == SyncStatus.completedBackup) { + if (event!.wasStopped) { + return S.of(context).syncStopped; + } + } + return S.of(context).allMemoriesPreserved; + } +} + +class SyncStatusCompletedWidget extends StatelessWidget { + const SyncStatusCompletedWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final colorScheme = getEnteColorScheme(context); + return Container( + color: colorScheme.backdropBase, + height: kContainerHeight, + child: Align( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.cloud_done_outlined, + color: Theme.of(context).colorScheme.greenAlternative, + size: 22, + ), + Padding( + padding: const EdgeInsets.only(left: 12), + child: Text( + S.of(context).allMemoriesPreserved, + style: getEnteTextTheme(context).small, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/ui/home/status_bar_widget.dart b/mobile/lib/ui/home/status_bar_widget.dart deleted file mode 100644 index 3815e4cf3a..0000000000 --- a/mobile/lib/ui/home/status_bar_widget.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import "package:intl/intl.dart"; -import "package:logging/logging.dart"; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/ente_theme_data.dart'; -import 'package:photos/events/notification_event.dart'; -import 'package:photos/events/sync_status_update_event.dart'; -import "package:photos/generated/l10n.dart"; -import 'package:photos/services/sync_service.dart'; -import 'package:photos/services/user_remote_flag_service.dart'; -import "package:photos/theme/ente_theme.dart"; -import 'package:photos/theme/text_style.dart'; -import 'package:photos/ui/account/verify_recovery_page.dart'; -import 'package:photos/ui/components/home_header_widget.dart'; -import 'package:photos/ui/components/notification_widget.dart'; -import 'package:photos/ui/home/header_error_widget.dart'; -import 'package:photos/utils/navigation_util.dart'; - -const double kContainerHeight = 36; - -class StatusBarWidget extends StatefulWidget { - const StatusBarWidget({Key? key}) : super(key: key); - - @override - State createState() => _StatusBarWidgetState(); -} - -class _StatusBarWidgetState extends State { - static final _logger = Logger("StatusBarWidget"); - - late StreamSubscription _subscription; - late StreamSubscription _notificationSubscription; - bool _showStatus = false; - bool _showErrorBanner = false; - Error? _syncError; - - @override - void initState() { - super.initState(); - - _subscription = Bus.instance.on().listen((event) { - _logger.info("Received event " + event.status.toString()); - if (event.status == SyncStatus.error) { - setState(() { - _syncError = event.error; - _showErrorBanner = true; - }); - } else { - setState(() { - _syncError = null; - _showErrorBanner = false; - }); - } - if (event.status == SyncStatus.completedFirstGalleryImport || - event.status == SyncStatus.completedBackup) { - Future.delayed(const Duration(milliseconds: 2000), () { - if (mounted) { - setState(() { - _showStatus = false; - }); - } - }); - } else { - setState(() { - _showStatus = true; - }); - } - }); - _notificationSubscription = - Bus.instance.on().listen((event) { - if (mounted) { - setState(() {}); - } - }); - } - - @override - void dispose() { - _subscription.cancel(); - _notificationSubscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - HomeHeaderWidget( - centerWidget: _showStatus - ? _showErrorBanner - ? const Text("ente", style: brandStyleMedium) - : const SyncStatusWidget() - : const Text("ente", style: brandStyleMedium), - ), - _showErrorBanner - ? Divider( - height: 8, - color: getEnteColorScheme(context).strokeFaint, - ) - : const SizedBox.shrink(), - _showErrorBanner - ? HeaderErrorWidget(error: _syncError) - : const SizedBox.shrink(), - UserRemoteFlagService.instance.shouldShowRecoveryVerification() && - !_showErrorBanner - ? Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - child: NotificationWidget( - startIcon: Icons.error_outline, - actionIcon: Icons.arrow_forward, - text: S.of(context).confirmYourRecoveryKey, - type: NotificationType.banner, - onTap: () async => { - await routeToPage( - context, - const VerifyRecoveryPage(), - forceCustomPageRoute: true, - ), - }, - ), - ) - : const SizedBox.shrink(), - ], - ); - } -} - -class SyncStatusWidget extends StatefulWidget { - const SyncStatusWidget({Key? key}) : super(key: key); - - @override - State createState() => _SyncStatusWidgetState(); -} - -class _SyncStatusWidgetState extends State { - static const Duration kSleepDuration = Duration(milliseconds: 3000); - - SyncStatusUpdate? _event; - late StreamSubscription _subscription; - - @override - void initState() { - super.initState(); - - _subscription = Bus.instance.on().listen((event) { - setState(() { - _event = event; - }); - }); - _event = SyncService.instance.getLastSyncStatusEvent(); - } - - @override - void dispose() { - _subscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final bool isNotOutdatedEvent = _event != null && - (_event!.status == SyncStatus.completedBackup || - _event!.status == SyncStatus.completedFirstGalleryImport) && - (DateTime.now().microsecondsSinceEpoch - _event!.timestamp > - kSleepDuration.inMicroseconds); - if (_event == null || - isNotOutdatedEvent || - //sync error cases are handled in StatusBarWidget - _event!.status == SyncStatus.error) { - return const SizedBox.shrink(); - } - if (_event!.status == SyncStatus.completedBackup) { - return const SyncStatusCompletedWidget(); - } - return RefreshIndicatorWidget(_event); - } -} - -class RefreshIndicatorWidget extends StatelessWidget { - static const _inProgressIcon = CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Color.fromRGBO(45, 194, 98, 1.0)), - ); - - final SyncStatusUpdate? event; - - const RefreshIndicatorWidget(this.event, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - height: kContainerHeight, - alignment: Alignment.center, - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(2), - width: 22, - height: 22, - child: _inProgressIcon, - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 4, 0, 0), - child: Text(_getRefreshingText(context)), - ), - ], - ), - ], - ), - ), - ); - } - - String _getRefreshingText(BuildContext context) { - if (event!.status == SyncStatus.startedFirstGalleryImport || - event!.status == SyncStatus.completedFirstGalleryImport) { - return S.of(context).loadingGallery; - } - if (event!.status == SyncStatus.applyingRemoteDiff) { - return S.of(context).syncing; - } - if (event!.status == SyncStatus.preparingForUpload) { - return S.of(context).encryptingBackup; - } - if (event!.status == SyncStatus.inProgress) { - final format = NumberFormat(); - return S.of(context).syncProgress( - format.format(event!.completed!), - format.format(event!.total!), - ); - } - if (event!.status == SyncStatus.paused) { - return event!.reason; - } - if (event!.status == SyncStatus.error) { - return event!.reason; - } - if (event!.status == SyncStatus.completedBackup) { - if (event!.wasStopped) { - return S.of(context).syncStopped; - } - } - return S.of(context).allMemoriesPreserved; - } -} - -class SyncStatusCompletedWidget extends StatelessWidget { - const SyncStatusCompletedWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).colorScheme.defaultBackgroundColor, - height: kContainerHeight, - child: Align( - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - Icons.cloud_done_outlined, - color: Theme.of(context).colorScheme.greenAlternative, - size: 22, - ), - Padding( - padding: const EdgeInsets.only(left: 12), - child: Text(S.of(context).allMemoriesPreserved), - ), - ], - ), - ], - ), - ), - ); - } -}