home_widget.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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_remote_flag_service.dart';
  25. import 'package:photos/services/user_service.dart';
  26. import 'package:photos/states/user_details_state.dart';
  27. import 'package:photos/theme/colors.dart';
  28. import 'package:photos/theme/ente_theme.dart';
  29. import 'package:photos/ui/collections_gallery_widget.dart';
  30. import 'package:photos/ui/common/bottom_shadow.dart';
  31. import 'package:photos/ui/create_collection_page.dart';
  32. import 'package:photos/ui/extents_page_view.dart';
  33. import 'package:photos/ui/home/grant_permissions_widget.dart';
  34. import 'package:photos/ui/home/header_widget.dart';
  35. import 'package:photos/ui/home/home_bottom_nav_bar.dart';
  36. import 'package:photos/ui/home/home_gallery_widget.dart';
  37. import 'package:photos/ui/home/landing_page_widget.dart';
  38. import 'package:photos/ui/home/preserve_footer_widget.dart';
  39. import 'package:photos/ui/home/start_backup_hook_widget.dart';
  40. import 'package:photos/ui/loading_photos_widget.dart';
  41. import 'package:photos/ui/notification/prompts/password_reminder.dart';
  42. import 'package:photos/ui/notification/update/change_log_page.dart';
  43. import 'package:photos/ui/settings/app_update_dialog.dart';
  44. import 'package:photos/ui/settings_page.dart';
  45. import 'package:photos/ui/shared_collections_gallery.dart';
  46. import 'package:photos/utils/dialog_util.dart';
  47. import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  48. import 'package:uni_links/uni_links.dart';
  49. class HomeWidget extends StatefulWidget {
  50. const HomeWidget({Key? key}) : super(key: key);
  51. @override
  52. State<StatefulWidget> createState() => _HomeWidgetState();
  53. }
  54. class _HomeWidgetState extends State<HomeWidget> {
  55. static const _deviceFolderGalleryWidget = CollectionsGalleryWidget();
  56. static const _sharedCollectionGallery = SharedCollectionGallery();
  57. static final _settingsPage = SettingsPage(
  58. emailNotifier: UserService.instance.emailValueNotifier,
  59. );
  60. static const _headerWidget = HeaderWidget();
  61. final _logger = Logger("HomeWidgetState");
  62. final _selectedFiles = SelectedFiles();
  63. final GlobalKey shareButtonKey = GlobalKey();
  64. final PageController _pageController = PageController();
  65. int _selectedTabIndex = 0;
  66. // for receiving media files
  67. // ignore: unused_field
  68. StreamSubscription? _intentDataStreamSubscription;
  69. List<SharedMediaFile>? _sharedFiles;
  70. late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
  71. late StreamSubscription<SubscriptionPurchasedEvent> _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. _sharedFiles = value;
  227. });
  228. },
  229. onError: (err) {
  230. _logger.severe("getIntentDataStream error: $err");
  231. },
  232. );
  233. // For sharing images coming from outside the app while the app is closed
  234. ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
  235. setState(() {
  236. _sharedFiles = value;
  237. });
  238. });
  239. }
  240. @override
  241. Widget build(BuildContext context) {
  242. _logger.info("Building home_Widget with tab $_selectedTabIndex");
  243. bool isSettingsOpen = false;
  244. final enableDrawer = LocalSyncService.instance.hasCompletedFirstImport();
  245. return UserDetailsStateWidget(
  246. child: WillPopScope(
  247. child: Scaffold(
  248. drawerScrimColor: getEnteColorScheme(context).strokeFainter,
  249. drawerEnableOpenDragGesture: false,
  250. //using a hack instead of enabling this as enabling this will create other problems
  251. drawer: enableDrawer
  252. ? ConstrainedBox(
  253. constraints: const BoxConstraints(maxWidth: 428),
  254. child: Drawer(
  255. width: double.infinity,
  256. child: _settingsPage,
  257. ),
  258. )
  259. : null,
  260. onDrawerChanged: (isOpened) => isSettingsOpen = isOpened,
  261. body: SafeArea(
  262. bottom: false,
  263. child: Builder(
  264. builder: (context) {
  265. return _getBody(context);
  266. },
  267. ),
  268. ),
  269. resizeToAvoidBottomInset: false,
  270. ),
  271. onWillPop: () async {
  272. if (_selectedTabIndex == 0) {
  273. if (isSettingsOpen) {
  274. Navigator.pop(context);
  275. return false;
  276. }
  277. if (Platform.isAndroid) {
  278. MoveToBackground.moveTaskToBack();
  279. return false;
  280. } else {
  281. return true;
  282. }
  283. } else {
  284. Bus.instance
  285. .fire(TabChangedEvent(0, TabChangedEventSource.backButton));
  286. return false;
  287. }
  288. },
  289. ),
  290. );
  291. }
  292. Widget _getBody(BuildContext context) {
  293. if (!Configuration.instance.hasConfiguredAccount()) {
  294. _closeDrawerIfOpen(context);
  295. return const LandingPageWidget();
  296. }
  297. if (!LocalSyncService.instance.hasGrantedPermissions()) {
  298. return const GrantPermissionsWidget();
  299. }
  300. if (!LocalSyncService.instance.hasCompletedFirstImport()) {
  301. return const LoadingPhotosWidget();
  302. }
  303. if (UserRemoteFlagService.instance.showPasswordReminder()) {
  304. return const PasswordReminder();
  305. }
  306. if (_sharedFiles != null && _sharedFiles!.isNotEmpty) {
  307. ReceiveSharingIntent.reset();
  308. return CreateCollectionPage(null, _sharedFiles);
  309. }
  310. final isBottomInsetPresent = MediaQuery.of(context).viewPadding.bottom != 0;
  311. final bool showBackupFolderHook =
  312. !Configuration.instance.hasSelectedAnyBackupFolder() &&
  313. !LocalSyncService.instance.hasGrantedLimitedPermissions() &&
  314. CollectionsService.instance.getActiveCollections().isEmpty;
  315. return Stack(
  316. children: [
  317. Builder(
  318. builder: (context) {
  319. return ExtentsPageView(
  320. onPageChanged: (page) {
  321. Bus.instance.fire(
  322. TabChangedEvent(
  323. page,
  324. TabChangedEventSource.pageView,
  325. ),
  326. );
  327. },
  328. controller: _pageController,
  329. openDrawer: Scaffold.of(context).openDrawer,
  330. physics: const BouncingScrollPhysics(),
  331. children: [
  332. showBackupFolderHook
  333. ? const StartBackupHookWidget(headerWidget: _headerWidget)
  334. : HomeGalleryWidget(
  335. header: _headerWidget,
  336. footer: const PreserveFooterWidget(),
  337. selectedFiles: _selectedFiles,
  338. ),
  339. _deviceFolderGalleryWidget,
  340. _sharedCollectionGallery,
  341. ],
  342. );
  343. },
  344. ),
  345. const Align(
  346. alignment: Alignment.bottomCenter,
  347. child: BottomShadowWidget(),
  348. ),
  349. Align(
  350. alignment: Alignment.bottomCenter,
  351. child: Padding(
  352. padding: EdgeInsets.only(bottom: isBottomInsetPresent ? 32 : 8),
  353. child: HomeBottomNavigationBar(
  354. _selectedFiles,
  355. selectedTabIndex: _selectedTabIndex,
  356. ),
  357. ),
  358. ),
  359. ],
  360. );
  361. }
  362. void _closeDrawerIfOpen(BuildContext context) {
  363. Scaffold.of(context).isDrawerOpen
  364. ? SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
  365. Scaffold.of(context).closeDrawer();
  366. })
  367. : null;
  368. }
  369. Future<bool> _initDeepLinks() async {
  370. // Platform messages may fail, so we use a try/catch PlatformException.
  371. try {
  372. final String? initialLink = await getInitialLink();
  373. // Parse the link and warn the user, if it is not correct,
  374. // but keep in mind it could be `null`.
  375. if (initialLink != null) {
  376. _logger.info("Initial link received: " + initialLink);
  377. _getCredentials(context, initialLink);
  378. return true;
  379. } else {
  380. _logger.info("No initial link received.");
  381. }
  382. } on PlatformException {
  383. // Handle exception by warning the user their action did not succeed
  384. // return?
  385. _logger.severe("PlatformException thrown while getting initial link");
  386. }
  387. // Attach a listener to the stream
  388. linkStream.listen(
  389. (String? link) {
  390. _logger.info("Link received: " + link!);
  391. _getCredentials(context, link);
  392. },
  393. onError: (err) {
  394. _logger.severe(err);
  395. },
  396. );
  397. return false;
  398. }
  399. void _getCredentials(BuildContext context, String? link) {
  400. if (Configuration.instance.hasConfiguredAccount()) {
  401. return;
  402. }
  403. final ott = Uri.parse(link!).queryParameters["ott"]!;
  404. UserService.instance.verifyEmail(context, ott);
  405. }
  406. showChangeLog(BuildContext context) async {
  407. final bool show = await UpdateService.instance.showChangeLog();
  408. if (!show || !Configuration.instance.isLoggedIn()) {
  409. return;
  410. }
  411. final colorScheme = getEnteColorScheme(context);
  412. await showBarModalBottomSheet(
  413. topControl: const SizedBox.shrink(),
  414. shape: const RoundedRectangleBorder(
  415. borderRadius: BorderRadius.only(
  416. topLeft: Radius.circular(5),
  417. topRight: Radius.circular(5),
  418. ),
  419. ),
  420. backgroundColor: colorScheme.backgroundElevated,
  421. enableDrag: false,
  422. barrierColor: backdropFaintDark,
  423. context: context,
  424. builder: (BuildContext context) {
  425. return Padding(
  426. padding:
  427. EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
  428. child: const ChangeLogPage(),
  429. );
  430. },
  431. );
  432. // Do not show change dialog again
  433. UpdateService.instance.hideChangeLog().ignore();
  434. }
  435. }