home_widget.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:flutter/widgets.dart';
  7. import 'package:flutter_gen/gen_l10n/app_localizations.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:move_to_background/move_to_background.dart';
  10. import 'package:photo_manager/photo_manager.dart';
  11. import 'package:photos/core/configuration.dart';
  12. import 'package:photos/core/event_bus.dart';
  13. import 'package:photos/db/files_db.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/local_photos_updated_event.dart';
  17. import 'package:photos/events/permission_granted_event.dart';
  18. import 'package:photos/events/subscription_purchased_event.dart';
  19. import 'package:photos/events/sync_status_update_event.dart';
  20. import 'package:photos/events/tab_changed_event.dart';
  21. import 'package:photos/events/trigger_logout_event.dart';
  22. import 'package:photos/events/user_logged_out_event.dart';
  23. import 'package:photos/models/selected_files.dart';
  24. import 'package:photos/services/local_sync_service.dart';
  25. import 'package:photos/services/update_service.dart';
  26. import 'package:photos/services/user_service.dart';
  27. import 'package:photos/ui/app_update_dialog.dart';
  28. import 'package:photos/ui/backup_folder_selection_page.dart';
  29. import 'package:photos/ui/collections_gallery_widget.dart';
  30. import 'package:photos/ui/common_elements.dart';
  31. import 'package:photos/ui/create_collection_page.dart';
  32. import 'package:photos/ui/extents_page_view.dart';
  33. import 'package:photos/ui/gallery.dart';
  34. import 'package:photos/ui/gallery_app_bar_widget.dart';
  35. import 'package:photos/ui/gallery_footer_widget.dart';
  36. import 'package:photos/ui/grant_permissions_widget.dart';
  37. import 'package:photos/ui/landing_page_widget.dart';
  38. import 'package:photos/ui/loading_photos_widget.dart';
  39. import 'package:photos/ui/memories_widget.dart';
  40. import 'package:photos/ui/nav_bar.dart';
  41. import 'package:photos/ui/settings_button.dart';
  42. import 'package:photos/ui/shared_collections_gallery.dart';
  43. import 'package:photos/ui/sync_indicator.dart';
  44. import 'package:photos/utils/dialog_util.dart';
  45. import 'package:photos/utils/navigation_util.dart';
  46. import 'package:receive_sharing_intent/receive_sharing_intent.dart';
  47. import 'package:uni_links/uni_links.dart';
  48. class HomeWidget extends StatefulWidget {
  49. const HomeWidget({Key key}) : super(key: key);
  50. @override
  51. State<StatefulWidget> createState() => _HomeWidgetState();
  52. }
  53. class _HomeWidgetState extends State<HomeWidget> {
  54. static const _deviceFolderGalleryWidget = CollectionsGalleryWidget();
  55. static const _sharedCollectionGallery = SharedCollectionGallery();
  56. static const _headerWidget = HeaderWidget();
  57. final _logger = Logger("HomeWidgetState");
  58. final _selectedFiles = SelectedFiles();
  59. final _settingsButton = SettingsButton();
  60. final PageController _pageController = PageController();
  61. int _selectedTabIndex = 0;
  62. Widget _headerWidgetWithSettingsButton;
  63. // for receiving media files
  64. StreamSubscription _intentDataStreamSubscription;
  65. List<SharedMediaFile> _sharedFiles;
  66. StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
  67. StreamSubscription<SubscriptionPurchasedEvent> _subscriptionPurchaseEvent;
  68. StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
  69. StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
  70. StreamSubscription<PermissionGrantedEvent> _permissionGrantedEvent;
  71. StreamSubscription<SyncStatusUpdate> _firstImportEvent;
  72. StreamSubscription<BackupFoldersUpdatedEvent> _backupFoldersUpdatedEvent;
  73. StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
  74. @override
  75. void initState() {
  76. _logger.info("Building initstate");
  77. _headerWidgetWithSettingsButton = Container(
  78. margin: const EdgeInsets.only(top: 12),
  79. child: Stack(
  80. children: [
  81. _headerWidget,
  82. _settingsButton,
  83. ],
  84. ),
  85. );
  86. _tabChangedEventSubscription =
  87. Bus.instance.on<TabChangedEvent>().listen((event) {
  88. if (event.source != TabChangedEventSource.tab_bar) {
  89. setState(() {
  90. _selectedTabIndex = event.selectedIndex;
  91. });
  92. }
  93. if (event.source != TabChangedEventSource.page_view) {
  94. _pageController.animateToPage(
  95. event.selectedIndex,
  96. duration: Duration(milliseconds: 150),
  97. curve: Curves.easeIn,
  98. );
  99. }
  100. });
  101. _subscriptionPurchaseEvent =
  102. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  103. setState(() {});
  104. });
  105. _accountConfiguredEvent =
  106. Bus.instance.on<AccountConfiguredEvent>().listen((event) {
  107. setState(() {});
  108. });
  109. _triggerLogoutEvent =
  110. Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
  111. AlertDialog alert = AlertDialog(
  112. title: Text(AppLocalizations.of(context).auth_session_expired),
  113. content: Text(AppLocalizations.of(context).auth_login_again),
  114. actions: [
  115. TextButton(
  116. child: Text(
  117. AppLocalizations.of(context).ok,
  118. style: TextStyle(
  119. color: Theme.of(context).buttonColor,
  120. ),
  121. ),
  122. onPressed: () async {
  123. Navigator.of(context, rootNavigator: true).pop('dialog');
  124. final dialog = createProgressDialog(context, AppLocalizations.of(context).auth_logging_out);
  125. await dialog.show();
  126. await Configuration.instance.logout();
  127. await dialog.hide();
  128. },
  129. ),
  130. ],
  131. );
  132. showDialog(
  133. context: context,
  134. builder: (BuildContext context) {
  135. return alert;
  136. },
  137. );
  138. });
  139. _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
  140. setState(() {});
  141. });
  142. _permissionGrantedEvent =
  143. Bus.instance.on<PermissionGrantedEvent>().listen((event) async {
  144. if (mounted) {
  145. setState(() {});
  146. }
  147. });
  148. _firstImportEvent =
  149. Bus.instance.on<SyncStatusUpdate>().listen((event) async {
  150. if (mounted &&
  151. event.status == SyncStatus.completed_first_gallery_import) {
  152. setState(() {});
  153. }
  154. });
  155. _backupFoldersUpdatedEvent =
  156. Bus.instance.on<BackupFoldersUpdatedEvent>().listen((event) async {
  157. if (mounted) {
  158. setState(() {});
  159. }
  160. });
  161. _initDeepLinks();
  162. UpdateService.instance.shouldUpdate().then((shouldUpdate) {
  163. if (shouldUpdate) {
  164. Future.delayed(Duration.zero, () {
  165. showDialog(
  166. context: context,
  167. builder: (BuildContext context) {
  168. return AppUpdateDialog(
  169. UpdateService.instance.getLatestVersionInfo());
  170. },
  171. barrierColor: Colors.black.withOpacity(0.85),
  172. );
  173. });
  174. }
  175. });
  176. // For sharing images coming from outside the app while the app is in the memory
  177. _initMediaShareSubscription();
  178. super.initState();
  179. }
  180. @override
  181. void dispose() {
  182. _tabChangedEventSubscription.cancel();
  183. _subscriptionPurchaseEvent.cancel();
  184. _triggerLogoutEvent.cancel();
  185. _loggedOutEvent.cancel();
  186. _permissionGrantedEvent.cancel();
  187. _firstImportEvent.cancel();
  188. _backupFoldersUpdatedEvent.cancel();
  189. _accountConfiguredEvent.cancel();
  190. super.dispose();
  191. }
  192. void _initMediaShareSubscription() {
  193. _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream()
  194. .listen((List<SharedMediaFile> value) {
  195. setState(() {
  196. _sharedFiles = value;
  197. });
  198. }, onError: (err) {
  199. _logger.severe("getIntentDataStream error: $err");
  200. });
  201. // For sharing images coming from outside the app while the app is closed
  202. ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
  203. setState(() {
  204. _sharedFiles = value;
  205. });
  206. });
  207. }
  208. @override
  209. Widget build(BuildContext context) {
  210. _logger.info("Building home_Widget");
  211. return WillPopScope(
  212. child: Scaffold(
  213. appBar: PreferredSize(
  214. preferredSize: Size.fromHeight(0),
  215. child: Container(),
  216. ),
  217. body: _getBody(),
  218. ),
  219. onWillPop: () async {
  220. if (_selectedTabIndex == 0) {
  221. if (Platform.isAndroid) {
  222. MoveToBackground.moveTaskToBack();
  223. return false;
  224. } else {
  225. return true;
  226. }
  227. } else {
  228. Bus.instance
  229. .fire(TabChangedEvent(0, TabChangedEventSource.back_button));
  230. return false;
  231. }
  232. },
  233. );
  234. }
  235. Widget _getBody() {
  236. if (!Configuration.instance.hasConfiguredAccount()) {
  237. return LandingPageWidget();
  238. }
  239. if (!LocalSyncService.instance.hasGrantedPermissions()) {
  240. return GrantPermissionsWidget();
  241. }
  242. if (!LocalSyncService.instance.hasCompletedFirstImport()) {
  243. return LoadingPhotosWidget();
  244. }
  245. if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
  246. ReceiveSharingIntent.reset();
  247. return CreateCollectionPage(null, _sharedFiles);
  248. }
  249. return Stack(
  250. children: [
  251. ExtentsPageView(
  252. children: [
  253. (Configuration.instance.getPathsToBackUp().isEmpty &&
  254. !LocalSyncService.instance.hasGrantedLimitedPermissions())
  255. ? _getBackupFolderSelectionHook()
  256. : _getMainGalleryWidget(),
  257. _deviceFolderGalleryWidget,
  258. _sharedCollectionGallery,
  259. ],
  260. onPageChanged: (page) {
  261. Bus.instance.fire(TabChangedEvent(
  262. page,
  263. TabChangedEventSource.page_view,
  264. ));
  265. },
  266. physics: NeverScrollableScrollPhysics(),
  267. controller: _pageController,
  268. ),
  269. Align(
  270. alignment: Alignment.bottomCenter,
  271. child: _buildBottomNavigationBar(),
  272. ),
  273. ],
  274. );
  275. }
  276. Future<bool> _initDeepLinks() async {
  277. // Platform messages may fail, so we use a try/catch PlatformException.
  278. try {
  279. String initialLink = await getInitialLink();
  280. // Parse the link and warn the user, if it is not correct,
  281. // but keep in mind it could be `null`.
  282. if (initialLink != null) {
  283. _logger.info("Initial link received: " + initialLink);
  284. _getCredentials(context, initialLink);
  285. return true;
  286. } else {
  287. _logger.info("No initial link received.");
  288. }
  289. } on PlatformException {
  290. // Handle exception by warning the user their action did not succeed
  291. // return?
  292. _logger.severe("PlatformException thrown while getting initial link");
  293. }
  294. // Attach a listener to the stream
  295. linkStream.listen((String link) {
  296. _logger.info("Link received: " + link);
  297. _getCredentials(context, link);
  298. }, onError: (err) {
  299. _logger.severe(err);
  300. });
  301. return false;
  302. }
  303. void _getCredentials(BuildContext context, String link) {
  304. if (Configuration.instance.hasConfiguredAccount()) {
  305. return;
  306. }
  307. final ott = Uri.parse(link).queryParameters["ott"];
  308. UserService.instance.verifyEmail(context, ott);
  309. }
  310. Widget _getMainGalleryWidget() {
  311. Widget header;
  312. if (_selectedFiles.files.isEmpty) {
  313. header = _headerWidgetWithSettingsButton;
  314. } else {
  315. header = _headerWidget;
  316. }
  317. final gallery = Gallery(
  318. asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
  319. final importantPaths = Configuration.instance.getPathsToBackUp();
  320. final ownerID = Configuration.instance.getUserID();
  321. if (importantPaths.isNotEmpty) {
  322. return FilesDB.instance.getImportantFiles(
  323. creationStartTime, creationEndTime, ownerID, importantPaths.toList(),
  324. limit: limit, asc: asc);
  325. } else {
  326. if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
  327. return FilesDB.instance.getAllLocalAndUploadedFiles(
  328. creationStartTime, creationEndTime, ownerID,
  329. limit: limit, asc: asc);
  330. } else {
  331. return FilesDB.instance.getAllUploadedFiles(
  332. creationStartTime, creationEndTime, ownerID,
  333. limit: limit, asc: asc);
  334. }
  335. }
  336. },
  337. reloadEvent: Bus.instance.on<LocalPhotosUpdatedEvent>(),
  338. forceReloadEvent: Bus.instance.on<BackupFoldersUpdatedEvent>(),
  339. tagPrefix: "home_gallery",
  340. selectedFiles: _selectedFiles,
  341. header: header,
  342. footer: GalleryFooterWidget(),
  343. );
  344. return Stack(
  345. children: [
  346. Container(
  347. margin: const EdgeInsets.only(bottom: 80),
  348. child: gallery,
  349. ),
  350. HomePageAppBar(_selectedFiles),
  351. ],
  352. );
  353. }
  354. Widget _getBackupFolderSelectionHook() {
  355. return Column(
  356. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  357. children: [
  358. _headerWidgetWithSettingsButton,
  359. Image.asset(
  360. "assets/preserved.png",
  361. height: 160,
  362. ),
  363. Center(
  364. child: Hero(
  365. tag: "select_folders",
  366. child: Material(
  367. type: MaterialType.transparency,
  368. child: Container(
  369. width: double.infinity,
  370. height: 64,
  371. padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
  372. child: button(
  373. AppLocalizations.of(context).start_backup,
  374. fontSize: 16,
  375. lineHeight: 1.5,
  376. padding: EdgeInsets.only(bottom: 4),
  377. onPressed: () async {
  378. if (LocalSyncService.instance
  379. .hasGrantedLimitedPermissions()) {
  380. PhotoManager.presentLimited();
  381. } else {
  382. routeToPage(
  383. context,
  384. BackupFolderSelectionPage(
  385. shouldSelectAll: true,
  386. buttonText: AppLocalizations.of(context).start_backup,
  387. ),
  388. );
  389. }
  390. },
  391. ),
  392. ),
  393. ),
  394. ),
  395. ),
  396. Padding(padding: EdgeInsets.all(50)),
  397. ],
  398. );
  399. }
  400. Widget _buildBottomNavigationBar() {
  401. return Container(
  402. decoration: BoxDecoration(
  403. color: Colors.black.withOpacity(0.90),
  404. ),
  405. child: SafeArea(
  406. child: Padding(
  407. padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 8),
  408. child: GNav(
  409. rippleColor: Theme.of(context).buttonColor.withOpacity(0.20),
  410. hoverColor: Theme.of(context).buttonColor.withOpacity(0.20),
  411. gap: 8,
  412. activeColor: Theme.of(context).buttonColor.withOpacity(0.75),
  413. iconSize: 24,
  414. padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
  415. duration: Duration(milliseconds: 400),
  416. tabMargin: EdgeInsets.only(left: 8, right: 8),
  417. tabBackgroundColor: Color.fromRGBO(15, 25, 25, 0.7),
  418. haptic: false,
  419. tabs: [
  420. GButton(
  421. icon: Icons.photo_library_outlined,
  422. text: 'photos',
  423. onPressed: () {
  424. _onTabChange(0); // To take care of occasional missing events
  425. },
  426. ),
  427. GButton(
  428. icon: Icons.folder_special_outlined,
  429. text: 'albums',
  430. onPressed: () {
  431. _onTabChange(1); // To take care of occasional missing events
  432. },
  433. ),
  434. GButton(
  435. icon: Icons.folder_shared_outlined,
  436. text: 'shared',
  437. onPressed: () {
  438. _onTabChange(2); // To take care of occasional missing events
  439. },
  440. ),
  441. ],
  442. selectedIndex: _selectedTabIndex,
  443. onTabChange: _onTabChange,
  444. ),
  445. ),
  446. ),
  447. );
  448. }
  449. void _onTabChange(int index) {
  450. Bus.instance.fire(TabChangedEvent(
  451. index,
  452. TabChangedEventSource.tab_bar,
  453. ));
  454. }
  455. }
  456. class HomePageAppBar extends StatefulWidget {
  457. const HomePageAppBar(
  458. this.selectedFiles, {
  459. Key key,
  460. }) : super(key: key);
  461. final SelectedFiles selectedFiles;
  462. @override
  463. _HomePageAppBarState createState() => _HomePageAppBarState();
  464. }
  465. class _HomePageAppBarState extends State<HomePageAppBar> {
  466. @override
  467. void initState() {
  468. super.initState();
  469. widget.selectedFiles.addListener(() {
  470. setState(() {});
  471. });
  472. }
  473. @override
  474. Widget build(BuildContext context) {
  475. final appBar = SizedBox(
  476. height: 60,
  477. child: GalleryAppBarWidget(
  478. GalleryAppBarType.homepage,
  479. null,
  480. widget.selectedFiles,
  481. ),
  482. );
  483. if (widget.selectedFiles.files.isEmpty) {
  484. return IgnorePointer(child: appBar);
  485. } else {
  486. return appBar;
  487. }
  488. }
  489. }
  490. class HeaderWidget extends StatelessWidget {
  491. static const _memoriesWidget = MemoriesWidget();
  492. static const _syncIndicator = SyncIndicator();
  493. const HeaderWidget({
  494. Key key,
  495. }) : super(key: key);
  496. @override
  497. Widget build(BuildContext context) {
  498. Logger("Header").info("Building header widget");
  499. const list = [
  500. _syncIndicator,
  501. _memoriesWidget,
  502. ];
  503. return Column(
  504. children: list,
  505. crossAxisAlignment: CrossAxisAlignment.start,
  506. );
  507. }
  508. }