sync_indicator.dart 8.5 KB

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