shared_collections_gallery.dart 16 KB

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