status_bar_widget.dart 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:photos/core/event_bus.dart';
  4. import 'package:photos/ente_theme_data.dart';
  5. import 'package:photos/events/notification_event.dart';
  6. import 'package:photos/events/sync_status_update_event.dart';
  7. import 'package:photos/services/sync_service.dart';
  8. import 'package:photos/services/user_remote_flag_service.dart';
  9. import 'package:photos/theme/text_style.dart';
  10. import 'package:photos/ui/account/verify_recovery_page.dart';
  11. import 'package:photos/ui/components/home_header_widget.dart';
  12. import 'package:photos/ui/components/notification_warning_widget.dart';
  13. import 'package:photos/ui/home/header_error_widget.dart';
  14. import 'package:photos/utils/navigation_util.dart';
  15. const double kContainerHeight = 36;
  16. class StatusBarWidget extends StatefulWidget {
  17. const StatusBarWidget({Key? key}) : super(key: key);
  18. @override
  19. State<StatusBarWidget> createState() => _StatusBarWidgetState();
  20. }
  21. class _StatusBarWidgetState extends State<StatusBarWidget> {
  22. late StreamSubscription<SyncStatusUpdate> _subscription;
  23. late StreamSubscription<NotificationEvent> _notificationSubscription;
  24. bool _showStatus = false;
  25. bool _showErrorBanner = false;
  26. Error? _syncError;
  27. @override
  28. void initState() {
  29. _subscription = Bus.instance.on<SyncStatusUpdate>().listen((event) {
  30. if (event.status == SyncStatus.error) {
  31. setState(() {
  32. _syncError = event.error;
  33. _showErrorBanner = true;
  34. });
  35. } else {
  36. setState(() {
  37. _syncError = null;
  38. _showErrorBanner = false;
  39. });
  40. }
  41. if (event.status == SyncStatus.completedFirstGalleryImport ||
  42. event.status == SyncStatus.completedBackup) {
  43. Future.delayed(const Duration(milliseconds: 2000), () {
  44. if (mounted) {
  45. setState(() {
  46. _showStatus = false;
  47. });
  48. }
  49. });
  50. } else {
  51. setState(() {
  52. _showStatus = true;
  53. });
  54. }
  55. });
  56. _notificationSubscription =
  57. Bus.instance.on<NotificationEvent>().listen((event) {
  58. if (mounted) {
  59. setState(() {});
  60. }
  61. });
  62. super.initState();
  63. }
  64. @override
  65. void dispose() {
  66. _subscription.cancel();
  67. _notificationSubscription.cancel();
  68. super.dispose();
  69. }
  70. @override
  71. Widget build(BuildContext context) {
  72. return Column(
  73. children: [
  74. HomeHeaderWidget(
  75. centerWidget: _showStatus
  76. ? _showErrorBanner
  77. ? const Text("ente", style: brandStyleMedium)
  78. : const SyncStatusWidget()
  79. : const Text("ente", style: brandStyleMedium),
  80. ),
  81. AnimatedOpacity(
  82. opacity: _showErrorBanner ? 1 : 0,
  83. duration: const Duration(milliseconds: 200),
  84. child: const Divider(
  85. height: 8,
  86. ),
  87. ),
  88. _showErrorBanner
  89. ? HeaderErrorWidget(error: _syncError)
  90. : const SizedBox.shrink(),
  91. UserRemoteFlagService.instance.shouldShowRecoveryVerification()
  92. ? NotificationWarningWidget(
  93. warningIcon: Icons.error_outline,
  94. actionIcon: Icons.arrow_forward,
  95. text: "Confirm your recovery key",
  96. onTap: () async => {
  97. await routeToPage(
  98. context,
  99. const VerifyRecoveryPage(),
  100. forceCustomPageRoute: true,
  101. )
  102. },
  103. )
  104. : const SizedBox.shrink()
  105. ],
  106. );
  107. }
  108. }
  109. class SyncStatusWidget extends StatefulWidget {
  110. const SyncStatusWidget({Key? key}) : super(key: key);
  111. @override
  112. State<SyncStatusWidget> createState() => _SyncStatusWidgetState();
  113. }
  114. class _SyncStatusWidgetState extends State<SyncStatusWidget> {
  115. static const Duration kSleepDuration = Duration(milliseconds: 3000);
  116. SyncStatusUpdate? _event;
  117. late StreamSubscription<SyncStatusUpdate> _subscription;
  118. @override
  119. void initState() {
  120. _subscription = Bus.instance.on<SyncStatusUpdate>().listen((event) {
  121. setState(() {
  122. _event = event;
  123. });
  124. });
  125. _event = SyncService.instance.getLastSyncStatusEvent();
  126. super.initState();
  127. }
  128. @override
  129. void dispose() {
  130. _subscription.cancel();
  131. super.dispose();
  132. }
  133. @override
  134. Widget build(BuildContext context) {
  135. final bool isNotOutdatedEvent = _event != null &&
  136. (_event!.status == SyncStatus.completedBackup ||
  137. _event!.status == SyncStatus.completedFirstGalleryImport) &&
  138. (DateTime.now().microsecondsSinceEpoch - _event!.timestamp >
  139. kSleepDuration.inMicroseconds);
  140. if (_event == null ||
  141. isNotOutdatedEvent ||
  142. //sync error cases are handled in StatusBarWidget
  143. _event!.status == SyncStatus.error) {
  144. return const SizedBox.shrink();
  145. }
  146. if (_event!.status == SyncStatus.completedBackup) {
  147. return const SyncStatusCompletedWidget();
  148. }
  149. return RefreshIndicatorWidget(_event);
  150. }
  151. }
  152. class RefreshIndicatorWidget extends StatelessWidget {
  153. static const _inProgressIcon = CircularProgressIndicator(
  154. strokeWidth: 2,
  155. valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(45, 194, 98, 1.0)),
  156. );
  157. final SyncStatusUpdate? event;
  158. const RefreshIndicatorWidget(this.event, {Key? key}) : super(key: key);
  159. @override
  160. Widget build(BuildContext context) {
  161. return Container(
  162. height: kContainerHeight,
  163. alignment: Alignment.center,
  164. child: SingleChildScrollView(
  165. physics: const NeverScrollableScrollPhysics(),
  166. child: Column(
  167. mainAxisAlignment: MainAxisAlignment.center,
  168. crossAxisAlignment: CrossAxisAlignment.center,
  169. children: [
  170. Row(
  171. mainAxisAlignment: MainAxisAlignment.center,
  172. crossAxisAlignment: CrossAxisAlignment.center,
  173. children: [
  174. Container(
  175. padding: const EdgeInsets.all(2),
  176. width: 22,
  177. height: 22,
  178. child: _inProgressIcon,
  179. ),
  180. Padding(
  181. padding: const EdgeInsets.fromLTRB(12, 4, 0, 0),
  182. child: Text(_getRefreshingText()),
  183. ),
  184. ],
  185. ),
  186. ],
  187. ),
  188. ),
  189. );
  190. }
  191. String _getRefreshingText() {
  192. if (event!.status == SyncStatus.startedFirstGalleryImport ||
  193. event!.status == SyncStatus.completedFirstGalleryImport) {
  194. return "Loading gallery...";
  195. }
  196. if (event!.status == SyncStatus.applyingRemoteDiff) {
  197. return "Syncing...";
  198. }
  199. if (event!.status == SyncStatus.preparingForUpload) {
  200. return "Encrypting backup...";
  201. }
  202. if (event!.status == SyncStatus.inProgress) {
  203. return event!.completed.toString() +
  204. "/" +
  205. event!.total.toString() +
  206. " memories preserved";
  207. }
  208. if (event!.status == SyncStatus.paused) {
  209. return event!.reason;
  210. }
  211. if (event!.status == SyncStatus.error) {
  212. return event!.reason;
  213. }
  214. if (event!.status == SyncStatus.completedBackup) {
  215. if (event!.wasStopped) {
  216. return "Sync stopped";
  217. }
  218. }
  219. return "All memories preserved";
  220. }
  221. }
  222. class BrandingWidget extends StatelessWidget {
  223. const BrandingWidget({Key? key}) : super(key: key);
  224. @override
  225. Widget build(BuildContext context) {
  226. return Row(
  227. children: [
  228. Container(
  229. height: kContainerHeight,
  230. padding: const EdgeInsets.only(left: 12, top: 4),
  231. child: const Align(
  232. alignment: Alignment.centerLeft,
  233. child: Text(
  234. "ente",
  235. style: TextStyle(
  236. fontWeight: FontWeight.bold,
  237. fontFamily: 'Montserrat',
  238. fontSize: 24,
  239. height: 1,
  240. ),
  241. ),
  242. ),
  243. ),
  244. ],
  245. );
  246. }
  247. }
  248. class SyncStatusCompletedWidget extends StatelessWidget {
  249. const SyncStatusCompletedWidget({Key? key}) : super(key: key);
  250. @override
  251. Widget build(BuildContext context) {
  252. return Container(
  253. color: Theme.of(context).colorScheme.defaultBackgroundColor,
  254. height: kContainerHeight,
  255. child: Align(
  256. alignment: Alignment.center,
  257. child: Column(
  258. mainAxisAlignment: MainAxisAlignment.center,
  259. crossAxisAlignment: CrossAxisAlignment.center,
  260. children: [
  261. Row(
  262. mainAxisAlignment: MainAxisAlignment.center,
  263. crossAxisAlignment: CrossAxisAlignment.center,
  264. children: [
  265. Icon(
  266. Icons.cloud_done_outlined,
  267. color: Theme.of(context).colorScheme.greenAlternative,
  268. size: 22,
  269. ),
  270. const Padding(
  271. padding: EdgeInsets.only(left: 12),
  272. child: Text("All memories preserved"),
  273. ),
  274. ],
  275. ),
  276. ],
  277. ),
  278. ),
  279. );
  280. }
  281. }