shared_collections_gallery.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/widgets.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/events/collection_updated_event.dart';
  9. import 'package:photos/events/local_photos_updated_event.dart';
  10. import 'package:photos/events/tab_changed_event.dart';
  11. import 'package:photos/events/user_logged_out_event.dart';
  12. import 'package:photos/models/collection_items.dart';
  13. import 'package:photos/services/collections_service.dart';
  14. import 'package:photos/ui/collection_page.dart';
  15. import 'package:photos/ui/collections_gallery_widget.dart';
  16. import 'package:photos/ui/loading_widget.dart';
  17. import 'package:photos/ui/thumbnail_widget.dart';
  18. import 'package:photos/utils/navigation_util.dart';
  19. import 'package:photos/utils/share_util.dart';
  20. import 'package:photos/utils/toast_util.dart';
  21. class SharedCollectionGallery extends StatefulWidget {
  22. const SharedCollectionGallery({Key key}) : super(key: key);
  23. @override
  24. _SharedCollectionGalleryState createState() =>
  25. _SharedCollectionGalleryState();
  26. }
  27. class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
  28. with AutomaticKeepAliveClientMixin {
  29. Logger _logger = Logger("SharedCollectionGallery");
  30. StreamSubscription<LocalPhotosUpdatedEvent> _localFilesSubscription;
  31. StreamSubscription<CollectionUpdatedEvent> _collectionUpdatesSubscription;
  32. StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
  33. @override
  34. void initState() {
  35. _localFilesSubscription =
  36. Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
  37. _logger.info("Files updated");
  38. setState(() {});
  39. });
  40. _collectionUpdatesSubscription =
  41. Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
  42. setState(() {});
  43. });
  44. _loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
  45. setState(() {});
  46. });
  47. super.initState();
  48. }
  49. @override
  50. Widget build(BuildContext context) {
  51. super.build(context);
  52. return FutureBuilder<SharedCollections>(
  53. future:
  54. Future.value(CollectionsService.instance.getLatestCollectionFiles())
  55. .then((files) async {
  56. final List<CollectionWithThumbnail> outgoing = [];
  57. final List<CollectionWithThumbnail> incoming = [];
  58. for (final file in files) {
  59. final c =
  60. CollectionsService.instance.getCollectionByID(file.collectionID);
  61. if (c.owner.id == Configuration.instance.getUserID()) {
  62. if (c.sharees.isNotEmpty) {
  63. outgoing.add(
  64. CollectionWithThumbnail(
  65. c,
  66. file,
  67. ),
  68. );
  69. }
  70. } else {
  71. incoming.add(
  72. CollectionWithThumbnail(
  73. c,
  74. file,
  75. ),
  76. );
  77. }
  78. }
  79. outgoing.sort((first, second) {
  80. return second.collection.updationTime
  81. .compareTo(first.collection.updationTime);
  82. });
  83. incoming.sort((first, second) {
  84. return second.collection.updationTime
  85. .compareTo(first.collection.updationTime);
  86. });
  87. return SharedCollections(outgoing, incoming);
  88. }),
  89. builder: (context, snapshot) {
  90. if (snapshot.hasData) {
  91. return _getSharedCollectionsGallery(snapshot.data);
  92. } else if (snapshot.hasError) {
  93. _logger.shout(snapshot.error);
  94. return Center(child: Text(snapshot.error.toString()));
  95. } else {
  96. return loadWidget;
  97. }
  98. },
  99. );
  100. }
  101. Widget _getSharedCollectionsGallery(SharedCollections collections) {
  102. return SingleChildScrollView(
  103. child: Container(
  104. margin: const EdgeInsets.only(bottom: 50),
  105. child: Column(
  106. children: [
  107. Padding(padding: EdgeInsets.all(6)),
  108. SectionTitle("incoming"),
  109. Padding(padding: EdgeInsets.all(16)),
  110. collections.incoming.isNotEmpty
  111. ? GridView.builder(
  112. shrinkWrap: true,
  113. physics: NeverScrollableScrollPhysics(),
  114. itemBuilder: (context, index) {
  115. return IncomingCollectionItem(
  116. collections.incoming[index]);
  117. },
  118. itemCount: collections.incoming.length,
  119. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  120. crossAxisCount: 2,
  121. ),
  122. )
  123. : _getIncomingCollectionEmptyState(),
  124. Padding(padding: EdgeInsets.all(16)),
  125. Divider(height: 0),
  126. Padding(padding: EdgeInsets.all(14)),
  127. SectionTitle("outgoing"),
  128. Padding(padding: EdgeInsets.all(16)),
  129. collections.outgoing.isNotEmpty
  130. ? Padding(
  131. padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
  132. child: ListView.builder(
  133. shrinkWrap: true,
  134. padding: EdgeInsets.only(bottom: 12),
  135. physics: NeverScrollableScrollPhysics(),
  136. itemBuilder: (context, index) {
  137. return OutgoingCollectionItem(
  138. collections.outgoing[index]);
  139. },
  140. itemCount: collections.outgoing.length,
  141. ),
  142. )
  143. : _getOutgoingCollectionEmptyState(),
  144. ],
  145. ),
  146. ),
  147. );
  148. }
  149. Widget _getIncomingCollectionEmptyState() {
  150. return Container(
  151. padding: EdgeInsets.only(top: 10),
  152. child: Column(
  153. children: [
  154. Text(
  155. "no one is sharing with you",
  156. style: TextStyle(color: Colors.white.withOpacity(0.6)),
  157. ),
  158. Container(
  159. padding: EdgeInsets.fromLTRB(28, 20, 28, 46),
  160. child: OutlinedButton(
  161. style: OutlinedButton.styleFrom(
  162. shape: RoundedRectangleBorder(
  163. borderRadius: BorderRadius.circular(10),
  164. ),
  165. padding: EdgeInsets.fromLTRB(50, 16, 50, 16),
  166. side: BorderSide(
  167. width: 2,
  168. color: Theme.of(context).buttonColor.withOpacity(0.5),
  169. ),
  170. ),
  171. child: Row(
  172. mainAxisAlignment: MainAxisAlignment.center,
  173. crossAxisAlignment: CrossAxisAlignment.center,
  174. mainAxisSize: MainAxisSize.min,
  175. children: [
  176. Icon(
  177. Icons.outgoing_mail,
  178. color: Colors.white.withOpacity(0.7),
  179. ),
  180. Padding(padding: EdgeInsets.all(6)),
  181. Text(
  182. "invite",
  183. style: TextStyle(
  184. color: Colors.white.withOpacity(0.8),
  185. ),
  186. ),
  187. ],
  188. ),
  189. onPressed: () async {
  190. shareText("Check out https://ente.io");
  191. },
  192. ),
  193. ),
  194. ],
  195. ),
  196. );
  197. }
  198. Widget _getOutgoingCollectionEmptyState() {
  199. return Container(
  200. padding: EdgeInsets.only(top: 10),
  201. child: Column(
  202. children: [
  203. Text(
  204. "you aren't sharing anything",
  205. style: TextStyle(color: Colors.white.withOpacity(0.6)),
  206. ),
  207. Container(
  208. padding: EdgeInsets.fromLTRB(28, 20, 28, 46),
  209. child: OutlinedButton(
  210. style: OutlinedButton.styleFrom(
  211. shape: RoundedRectangleBorder(
  212. borderRadius: BorderRadius.circular(10),
  213. ),
  214. padding: EdgeInsets.fromLTRB(50, 16, 50, 16),
  215. side: BorderSide(
  216. width: 2,
  217. color: Theme.of(context).buttonColor.withOpacity(0.5),
  218. ),
  219. ),
  220. child: Row(
  221. mainAxisAlignment: MainAxisAlignment.center,
  222. crossAxisAlignment: CrossAxisAlignment.center,
  223. mainAxisSize: MainAxisSize.min,
  224. children: [
  225. Icon(
  226. Icons.person_add,
  227. color: Colors.white.withOpacity(0.7),
  228. ),
  229. Padding(padding: EdgeInsets.all(6)),
  230. Text(
  231. "share",
  232. style: TextStyle(
  233. color: Colors.white.withOpacity(0.8),
  234. ),
  235. ),
  236. ],
  237. ),
  238. onPressed: () async {
  239. await showToast("select an album on ente to share",
  240. toastLength: Toast.LENGTH_LONG);
  241. Bus.instance.fire(
  242. TabChangedEvent(1, TabChangedEventSource.collections_page));
  243. },
  244. ),
  245. ),
  246. ],
  247. ),
  248. );
  249. }
  250. @override
  251. void dispose() {
  252. _localFilesSubscription.cancel();
  253. _collectionUpdatesSubscription.cancel();
  254. _loggedOutEvent.cancel();
  255. super.dispose();
  256. }
  257. @override
  258. bool get wantKeepAlive => true;
  259. }
  260. class OutgoingCollectionItem extends StatelessWidget {
  261. final CollectionWithThumbnail c;
  262. const OutgoingCollectionItem(
  263. this.c, {
  264. Key key,
  265. }) : super(key: key);
  266. @override
  267. Widget build(BuildContext context) {
  268. final sharees = List<String>();
  269. for (int index = 0; index < c.collection.sharees.length; index++) {
  270. if (index < 2) {
  271. sharees.add(c.collection.sharees[index].name);
  272. } else {
  273. final remaining = c.collection.sharees.length - index;
  274. if (remaining == 1) {
  275. // If it's the last sharee
  276. sharees.add(c.collection.sharees[index].name);
  277. } else {
  278. sharees.add("and " +
  279. remaining.toString() +
  280. " other" +
  281. (remaining > 1 ? "s" : ""));
  282. }
  283. break;
  284. }
  285. }
  286. return GestureDetector(
  287. behavior: HitTestBehavior.opaque,
  288. child: Container(
  289. margin: EdgeInsets.fromLTRB(16, 12, 8, 12),
  290. child: Row(
  291. children: <Widget>[
  292. ClipRRect(
  293. borderRadius: BorderRadius.circular(8.0),
  294. child: Container(
  295. child: Hero(
  296. tag: "outgoing_collection" + c.thumbnail.tag(),
  297. child: ThumbnailWidget(
  298. c.thumbnail,
  299. key: Key("outgoing_collection" + c.thumbnail.tag()),
  300. )),
  301. height: 60,
  302. width: 60,
  303. ),
  304. ),
  305. Padding(padding: EdgeInsets.all(8)),
  306. Column(
  307. crossAxisAlignment: CrossAxisAlignment.start,
  308. children: [
  309. Text(
  310. c.collection.name,
  311. style: TextStyle(
  312. fontSize: 16,
  313. ),
  314. ),
  315. Padding(
  316. padding: EdgeInsets.fromLTRB(0, 4, 0, 0),
  317. child: Text(
  318. "Shared with " + sharees.join(", "),
  319. style: TextStyle(
  320. fontSize: 14,
  321. color: Theme.of(context).primaryColorLight,
  322. ),
  323. textAlign: TextAlign.left,
  324. overflow: TextOverflow.ellipsis,
  325. ),
  326. ),
  327. ],
  328. ),
  329. ],
  330. ),
  331. ),
  332. onTap: () {
  333. final page = CollectionPage(
  334. c,
  335. tagPrefix: "outgoing_collection",
  336. );
  337. routeToPage(context, page);
  338. },
  339. );
  340. }
  341. }
  342. class IncomingCollectionItem extends StatelessWidget {
  343. final CollectionWithThumbnail c;
  344. const IncomingCollectionItem(
  345. this.c, {
  346. Key key,
  347. }) : super(key: key);
  348. @override
  349. Widget build(BuildContext context) {
  350. return GestureDetector(
  351. child: Column(
  352. children: <Widget>[
  353. ClipRRect(
  354. borderRadius: BorderRadius.circular(18.0),
  355. child: Container(
  356. child: Stack(
  357. children: [
  358. Hero(
  359. tag: "shared_collection" + c.thumbnail.tag(),
  360. child: ThumbnailWidget(
  361. c.thumbnail,
  362. key: Key("shared_collection" + c.thumbnail.tag()),
  363. )),
  364. Align(
  365. alignment: Alignment.bottomRight,
  366. child: Container(
  367. child: Text(
  368. c.collection.owner.name == null ||
  369. c.collection.owner.name.isEmpty
  370. ? c.collection.owner.email.substring(0, 1)
  371. : c.collection.owner.name.substring(0, 1),
  372. textAlign: TextAlign.center,
  373. ),
  374. padding: EdgeInsets.all(8),
  375. margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
  376. decoration: BoxDecoration(
  377. shape: BoxShape.circle,
  378. color: Theme.of(context).buttonColor,
  379. ),
  380. ),
  381. ),
  382. ],
  383. ),
  384. height: 160,
  385. width: 160,
  386. ),
  387. ),
  388. Padding(padding: EdgeInsets.all(2)),
  389. Expanded(
  390. child: Padding(
  391. padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
  392. child: Text(
  393. c.collection.name,
  394. style: TextStyle(
  395. fontSize: 16,
  396. ),
  397. overflow: TextOverflow.ellipsis,
  398. ),
  399. ),
  400. ),
  401. ],
  402. ),
  403. onTap: () {
  404. routeToPage(context, CollectionPage(c, tagPrefix: "shared_collection"));
  405. },
  406. );
  407. }
  408. }