gallery.dart 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import 'dart:async';
  2. import 'dart:collection';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/events/event.dart';
  8. import 'package:photos/models/file.dart';
  9. import 'package:photos/ui/detail_page.dart';
  10. import 'package:photos/ui/loading_widget.dart';
  11. import 'package:photos/ui/sync_indicator.dart';
  12. import 'package:photos/ui/thumbnail_widget.dart';
  13. import 'package:photos/utils/date_time_util.dart';
  14. import 'package:pull_to_refresh/pull_to_refresh.dart';
  15. class Gallery extends StatefulWidget {
  16. final Future<List<File>> Function() loader;
  17. // TODO: Verify why the event is necessary when calling loader post onRefresh
  18. // should have done the job.
  19. final Stream<Event> reloadEvent;
  20. final Future<void> Function() onRefresh;
  21. final Set<File> selectedFiles;
  22. final Function(Set<File>) onFileSelectionChange;
  23. Gallery(
  24. this.loader, {
  25. this.reloadEvent,
  26. this.onRefresh,
  27. this.selectedFiles,
  28. this.onFileSelectionChange,
  29. });
  30. @override
  31. _GalleryState createState() {
  32. return _GalleryState();
  33. }
  34. }
  35. class _GalleryState extends State<Gallery> {
  36. final Logger _logger = Logger("Gallery");
  37. final ScrollController _scrollController = ScrollController();
  38. final List<List<File>> _collatedFiles = List<List<File>>();
  39. bool _requiresLoad = false;
  40. AsyncSnapshot<List<File>> _lastSnapshot;
  41. Set<File> _selectedFiles = HashSet<File>();
  42. List<File> _files;
  43. RefreshController _refreshController = RefreshController();
  44. @override
  45. void initState() {
  46. _requiresLoad = true;
  47. if (widget.reloadEvent != null) {
  48. widget.reloadEvent.listen((event) {
  49. setState(() {
  50. _requiresLoad = true;
  51. });
  52. });
  53. }
  54. super.initState();
  55. }
  56. @override
  57. Widget build(BuildContext context) {
  58. if (!_requiresLoad) {
  59. return _onSnapshotAvailable(_lastSnapshot);
  60. }
  61. return FutureBuilder<List<File>>(
  62. future: widget.loader(),
  63. builder: (context, snapshot) {
  64. _lastSnapshot = snapshot;
  65. return _onSnapshotAvailable(snapshot);
  66. },
  67. );
  68. }
  69. Widget _onSnapshotAvailable(AsyncSnapshot<List<File>> snapshot) {
  70. if (snapshot.hasData) {
  71. _requiresLoad = false;
  72. return _onDataLoaded(snapshot.data);
  73. } else if (snapshot.hasError) {
  74. _requiresLoad = false;
  75. return Center(child: Text(snapshot.error.toString()));
  76. } else {
  77. return Center(child: loadWidget);
  78. }
  79. }
  80. Widget _onDataLoaded(List<File> files) {
  81. _files = files;
  82. if (_files.isEmpty) {
  83. return Center(child: Text("Nothing to see here! 👀"));
  84. }
  85. _selectedFiles = widget.selectedFiles ?? Set<File>();
  86. _collateFiles();
  87. final list = ListView.builder(
  88. itemCount: _collatedFiles.length,
  89. itemBuilder: _buildListItem,
  90. controller: _scrollController,
  91. cacheExtent: 1000,
  92. );
  93. if (widget.onRefresh != null) {
  94. return SmartRefresher(
  95. controller: _refreshController,
  96. child: list,
  97. header: SyncIndicator(_refreshController),
  98. onRefresh: () {
  99. widget.onRefresh().then((_) {
  100. _refreshController.refreshCompleted();
  101. widget.loader().then((_) => setState(() {
  102. _requiresLoad = true;
  103. }));
  104. }).catchError((e) {
  105. _refreshController.refreshFailed();
  106. setState(() {});
  107. });
  108. },
  109. );
  110. } else {
  111. return list;
  112. }
  113. }
  114. Widget _buildListItem(BuildContext context, int index) {
  115. var files = _collatedFiles[index];
  116. return Column(
  117. children: <Widget>[_getDay(files[0].createTimestamp), _getGallery(files)],
  118. );
  119. }
  120. Widget _getDay(int timestamp) {
  121. return Container(
  122. padding: const EdgeInsets.all(8.0),
  123. alignment: Alignment.centerLeft,
  124. child: Text(
  125. getDayAndMonth(DateTime.fromMicrosecondsSinceEpoch(timestamp)),
  126. style: TextStyle(fontSize: 16),
  127. ),
  128. );
  129. }
  130. Widget _getGallery(List<File> files) {
  131. return GridView.builder(
  132. shrinkWrap: true,
  133. padding: EdgeInsets.only(bottom: 12),
  134. physics: ScrollPhysics(), // to disable GridView's scrolling
  135. itemBuilder: (context, index) {
  136. return _buildFile(context, files[index]);
  137. },
  138. itemCount: files.length,
  139. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  140. crossAxisCount: 4,
  141. ),
  142. );
  143. }
  144. Widget _buildFile(BuildContext context, File file) {
  145. return GestureDetector(
  146. onTap: () {
  147. if (_selectedFiles.isNotEmpty) {
  148. _selectFile(file);
  149. } else {
  150. _routeToDetailPage(file, context);
  151. }
  152. },
  153. onLongPress: () {
  154. HapticFeedback.lightImpact();
  155. _selectFile(file);
  156. },
  157. child: Container(
  158. margin: const EdgeInsets.all(2.0),
  159. decoration: BoxDecoration(
  160. border: _selectedFiles.contains(file)
  161. ? Border.all(width: 4.0, color: Colors.blue)
  162. : null,
  163. ),
  164. child: Hero(
  165. tag: file.tag(),
  166. child: ThumbnailWidget(file),
  167. ),
  168. ),
  169. );
  170. }
  171. void _selectFile(File file) {
  172. setState(() {
  173. if (_selectedFiles.contains(file)) {
  174. _selectedFiles.remove(file);
  175. } else {
  176. _selectedFiles.add(file);
  177. }
  178. widget.onFileSelectionChange(_selectedFiles);
  179. });
  180. }
  181. void _routeToDetailPage(File file, BuildContext context) {
  182. final page = DetailPage(
  183. _files,
  184. _files.indexOf(file),
  185. );
  186. Navigator.of(context).push(
  187. MaterialPageRoute(
  188. builder: (BuildContext context) {
  189. return page;
  190. },
  191. ),
  192. );
  193. }
  194. void _collateFiles() {
  195. final dailyFiles = List<File>();
  196. final collatedFiles = List<List<File>>();
  197. for (int index = 0; index < _files.length; index++) {
  198. if (index > 0 &&
  199. !_areFilesFromSameDay(_files[index], _files[index - 1])) {
  200. var collatedDailyFiles = List<File>();
  201. collatedDailyFiles.addAll(dailyFiles);
  202. collatedFiles.add(collatedDailyFiles);
  203. dailyFiles.clear();
  204. }
  205. dailyFiles.add(_files[index]);
  206. }
  207. if (dailyFiles.isNotEmpty) {
  208. collatedFiles.add(dailyFiles);
  209. }
  210. _collatedFiles.clear();
  211. _collatedFiles.addAll(collatedFiles);
  212. }
  213. bool _areFilesFromSameDay(File first, File second) {
  214. var firstDate = DateTime.fromMicrosecondsSinceEpoch(first.createTimestamp);
  215. var secondDate =
  216. DateTime.fromMicrosecondsSinceEpoch(second.createTimestamp);
  217. return firstDate.year == secondDate.year &&
  218. firstDate.month == secondDate.month &&
  219. firstDate.day == secondDate.day;
  220. }
  221. }