diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart new file mode 100644 index 0000000000..7f2519a190 --- /dev/null +++ b/mobile/lib/ui/components/home_header_widget.dart @@ -0,0 +1,100 @@ +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 deleted file mode 100644 index ffda3f4c42..0000000000 --- a/mobile/lib/ui/home/error_warning_header_widget.dart +++ /dev/null @@ -1,102 +0,0 @@ -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 e4cf9057a7..a322382f14 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: [ - ErrorWarningHeader(), + StatusBarWidget(), MemoriesWidget(), ], ); diff --git a/mobile/lib/ui/home/home_app_bar_widget.dart b/mobile/lib/ui/home/home_app_bar_widget.dart index f61ac2818a..5cda20e593 100644 --- a/mobile/lib/ui/home/home_app_bar_widget.dart +++ b/mobile/lib/ui/home/home_app_bar_widget.dart @@ -2,20 +2,15 @@ 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/error_warning_header_widget.dart"; +import "package:photos/ui/home/status_bar_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"; @@ -38,7 +33,6 @@ class _HomeAppBarWidgetState extends State { @override void initState() { super.initState(); - _subscription = Bus.instance.on().listen((event) { _logger.info("Received event " + event.status.toString()); @@ -78,21 +72,11 @@ class _HomeAppBarWidgetState extends State { AppBar build(BuildContext context) { return AppBar( centerTitle: true, - 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), - ), + title: _showStatus + ? _showErrorBanner + ? const Text("ente", style: brandStyleMedium) + : const SyncStatusWidget() + : const Text("ente", style: brandStyleMedium), actions: [ IconButtonWidget( icon: Icons.add_photo_alternate_outlined, @@ -149,173 +133,3 @@ 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: 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 new file mode 100644 index 0000000000..3815e4cf3a --- /dev/null +++ b/mobile/lib/ui/home/status_bar_widget.dart @@ -0,0 +1,292 @@ +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), + ), + ], + ), + ], + ), + ), + ); + } +}