sync_indicator.dart 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:photos/core/errors.dart';
  4. import 'package:photos/core/event_bus.dart';
  5. import 'package:photos/events/sync_status_update_event.dart';
  6. import 'package:photos/services/sync_service.dart';
  7. import 'package:photos/ui/payment/subscription.dart';
  8. import 'package:photos/utils/email_util.dart';
  9. class SyncIndicator extends StatefulWidget {
  10. const SyncIndicator({Key key}) : super(key: key);
  11. @override
  12. _SyncIndicatorState createState() => _SyncIndicatorState();
  13. }
  14. class _SyncIndicatorState extends State<SyncIndicator> {
  15. static const kSleepDuration = Duration(milliseconds: 3000);
  16. SyncStatusUpdate _event;
  17. double _containerHeight = 48;
  18. StreamSubscription<SyncStatusUpdate> _subscription;
  19. static const _inProgressIcon = CircularProgressIndicator(
  20. strokeWidth: 2,
  21. valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(45, 194, 98, 1.0)),
  22. );
  23. @override
  24. void initState() {
  25. _subscription = Bus.instance.on<SyncStatusUpdate>().listen((event) {
  26. setState(() {
  27. _event = event;
  28. });
  29. });
  30. _event = SyncService.instance.getLastSyncStatusEvent();
  31. super.initState();
  32. }
  33. @override
  34. void dispose() {
  35. _subscription.cancel();
  36. super.dispose();
  37. }
  38. @override
  39. Widget build(BuildContext context) {
  40. bool isNotOutdatedEvent = _event != null &&
  41. (_event.status == SyncStatus.completed_backup ||
  42. _event.status == SyncStatus.completed_first_gallery_import) &&
  43. (DateTime.now().microsecondsSinceEpoch - _event.timestamp >
  44. kSleepDuration.inMicroseconds);
  45. if (_event == null || isNotOutdatedEvent) {
  46. return Container();
  47. }
  48. if (_event.status == SyncStatus.error) {
  49. return _getErrorWidget();
  50. }
  51. if (_event.status == SyncStatus.completed_first_gallery_import ||
  52. _event.status == SyncStatus.completed_backup) {
  53. Future.delayed(kSleepDuration, () {
  54. if (mounted) {
  55. setState(() {
  56. _containerHeight = 0;
  57. });
  58. }
  59. });
  60. } else {
  61. _containerHeight = 48;
  62. }
  63. final icon = _event.status == SyncStatus.completed_backup
  64. ? Icon(
  65. Icons.cloud_done_outlined,
  66. color: Theme.of(context).buttonColor,
  67. )
  68. : _inProgressIcon;
  69. return AnimatedContainer(
  70. duration: Duration(milliseconds: 300),
  71. height: _containerHeight,
  72. width: double.infinity,
  73. padding: EdgeInsets.all(8),
  74. alignment: Alignment.center,
  75. child: SingleChildScrollView(
  76. physics: NeverScrollableScrollPhysics(),
  77. child: Column(
  78. mainAxisAlignment: MainAxisAlignment.center,
  79. crossAxisAlignment: CrossAxisAlignment.center,
  80. children: [
  81. Row(
  82. mainAxisAlignment: MainAxisAlignment.center,
  83. crossAxisAlignment: CrossAxisAlignment.center,
  84. children: [
  85. Container(
  86. padding: EdgeInsets.all(2),
  87. width: 22,
  88. height: 22,
  89. child: icon,
  90. ),
  91. Padding(
  92. padding: const EdgeInsets.fromLTRB(12, 4, 0, 0),
  93. child: Text(_getRefreshingText()),
  94. ),
  95. ],
  96. ),
  97. Padding(padding: EdgeInsets.all(4)),
  98. Divider(),
  99. ],
  100. ),
  101. ),
  102. );
  103. }
  104. Widget _getErrorWidget() {
  105. if (_event.error is NoActiveSubscriptionError) {
  106. return Container(
  107. margin: EdgeInsets.only(top: 8),
  108. child: Column(
  109. children: [
  110. Row(
  111. mainAxisAlignment: MainAxisAlignment.center,
  112. crossAxisAlignment: CrossAxisAlignment.center,
  113. children: [
  114. Icon(
  115. Icons.error_outline,
  116. color: Theme.of(context).buttonColor,
  117. ),
  118. Padding(padding: EdgeInsets.all(4)),
  119. Text("Your subscription has expired"),
  120. ],
  121. ),
  122. Padding(padding: EdgeInsets.all(6)),
  123. Container(
  124. width: double.infinity,
  125. height: 64,
  126. padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
  127. child: OutlinedButton(
  128. child: Text("Subscribe"),
  129. onPressed: () {
  130. Navigator.of(context).push(
  131. MaterialPageRoute(
  132. builder: (BuildContext context) {
  133. return getSubscriptionPage();
  134. },
  135. ),
  136. );
  137. },
  138. ),
  139. ),
  140. Padding(padding: EdgeInsets.all(8)),
  141. ],
  142. ),
  143. );
  144. } else if (_event.error is StorageLimitExceededError) {
  145. return Container(
  146. margin: EdgeInsets.only(top: 8),
  147. child: Column(
  148. children: [
  149. Row(
  150. mainAxisAlignment: MainAxisAlignment.center,
  151. crossAxisAlignment: CrossAxisAlignment.center,
  152. children: [
  153. Icon(
  154. Icons.error_outline,
  155. color: Theme.of(context).buttonColor,
  156. ),
  157. Padding(padding: EdgeInsets.all(4)),
  158. Text("Storage limit exceeded"),
  159. ],
  160. ),
  161. Padding(padding: EdgeInsets.all(6)),
  162. Container(
  163. width: double.infinity,
  164. height: 64,
  165. padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
  166. child: OutlinedButton(
  167. child: Text("Upgrade"),
  168. onPressed: () {
  169. Navigator.of(context).push(
  170. MaterialPageRoute(
  171. builder: (BuildContext context) {
  172. return getSubscriptionPage();
  173. },
  174. ),
  175. );
  176. },
  177. ),
  178. ),
  179. Padding(padding: EdgeInsets.all(8)),
  180. ],
  181. ),
  182. );
  183. } else {
  184. return Center(
  185. child: Column(
  186. children: [
  187. Icon(
  188. Icons.error_outline,
  189. color: Colors.red[400],
  190. ),
  191. Padding(padding: EdgeInsets.all(4)),
  192. Text(
  193. "We could not backup your data\nwe will retry later",
  194. style: TextStyle(height: 1.4),
  195. textAlign: TextAlign.center,
  196. ),
  197. Padding(padding: EdgeInsets.all(8)),
  198. InkWell(
  199. child: OutlinedButton(
  200. style: OutlinedButton.styleFrom(
  201. shape: RoundedRectangleBorder(
  202. borderRadius: BorderRadius.circular(10),
  203. ),
  204. padding: EdgeInsets.fromLTRB(50, 16, 50, 16),
  205. side: BorderSide(
  206. width: 1,
  207. color: Colors.orange[300],
  208. ),
  209. ),
  210. child: Text(
  211. "Raise ticket",
  212. style: TextStyle(
  213. fontWeight: FontWeight.bold,
  214. fontSize: 14,
  215. color: Colors.orange[300],
  216. ),
  217. textAlign: TextAlign.center,
  218. ),
  219. onPressed: () {
  220. sendLogs(
  221. context,
  222. "Raise ticket",
  223. "support@ente.io",
  224. subject: "Backup failed",
  225. );
  226. },
  227. ),
  228. ),
  229. Padding(padding: EdgeInsets.all(16)),
  230. Divider(
  231. thickness: 2,
  232. height: 0,
  233. ),
  234. Padding(padding: EdgeInsets.all(12)),
  235. ],
  236. ),
  237. );
  238. }
  239. }
  240. String _getRefreshingText() {
  241. if (_event.status == SyncStatus.started_first_gallery_import ||
  242. _event.status == SyncStatus.completed_first_gallery_import) {
  243. return "Loading gallery...";
  244. }
  245. if (_event.status == SyncStatus.applying_remote_diff) {
  246. return "Syncing...";
  247. }
  248. if (_event.status == SyncStatus.preparing_for_upload) {
  249. return "Encrypting backup...";
  250. }
  251. if (_event.status == SyncStatus.in_progress) {
  252. return _event.completed.toString() +
  253. "/" +
  254. _event.total.toString() +
  255. " Memories preserved";
  256. }
  257. if (_event.status == SyncStatus.paused) {
  258. return _event.reason;
  259. }
  260. if (_event.status == SyncStatus.completed_backup) {
  261. if (_event.wasStopped) {
  262. return "Sync stopped";
  263. } else {
  264. return "All memories preserved";
  265. }
  266. }
  267. // _event.status == SyncStatus.error
  268. return _event.reason ?? "Upload failed";
  269. }
  270. }