home_widget.dart 18 KB

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