shared_collections_gallery.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // @dart=2.9
  2. import 'dart:async';
  3. import 'dart:math';
  4. import 'package:flutter/material.dart';
  5. import 'package:fluttertoast/fluttertoast.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/db/files_db.dart';
  10. import 'package:photos/events/collection_updated_event.dart';
  11. import 'package:photos/events/local_photos_updated_event.dart';
  12. import 'package:photos/events/tab_changed_event.dart';
  13. import 'package:photos/events/user_logged_out_event.dart';
  14. import 'package:photos/models/collection_items.dart';
  15. import 'package:photos/models/gallery_type.dart';
  16. import 'package:photos/services/collections_service.dart';
  17. import 'package:photos/theme/colors.dart';
  18. import 'package:photos/ui/collections/section_title.dart';
  19. import 'package:photos/ui/common/gradient_button.dart';
  20. import 'package:photos/ui/common/loading_widget.dart';
  21. import 'package:photos/ui/sharing/user_avator_widget.dart';
  22. import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
  23. import 'package:photos/ui/viewer/gallery/collection_page.dart';
  24. import 'package:photos/utils/navigation_util.dart';
  25. import 'package:photos/utils/share_util.dart';
  26. import 'package:photos/utils/toast_util.dart';
  27. class SharedCollectionGallery extends StatefulWidget {
  28. const SharedCollectionGallery({Key key}) : super(key: key);
  29. @override
  30. State<SharedCollectionGallery> createState() =>
  31. _SharedCollectionGalleryState();
  32. }
  33. class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
  34. with AutomaticKeepAliveClientMixin {
  35. final Logger _logger = Logger("SharedCollectionGallery");
  36. StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
  37. StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
  38. StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
  39. @override
  40. void initState() {
  41. _localFilesSubscription =
  42. Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
  43. debugPrint("SetState Shared Collections on ${event.reason}");
  44. setState(() {});
  45. });
  46. _collectionUpdatesSubscription =
  47. Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
  48. debugPrint("SetState Shared Collections on ${event.reason}");
  49. setState(() {});
  50. });
  51. _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
  52. setState(() {});
  53. });
  54. super.initState();
  55. }
  56. @override
  57. Widget build(BuildContext context) {
  58. super.build(context);
  59. return FutureBuilder<SharedCollections>(
  60. future:
  61. Future.value(CollectionsService.instance.getLatestCollectionFiles())
  62. .then((files) async {
  63. final List<CollectionWithThumbnail> outgoing = [];
  64. final List<CollectionWithThumbnail> incoming = [];
  65. for (final file in files) {
  66. final c =
  67. CollectionsService.instance.getCollectionByID(file.collectionID);
  68. if (c.owner.id == Configuration.instance.getUserID()) {
  69. if (c.sharees.isNotEmpty ||
  70. c.publicURLs.isNotEmpty ||
  71. c.isSharedFilesCollection()) {
  72. outgoing.add(
  73. CollectionWithThumbnail(
  74. c,
  75. file,
  76. ),
  77. );
  78. }
  79. } else {
  80. incoming.add(
  81. CollectionWithThumbnail(
  82. c,
  83. file,
  84. ),
  85. );
  86. }
  87. }
  88. outgoing.sort((first, second) {
  89. if (second.collection.isSharedFilesCollection() ==
  90. first.collection.isSharedFilesCollection()) {
  91. return second.collection.updationTime
  92. .compareTo(first.collection.updationTime);
  93. } else {
  94. if (first.collection.isSharedFilesCollection()) {
  95. return 1;
  96. }
  97. return -1;
  98. }
  99. });
  100. incoming.sort((first, second) {
  101. return second.collection.updationTime
  102. .compareTo(first.collection.updationTime);
  103. });
  104. return SharedCollections(outgoing, incoming);
  105. }),
  106. builder: (context, snapshot) {
  107. if (snapshot.hasData) {
  108. return _getSharedCollectionsGallery(snapshot.data);
  109. } else if (snapshot.hasError) {
  110. _logger.shout(snapshot.error);
  111. return Center(child: Text(snapshot.error.toString()));
  112. } else {
  113. return const EnteLoadingWidget();
  114. }
  115. },
  116. );
  117. }
  118. Widget _getSharedCollectionsGallery(SharedCollections collections) {
  119. const double horizontalPaddingOfGridRow = 16;
  120. const double crossAxisSpacingOfGrid = 9;
  121. final Size size = MediaQuery.of(context).size;
  122. final int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
  123. final double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
  124. (albumsCountInOneRow - 1) * crossAxisSpacingOfGrid;
  125. final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
  126. (totalWhiteSpaceOfRow / albumsCountInOneRow);
  127. return SingleChildScrollView(
  128. child: Container(
  129. margin: const EdgeInsets.only(bottom: 50),
  130. child: Column(
  131. children: [
  132. const SizedBox(height: 12),
  133. const SectionTitle(title: "Shared with me"),
  134. const SizedBox(height: 12),
  135. collections.incoming.isNotEmpty
  136. ? Padding(
  137. padding: const EdgeInsets.symmetric(horizontal: 16),
  138. child: GridView.builder(
  139. shrinkWrap: true,
  140. physics: const NeverScrollableScrollPhysics(),
  141. itemBuilder: (context, index) {
  142. return IncomingCollectionItem(
  143. collections.incoming[index],
  144. );
  145. },
  146. itemCount: collections.incoming.length,
  147. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  148. crossAxisCount: albumsCountInOneRow,
  149. mainAxisSpacing: 12,
  150. crossAxisSpacing: crossAxisSpacingOfGrid,
  151. childAspectRatio:
  152. sideOfThumbnail / (sideOfThumbnail + 24),
  153. ), //24 is height of album title
  154. ),
  155. )
  156. : _getIncomingCollectionEmptyState(),
  157. const SectionTitle(title: "Shared by me"),
  158. const SizedBox(height: 12),
  159. collections.outgoing.isNotEmpty
  160. ? ListView.builder(
  161. shrinkWrap: true,
  162. padding: const EdgeInsets.only(bottom: 12),
  163. physics: const NeverScrollableScrollPhysics(),
  164. itemBuilder: (context, index) {
  165. return OutgoingCollectionItem(
  166. collections.outgoing[index],
  167. );
  168. },
  169. itemCount: collections.outgoing.length,
  170. )
  171. : _getOutgoingCollectionEmptyState(),
  172. const SizedBox(height: 32),
  173. ],
  174. ),
  175. ),
  176. );
  177. }
  178. Widget _getIncomingCollectionEmptyState() {
  179. return SizedBox(
  180. height: 220,
  181. child: Column(
  182. mainAxisAlignment: MainAxisAlignment.center,
  183. children: [
  184. Text(
  185. "Ask your loved ones to share",
  186. style: Theme.of(context).textTheme.caption,
  187. ),
  188. const Padding(padding: EdgeInsets.only(top: 14)),
  189. SizedBox(
  190. width: 200,
  191. height: 50,
  192. child: GradientButton(
  193. onTap: () async {
  194. shareText("Check out https://ente.io");
  195. },
  196. iconData: Icons.outgoing_mail,
  197. text: "Invite",
  198. ),
  199. ),
  200. const SizedBox(height: 60),
  201. ],
  202. ),
  203. );
  204. }
  205. Widget _getOutgoingCollectionEmptyState() {
  206. return SizedBox(
  207. height: 200,
  208. child: Column(
  209. mainAxisAlignment: MainAxisAlignment.center,
  210. children: [
  211. Text(
  212. "Share your first album",
  213. style: Theme.of(context).textTheme.caption,
  214. ),
  215. const Padding(padding: EdgeInsets.only(top: 14)),
  216. SizedBox(
  217. width: 200,
  218. height: 50,
  219. child: GradientButton(
  220. onTap: () async {
  221. await showToast(
  222. context,
  223. "Open an album and tap the share button on the top right to share.",
  224. toastLength: Toast.LENGTH_LONG,
  225. );
  226. Bus.instance.fire(
  227. TabChangedEvent(1, TabChangedEventSource.collectionsPage),
  228. );
  229. },
  230. iconData: Icons.person_add,
  231. text: "Share",
  232. ),
  233. ),
  234. const SizedBox(height: 60),
  235. ],
  236. ),
  237. );
  238. }
  239. @override
  240. void dispose() {
  241. _localFilesSubscription.cancel();
  242. _collectionUpdatesSubscription.cancel();
  243. _loggedOutEvent.cancel();
  244. super.dispose();
  245. }
  246. @override
  247. bool get wantKeepAlive => true;
  248. }
  249. class OutgoingCollectionItem extends StatelessWidget {
  250. final CollectionWithThumbnail c;
  251. const OutgoingCollectionItem(
  252. this.c, {
  253. Key key,
  254. }) : super(key: key);
  255. @override
  256. Widget build(BuildContext context) {
  257. final sharees = <String>[];
  258. for (int index = 0; index < c.collection.sharees.length; index++) {
  259. final sharee = c.collection.sharees[index];
  260. final name =
  261. (sharee.name?.isNotEmpty ?? false) ? sharee.name : sharee.email;
  262. if (index < 2) {
  263. sharees.add(name);
  264. } else {
  265. final remaining = c.collection.sharees.length - index;
  266. if (remaining == 1) {
  267. // If it's the last sharee
  268. sharees.add(name);
  269. } else {
  270. sharees.add(
  271. "and " +
  272. remaining.toString() +
  273. " other" +
  274. (remaining > 1 ? "s" : ""),
  275. );
  276. }
  277. break;
  278. }
  279. }
  280. return GestureDetector(
  281. behavior: HitTestBehavior.opaque,
  282. child: Container(
  283. margin: const EdgeInsets.fromLTRB(16, 12, 16, 12),
  284. child: Row(
  285. children: <Widget>[
  286. ClipRRect(
  287. borderRadius: BorderRadius.circular(1),
  288. child: SizedBox(
  289. height: 60,
  290. width: 60,
  291. child: Hero(
  292. tag: "outgoing_collection" + c.thumbnail.tag,
  293. child: ThumbnailWidget(
  294. c.thumbnail,
  295. key: Key("outgoing_collection" + c.thumbnail.tag),
  296. ),
  297. ),
  298. ),
  299. ),
  300. const Padding(padding: EdgeInsets.all(8)),
  301. Expanded(
  302. child: Column(
  303. crossAxisAlignment: CrossAxisAlignment.start,
  304. children: [
  305. Row(
  306. children: [
  307. Text(
  308. c.collection.name,
  309. style: const TextStyle(
  310. fontSize: 16,
  311. ),
  312. ),
  313. const Padding(padding: EdgeInsets.all(2)),
  314. c.collection.publicURLs.isEmpty
  315. ? Container()
  316. : (c.collection.publicURLs.first.isExpired
  317. ? const Icon(
  318. Icons.link,
  319. color: warning500,
  320. )
  321. : const Icon(Icons.link)),
  322. ],
  323. ),
  324. sharees.isEmpty
  325. ? Container()
  326. : Padding(
  327. padding: const EdgeInsets.fromLTRB(0, 4, 0, 0),
  328. child: Text(
  329. "Shared with " + sharees.join(", "),
  330. style: TextStyle(
  331. fontSize: 14,
  332. color: Theme.of(context).primaryColorLight,
  333. ),
  334. textAlign: TextAlign.left,
  335. overflow: TextOverflow.ellipsis,
  336. ),
  337. ),
  338. ],
  339. ),
  340. ),
  341. ],
  342. ),
  343. ),
  344. onTap: () {
  345. final page = CollectionPage(
  346. c,
  347. appBarType: GalleryType.ownedCollection,
  348. tagPrefix: "outgoing_collection",
  349. );
  350. routeToPage(context, page);
  351. },
  352. );
  353. }
  354. }
  355. class IncomingCollectionItem extends StatelessWidget {
  356. final CollectionWithThumbnail c;
  357. const IncomingCollectionItem(
  358. this.c, {
  359. Key key,
  360. }) : super(key: key);
  361. @override
  362. Widget build(BuildContext context) {
  363. const double horizontalPaddingOfGridRow = 16;
  364. const double crossAxisSpacingOfGrid = 9;
  365. final TextStyle albumTitleTextStyle =
  366. Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 14);
  367. final Size size = MediaQuery.of(context).size;
  368. final int albumsCountInOneRow = max(size.width ~/ 220.0, 2);
  369. final double totalWhiteSpaceOfRow = (horizontalPaddingOfGridRow * 2) +
  370. (albumsCountInOneRow - 1) * crossAxisSpacingOfGrid;
  371. final double sideOfThumbnail = (size.width / albumsCountInOneRow) -
  372. (totalWhiteSpaceOfRow / albumsCountInOneRow);
  373. return GestureDetector(
  374. child: Column(
  375. crossAxisAlignment: CrossAxisAlignment.start,
  376. children: <Widget>[
  377. ClipRRect(
  378. borderRadius: BorderRadius.circular(1),
  379. child: SizedBox(
  380. height: sideOfThumbnail,
  381. width: sideOfThumbnail,
  382. child: Stack(
  383. children: [
  384. Hero(
  385. tag: "shared_collection" + c.thumbnail.tag,
  386. child: ThumbnailWidget(
  387. c.thumbnail,
  388. key: Key("shared_collection" + c.thumbnail.tag),
  389. ),
  390. ),
  391. Align(
  392. alignment: Alignment.bottomRight,
  393. child: Padding(
  394. padding: const EdgeInsets.only(right: 8.0, bottom: 8.0),
  395. child: UserAvatarWidget(
  396. c.collection.owner,
  397. thumbnailView: true,
  398. ),
  399. ),
  400. ),
  401. ],
  402. ),
  403. ),
  404. ),
  405. const SizedBox(height: 4),
  406. Row(
  407. children: [
  408. Container(
  409. constraints: BoxConstraints(maxWidth: sideOfThumbnail - 40),
  410. child: Text(
  411. c.collection.name,
  412. style: albumTitleTextStyle,
  413. overflow: TextOverflow.ellipsis,
  414. ),
  415. ),
  416. FutureBuilder<int>(
  417. future: FilesDB.instance.collectionFileCount(c.collection.id),
  418. builder: (context, snapshot) {
  419. if (snapshot.hasData && snapshot.data > 0) {
  420. return RichText(
  421. text: TextSpan(
  422. style: albumTitleTextStyle.copyWith(
  423. color: albumTitleTextStyle.color.withOpacity(0.5),
  424. ),
  425. children: [
  426. const TextSpan(text: " \u2022 "),
  427. TextSpan(text: snapshot.data.toString()),
  428. ],
  429. ),
  430. );
  431. } else {
  432. return Container();
  433. }
  434. },
  435. ),
  436. ],
  437. ),
  438. ],
  439. ),
  440. onTap: () {
  441. routeToPage(
  442. context,
  443. CollectionPage(
  444. c,
  445. appBarType: GalleryType.sharedCollection,
  446. tagPrefix: "shared_collection",
  447. ),
  448. );
  449. },
  450. );
  451. }
  452. }