home_widget.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/scheduler.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
  8. import 'package:move_to_background/move_to_background.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/core/event_bus.dart';
  11. import 'package:photos/ente_theme_data.dart';
  12. import 'package:photos/events/account_configured_event.dart';
  13. import 'package:photos/events/backup_folders_updated_event.dart';
  14. import 'package:photos/events/permission_granted_event.dart';
  15. import 'package:photos/events/subscription_purchased_event.dart';
  16. import 'package:photos/events/sync_status_update_event.dart';
  17. import 'package:photos/events/tab_changed_event.dart';
  18. import 'package:photos/events/trigger_logout_event.dart';
  19. import 'package:photos/events/user_logged_out_event.dart';
  20. import 'package:photos/models/selected_files.dart';
  21. import 'package:photos/services/collections_service.dart';
  22. import 'package:photos/services/local_sync_service.dart';
  23. import 'package:photos/services/update_service.dart';
  24. import 'package:photos/services/user_service.dart';
  25. import 'package:photos/states/user_details_state.dart';
  26. import 'package:photos/theme/colors.dart';
  27. import 'package:photos/theme/ente_theme.dart';
  28. import 'package:photos/ui/collections_gallery_widget.dart';
  29. import 'package:photos/ui/common/bottom_shadow.dart';
  30. import 'package:photos/ui/create_collection_sheet.dart';
  31. import 'package:photos/ui/extents_page_view.dart';
  32. import 'package:photos/ui/home/grant_permissions_widget.dart';
  33. import 'package:photos/ui/home/header_widget.dart';
  34. import 'package:photos/ui/home/home_bottom_nav_bar.dart';
  35. import 'package:photos/ui/home/home_gallery_widget.dart';
  36. import 'package:photos/ui/home/landing_page_widget.dart';
  37. import 'package:photos/ui/home/preserve_footer_widget.dart';
  38. import 'package:photos/ui/home/start_backup_hook_widget.dart';
  39. import 'package:photos/ui/loading_photos_widget.dart';
  40. import 'package:photos/ui/notification/update/change_log_page.dart';
  41. import 'package:photos/ui/settings/app_update_dialog.dart';
  42. import 'package:photos/ui/settings_page.dart';
  43. import 'package:photos/ui/shared_collections_gallery.dart';
  44. import 'package:photos/utils/dialog_util.dart';
  45. import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  46. import 'package:uni_links/uni_links.dart';
  47. class HomeWidget extends StatefulWidget {
  48. const HomeWidget({Key? key}) : super(key: key);
  49. @override
  50. State<StatefulWidget> createState() => _HomeWidgetState();
  51. }
  52. class _HomeWidgetState extends State<HomeWidget> {
  53. static const _deviceFolderGalleryWidget = CollectionsGalleryWidget();
  54. static const _sharedCollectionGallery = SharedCollectionGallery();
  55. static final _settingsPage = SettingsPage(
  56. emailNotifier: UserService.instance.emailValueNotifier,
  57. );
  58. static const _headerWidget = HeaderWidget();
  59. final _logger = Logger("HomeWidgetState");
  60. final _selectedFiles = SelectedFiles();
  61. final GlobalKey shareButtonKey = GlobalKey();
  62. final PageController _pageController = PageController();
  63. int _selectedTabIndex = 0;
  64. // for receiving media files
  65. // ignore: unused_field
  66. StreamSubscription? _intentDataStreamSubscription;
  67. List<SharedMediaFile>? _sharedFiles;
  68. bool _shouldRenderCreateCollectionSheet = false;
  69. late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
  70. late StreamSubscription<SubscriptionPurchasedEvent>
  71. _subscriptionPurchaseEvent;
  72. late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
  73. late StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
  74. late StreamSubscription<PermissionGrantedEvent> _permissionGrantedEvent;
  75. late StreamSubscription<SyncStatusUpdate> _firstImportEvent;
  76. late StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
  77. late StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
  78. @override
  79. void initState() {
  80. _logger.info("Building initstate");
  81. _tabChangedEventSubscription =
  82. Bus.instance.on<TabChangedEvent>().listen((event) {
  83. if (event.source != TabChangedEventSource.pageView) {
  84. debugPrint(
  85. "TabChange going from $_selectedTabIndex to ${event.selectedIndex} souce: ${event.source}",
  86. );
  87. _selectedTabIndex = event.selectedIndex;
  88. // _pageController.jumpToPage(_selectedTabIndex);
  89. _pageController.animateToPage(
  90. event.selectedIndex,
  91. duration: const Duration(milliseconds: 100),
  92. curve: Curves.easeIn,
  93. );
  94. }
  95. });
  96. _subscriptionPurchaseEvent =
  97. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  98. setState(() {});
  99. });
  100. _accountConfiguredEvent =
  101. Bus.instance.on<AccountConfiguredEvent>().listen((event) {
  102. setState(() {});
  103. });
  104. _triggerLogoutEvent =
  105. Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
  106. await _autoLogoutAlert();
  107. });
  108. _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
  109. _logger.info('logged out, selectTab index to 0');
  110. _selectedTabIndex = 0;
  111. if (mounted) {
  112. setState(() {});
  113. }
  114. });
  115. _permissionGrantedEvent =
  116. Bus.instance.on<PermissionGrantedEvent>().listen((event) async {
  117. if (mounted) {
  118. setState(() {});
  119. }
  120. });
  121. _firstImportEvent =
  122. Bus.instance.on<SyncStatusUpdate>().listen((event) async {
  123. if (mounted && event.status == SyncStatus.completedFirstGalleryImport) {
  124. Duration delayInRefresh = const Duration(milliseconds: 0);
  125. // Loading page will redirect to BackupFolderSelectionPage.
  126. // To avoid showing folder hook in middle during routing,
  127. // delay state refresh for home page
  128. if (!LocalSyncService.instance.hasGrantedLimitedPermissions()) {
  129. delayInRefresh = const Duration(milliseconds: 250);
  130. }
  131. Future.delayed(
  132. delayInRefresh,
  133. () => {
  134. if (mounted)
  135. {
  136. setState(
  137. () {},
  138. )
  139. }
  140. },
  141. );
  142. }
  143. });
  144. _backupFoldersUpdatedEvent =
  145. Bus.instance.on<BackupFoldersUpdatedEvent>().listen((event) async {
  146. if (mounted) {
  147. setState(() {});
  148. }
  149. });
  150. _initDeepLinks();
  151. UpdateService.instance.shouldUpdate().then((shouldUpdate) {
  152. if (shouldUpdate) {
  153. Future.delayed(Duration.zero, () {
  154. showDialog(
  155. context: context,
  156. builder: (BuildContext context) {
  157. return AppUpdateDialog(
  158. UpdateService.instance.getLatestVersionInfo(),
  159. );
  160. },
  161. barrierColor: Colors.black.withOpacity(0.85),
  162. );
  163. });
  164. }
  165. });
  166. // For sharing images coming from outside the app while the app is in the memory
  167. _initMediaShareSubscription();
  168. WidgetsBinding.instance.addPostFrameCallback(
  169. (_) => Future.delayed(
  170. const Duration(seconds: 1),
  171. () => {
  172. if (mounted) {showChangeLog(context)}
  173. },
  174. ),
  175. );
  176. super.initState();
  177. }
  178. Future<void> _autoLogoutAlert() async {
  179. final AlertDialog alert = AlertDialog(
  180. title: const Text("Session expired"),
  181. content: const Text("Please login again"),
  182. actions: [
  183. TextButton(
  184. child: Text(
  185. "Ok",
  186. style: TextStyle(
  187. color: Theme.of(context).colorScheme.greenAlternative,
  188. ),
  189. ),
  190. onPressed: () async {
  191. Navigator.of(context, rootNavigator: true).pop('dialog');
  192. Navigator.of(context).popUntil((route) => route.isFirst);
  193. final dialog = createProgressDialog(context, "Logging out...");
  194. await dialog.show();
  195. await Configuration.instance.logout();
  196. await dialog.hide();
  197. },
  198. ),
  199. ],
  200. );
  201. await showDialog(
  202. context: context,
  203. builder: (BuildContext context) {
  204. return alert;
  205. },
  206. );
  207. }
  208. @override
  209. void dispose() {
  210. _tabChangedEventSubscription.cancel();
  211. _subscriptionPurchaseEvent.cancel();
  212. _triggerLogoutEvent.cancel();
  213. _loggedOutEvent.cancel();
  214. _permissionGrantedEvent.cancel();
  215. _firstImportEvent.cancel();
  216. _backupFoldersUpdatedEvent.cancel();
  217. _accountConfiguredEvent.cancel();
  218. _intentDataStreamSubscription?.cancel();
  219. super.dispose();
  220. }
  221. void _initMediaShareSubscription() {
  222. _intentDataStreamSubscription =
  223. ReceiveSharingIntent.getMediaStream().listen(
  224. (List<SharedMediaFile> value) {
  225. setState(() {
  226. _shouldRenderCreateCollectionSheet = true;
  227. _sharedFiles = value;
  228. });
  229. },
  230. onError: (err) {
  231. _logger.severe("getIntentDataStream error: $err");
  232. },
  233. );
  234. // For sharing images coming from outside the app while the app is closed
  235. ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
  236. setState(() {
  237. _sharedFiles = value;
  238. });
  239. });
  240. }
  241. @override
  242. Widget build(BuildContext context) {
  243. _logger.info("Building home_Widget with tab $_selectedTabIndex");
  244. bool isSettingsOpen = false;
  245. final enableDrawer = LocalSyncService.instance.hasCompletedFirstImport();
  246. return UserDetailsStateWidget(
  247. child: WillPopScope(
  248. child: Scaffold(
  249. drawerScrimColor: getEnteColorScheme(context).strokeFainter,
  250. drawerEnableOpenDragGesture: false,
  251. //using a hack instead of enabling this as enabling this will create other problems
  252. drawer: enableDrawer
  253. ? ConstrainedBox(
  254. constraints: const BoxConstraints(maxWidth: 428),
  255. child: Drawer(
  256. width: double.infinity,
  257. child: _settingsPage,
  258. ),
  259. )
  260. : null,
  261. onDrawerChanged: (isOpened) => isSettingsOpen = isOpened,
  262. body: SafeArea(
  263. bottom: false,
  264. child: Builder(
  265. builder: (context) {
  266. return _getBody(context);
  267. },
  268. ),
  269. ),
  270. resizeToAvoidBottomInset: false,
  271. ),
  272. onWillPop: () async {
  273. if (_selectedTabIndex == 0) {
  274. if (isSettingsOpen) {
  275. Navigator.pop(context);
  276. return false;
  277. }
  278. if (Platform.isAndroid) {
  279. MoveToBackground.moveTaskToBack();
  280. return false;
  281. } else {
  282. return true;
  283. }
  284. } else {
  285. Bus.instance
  286. .fire(TabChangedEvent(0, TabChangedEventSource.backButton));
  287. return false;
  288. }
  289. },
  290. ),
  291. );
  292. }
  293. Widget _getBody(BuildContext context) {
  294. if (!Configuration.instance.hasConfiguredAccount()) {
  295. _closeDrawerIfOpen(context);
  296. return const LandingPageWidget();
  297. }
  298. if (!LocalSyncService.instance.hasGrantedPermissions()) {
  299. return const GrantPermissionsWidget();
  300. }
  301. if (!LocalSyncService.instance.hasCompletedFirstImport()) {
  302. return const LoadingPhotosWidget();
  303. }
  304. if (_sharedFiles != null &&
  305. _sharedFiles!.isNotEmpty &&
  306. _shouldRenderCreateCollectionSheet) {
  307. //The gallery is getting rebuilt for some reason when the keyboard is up.
  308. //So to stop showing multiple CreateCollectionSheets, this flag
  309. //needs to be set to false the first time it is rendered.
  310. _shouldRenderCreateCollectionSheet = false;
  311. ReceiveSharingIntent.reset();
  312. Future.delayed(const Duration(milliseconds: 10), () {
  313. createCollectionSheet(
  314. null,
  315. _sharedFiles,
  316. context,
  317. actionType: CollectionActionType.addFiles,
  318. );
  319. });
  320. }
  321. final isBottomInsetPresent = MediaQuery.of(context).viewPadding.bottom != 0;
  322. final bool showBackupFolderHook =
  323. !Configuration.instance.hasSelectedAnyBackupFolder() &&
  324. !LocalSyncService.instance.hasGrantedLimitedPermissions() &&
  325. CollectionsService.instance.getActiveCollections().isEmpty;
  326. return Stack(
  327. children: [
  328. Builder(
  329. builder: (context) {
  330. return ExtentsPageView(
  331. onPageChanged: (page) {
  332. Bus.instance.fire(
  333. TabChangedEvent(
  334. page,
  335. TabChangedEventSource.pageView,
  336. ),
  337. );
  338. },
  339. controller: _pageController,
  340. openDrawer: Scaffold.of(context).openDrawer,
  341. physics: const BouncingScrollPhysics(),
  342. children: [
  343. showBackupFolderHook
  344. ? const StartBackupHookWidget(headerWidget: _headerWidget)
  345. : HomeGalleryWidget(
  346. header: _headerWidget,
  347. footer: const PreserveFooterWidget(),
  348. selectedFiles: _selectedFiles,
  349. ),
  350. _deviceFolderGalleryWidget,
  351. _sharedCollectionGallery,
  352. ],
  353. );
  354. },
  355. ),
  356. const Align(
  357. alignment: Alignment.bottomCenter,
  358. child: BottomShadowWidget(),
  359. ),
  360. Align(
  361. alignment: Alignment.bottomCenter,
  362. child: Padding(
  363. padding: EdgeInsets.only(bottom: isBottomInsetPresent ? 32 : 8),
  364. child: HomeBottomNavigationBar(
  365. _selectedFiles,
  366. selectedTabIndex: _selectedTabIndex,
  367. ),
  368. ),
  369. ),
  370. ],
  371. );
  372. }
  373. void _closeDrawerIfOpen(BuildContext context) {
  374. Scaffold.of(context).isDrawerOpen
  375. ? SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
  376. Scaffold.of(context).closeDrawer();
  377. })
  378. : null;
  379. }
  380. Future<bool> _initDeepLinks() async {
  381. // Platform messages may fail, so we use a try/catch PlatformException.
  382. try {
  383. final String? initialLink = await getInitialLink();
  384. // Parse the link and warn the user, if it is not correct,
  385. // but keep in mind it could be `null`.
  386. if (initialLink != null) {
  387. _logger.info("Initial link received: " + initialLink);
  388. _getCredentials(context, initialLink);
  389. return true;
  390. } else {
  391. _logger.info("No initial link received.");
  392. }
  393. } on PlatformException {
  394. // Handle exception by warning the user their action did not succeed
  395. // return?
  396. _logger.severe("PlatformException thrown while getting initial link");
  397. }
  398. // Attach a listener to the stream
  399. linkStream.listen(
  400. (String? link) {
  401. _logger.info("Link received: " + link!);
  402. _getCredentials(context, link);
  403. },
  404. onError: (err) {
  405. _logger.severe(err);
  406. },
  407. );
  408. return false;
  409. }
  410. void _getCredentials(BuildContext context, String? link) {
  411. if (Configuration.instance.hasConfiguredAccount()) {
  412. return;
  413. }
  414. final ott = Uri.parse(link!).queryParameters["ott"]!;
  415. UserService.instance.verifyEmail(context, ott);
  416. }
  417. showChangeLog(BuildContext context) async {
  418. final bool show = await UpdateService.instance.showChangeLog();
  419. if (!show || !Configuration.instance.isLoggedIn()) {
  420. return;
  421. }
  422. final colorScheme = getEnteColorScheme(context);
  423. await showBarModalBottomSheet(
  424. topControl: const SizedBox.shrink(),
  425. shape: const RoundedRectangleBorder(
  426. borderRadius: BorderRadius.only(
  427. topLeft: Radius.circular(5),
  428. topRight: Radius.circular(5),
  429. ),
  430. ),
  431. backgroundColor: colorScheme.backgroundElevated,
  432. enableDrag: false,
  433. barrierColor: backdropFaintDark,
  434. context: context,
  435. builder: (BuildContext context) {
  436. return Padding(
  437. padding:
  438. EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
  439. child: const ChangeLogPage(),
  440. );
  441. },
  442. );
  443. // Do not show change dialog again
  444. UpdateService.instance.hideChangeLog().ignore();
  445. }
  446. }