2020-06-10 18:17:54 +00:00
|
|
|
import 'dart:async';
|
2020-04-17 20:42:38 +00:00
|
|
|
import 'dart:collection';
|
2020-04-12 12:38:49 +00:00
|
|
|
|
|
|
|
import 'package:flutter/cupertino.dart';
|
2020-03-28 18:18:27 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2020-04-18 18:46:38 +00:00
|
|
|
import 'package:flutter/services.dart';
|
2020-06-17 15:09:47 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2020-06-15 19:03:43 +00:00
|
|
|
import 'package:photos/events/event.dart';
|
2020-06-19 23:03:26 +00:00
|
|
|
import 'package:photos/models/file.dart';
|
2020-05-01 18:20:12 +00:00
|
|
|
import 'package:photos/ui/detail_page.dart';
|
2020-06-15 04:25:58 +00:00
|
|
|
import 'package:photos/ui/loading_widget.dart';
|
2020-06-15 18:42:25 +00:00
|
|
|
import 'package:photos/ui/sync_indicator.dart';
|
2020-05-01 18:20:12 +00:00
|
|
|
import 'package:photos/ui/thumbnail_widget.dart';
|
|
|
|
import 'package:photos/utils/date_time_util.dart';
|
2020-06-06 13:48:29 +00:00
|
|
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
2020-03-28 18:18:27 +00:00
|
|
|
|
|
|
|
class Gallery extends StatefulWidget {
|
2020-07-12 22:17:44 +00:00
|
|
|
final List<File> Function() syncLoader;
|
|
|
|
final Future<List<File>> Function() asyncLoader;
|
2020-06-16 12:56:23 +00:00
|
|
|
// TODO: Verify why the event is necessary when calling loader post onRefresh
|
|
|
|
// should have done the job.
|
|
|
|
final Stream<Event> reloadEvent;
|
|
|
|
final Future<void> Function() onRefresh;
|
2020-06-19 23:03:26 +00:00
|
|
|
final Set<File> selectedFiles;
|
|
|
|
final Function(Set<File>) onFileSelectionChange;
|
2020-04-14 15:36:18 +00:00
|
|
|
|
2020-07-12 22:17:44 +00:00
|
|
|
Gallery({
|
|
|
|
this.syncLoader,
|
|
|
|
this.asyncLoader,
|
2020-06-16 12:56:23 +00:00
|
|
|
this.reloadEvent,
|
|
|
|
this.onRefresh,
|
2020-06-19 23:03:26 +00:00
|
|
|
this.selectedFiles,
|
|
|
|
this.onFileSelectionChange,
|
2020-06-15 04:25:58 +00:00
|
|
|
});
|
2020-04-14 15:36:18 +00:00
|
|
|
|
2020-03-28 18:18:27 +00:00
|
|
|
@override
|
|
|
|
_GalleryState createState() {
|
2020-04-18 18:46:38 +00:00
|
|
|
return _GalleryState();
|
2020-03-28 18:18:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _GalleryState extends State<Gallery> {
|
2020-06-17 15:09:47 +00:00
|
|
|
final Logger _logger = Logger("Gallery");
|
2020-04-12 12:38:49 +00:00
|
|
|
final ScrollController _scrollController = ScrollController();
|
2020-06-19 23:03:26 +00:00
|
|
|
final List<List<File>> _collatedFiles = List<List<File>>();
|
2020-06-15 12:46:51 +00:00
|
|
|
|
|
|
|
bool _requiresLoad = false;
|
2020-06-19 23:03:26 +00:00
|
|
|
AsyncSnapshot<List<File>> _lastSnapshot;
|
|
|
|
Set<File> _selectedFiles = HashSet<File>();
|
|
|
|
List<File> _files;
|
2020-06-15 03:12:02 +00:00
|
|
|
RefreshController _refreshController = RefreshController();
|
2020-06-10 18:17:54 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
2020-06-15 12:46:51 +00:00
|
|
|
_requiresLoad = true;
|
2020-06-16 12:56:23 +00:00
|
|
|
if (widget.reloadEvent != null) {
|
|
|
|
widget.reloadEvent.listen((event) {
|
|
|
|
setState(() {
|
|
|
|
_requiresLoad = true;
|
|
|
|
});
|
2020-06-15 19:03:43 +00:00
|
|
|
});
|
2020-06-16 12:56:23 +00:00
|
|
|
}
|
2020-06-10 18:17:54 +00:00
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
2020-03-28 18:18:27 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2020-06-15 12:46:51 +00:00
|
|
|
if (!_requiresLoad) {
|
2020-07-12 22:17:44 +00:00
|
|
|
if (widget.syncLoader != null) {
|
|
|
|
return _onDataLoaded();
|
|
|
|
}
|
|
|
|
return _onSnapshotAvailable();
|
|
|
|
}
|
|
|
|
if (widget.syncLoader != null) {
|
|
|
|
_files = widget.syncLoader();
|
|
|
|
return _onDataLoaded();
|
2020-06-15 12:46:51 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
return FutureBuilder<List<File>>(
|
2020-07-12 22:17:44 +00:00
|
|
|
future: widget.asyncLoader(),
|
2020-06-15 04:25:58 +00:00
|
|
|
builder: (context, snapshot) {
|
2020-07-12 22:17:44 +00:00
|
|
|
if (snapshot.hasData) {
|
|
|
|
_logger.info(snapshot.data.length.toString());
|
|
|
|
}
|
2020-06-15 12:46:51 +00:00
|
|
|
_lastSnapshot = snapshot;
|
2020-07-12 22:17:44 +00:00
|
|
|
return _onSnapshotAvailable();
|
2020-06-15 04:25:58 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-12 22:17:44 +00:00
|
|
|
Widget _onSnapshotAvailable() {
|
|
|
|
if (_lastSnapshot.hasData) {
|
2020-06-17 11:21:29 +00:00
|
|
|
_requiresLoad = false;
|
2020-07-12 22:17:44 +00:00
|
|
|
_files = _lastSnapshot.data;
|
|
|
|
return _onDataLoaded();
|
|
|
|
} else if (_lastSnapshot.hasError) {
|
2020-06-17 11:21:29 +00:00
|
|
|
_requiresLoad = false;
|
2020-07-12 22:17:44 +00:00
|
|
|
return Center(child: Text(_lastSnapshot.error.toString()));
|
2020-06-15 12:46:51 +00:00
|
|
|
} else {
|
|
|
|
return Center(child: loadWidget);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-12 22:17:44 +00:00
|
|
|
Widget _onDataLoaded() {
|
|
|
|
_logger.info("Loaded " + _files.length.toString());
|
2020-06-19 23:03:26 +00:00
|
|
|
if (_files.isEmpty) {
|
2020-06-15 19:38:57 +00:00
|
|
|
return Center(child: Text("Nothing to see here! 👀"));
|
2020-06-15 04:25:58 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
_selectedFiles = widget.selectedFiles ?? Set<File>();
|
|
|
|
_collateFiles();
|
2020-06-06 14:23:26 +00:00
|
|
|
final list = ListView.builder(
|
2020-06-19 23:03:26 +00:00
|
|
|
itemCount: _collatedFiles.length,
|
2020-06-06 14:23:26 +00:00
|
|
|
itemBuilder: _buildListItem,
|
|
|
|
controller: _scrollController,
|
|
|
|
cacheExtent: 1000,
|
2020-04-14 15:36:18 +00:00
|
|
|
);
|
2020-06-16 12:56:23 +00:00
|
|
|
if (widget.onRefresh != null) {
|
2020-06-06 14:23:26 +00:00
|
|
|
return SmartRefresher(
|
|
|
|
controller: _refreshController,
|
|
|
|
child: list,
|
2020-06-15 18:55:07 +00:00
|
|
|
header: SyncIndicator(_refreshController),
|
2020-06-15 03:12:02 +00:00
|
|
|
onRefresh: () {
|
2020-06-16 12:56:23 +00:00
|
|
|
widget.onRefresh().then((_) {
|
2020-06-06 14:23:26 +00:00
|
|
|
_refreshController.refreshCompleted();
|
2020-07-12 22:17:44 +00:00
|
|
|
widget.asyncLoader().then((_) => setState(() {
|
2020-06-15 12:46:51 +00:00
|
|
|
_requiresLoad = true;
|
|
|
|
}));
|
2020-06-06 14:23:26 +00:00
|
|
|
}).catchError((e) {
|
|
|
|
_refreshController.refreshFailed();
|
2020-06-15 18:42:25 +00:00
|
|
|
setState(() {});
|
2020-06-06 14:23:26 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return list;
|
|
|
|
}
|
2020-03-28 18:18:27 +00:00
|
|
|
}
|
|
|
|
|
2020-04-13 15:01:27 +00:00
|
|
|
Widget _buildListItem(BuildContext context, int index) {
|
2020-06-19 23:03:26 +00:00
|
|
|
var files = _collatedFiles[index];
|
2020-04-13 15:01:27 +00:00
|
|
|
return Column(
|
2020-07-06 19:09:47 +00:00
|
|
|
children: <Widget>[_getDay(files[0].creationTime), _getGallery(files)],
|
2020-04-13 15:01:27 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:53:03 +00:00
|
|
|
Widget _getDay(int timestamp) {
|
2020-04-13 15:01:27 +00:00
|
|
|
return Container(
|
2020-04-13 17:53:03 +00:00
|
|
|
padding: const EdgeInsets.all(8.0),
|
2020-04-13 15:01:27 +00:00
|
|
|
alignment: Alignment.centerLeft,
|
2020-04-13 17:53:03 +00:00
|
|
|
child: Text(
|
|
|
|
getDayAndMonth(DateTime.fromMicrosecondsSinceEpoch(timestamp)),
|
|
|
|
style: TextStyle(fontSize: 16),
|
|
|
|
),
|
2020-04-13 15:01:27 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
Widget _getGallery(List<File> files) {
|
2020-04-13 15:01:27 +00:00
|
|
|
return GridView.builder(
|
|
|
|
shrinkWrap: true,
|
|
|
|
padding: EdgeInsets.only(bottom: 12),
|
|
|
|
physics: ScrollPhysics(), // to disable GridView's scrolling
|
|
|
|
itemBuilder: (context, index) {
|
2020-06-19 23:03:26 +00:00
|
|
|
return _buildFile(context, files[index]);
|
2020-04-13 15:01:27 +00:00
|
|
|
},
|
2020-06-19 23:03:26 +00:00
|
|
|
itemCount: files.length,
|
2020-04-13 15:01:27 +00:00
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
crossAxisCount: 4,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
Widget _buildFile(BuildContext context, File file) {
|
2020-03-28 18:18:27 +00:00
|
|
|
return GestureDetector(
|
2020-04-05 14:00:44 +00:00
|
|
|
onTap: () {
|
2020-06-19 23:03:26 +00:00
|
|
|
if (_selectedFiles.isNotEmpty) {
|
|
|
|
_selectFile(file);
|
2020-04-17 20:42:38 +00:00
|
|
|
} else {
|
2020-06-19 23:03:26 +00:00
|
|
|
_routeToDetailPage(file, context);
|
2020-04-17 20:42:38 +00:00
|
|
|
}
|
2020-03-30 10:57:04 +00:00
|
|
|
},
|
|
|
|
onLongPress: () {
|
2020-04-18 18:46:38 +00:00
|
|
|
HapticFeedback.lightImpact();
|
2020-06-19 23:03:26 +00:00
|
|
|
_selectFile(file);
|
2020-03-28 18:18:27 +00:00
|
|
|
},
|
2020-04-17 20:42:38 +00:00
|
|
|
child: Container(
|
|
|
|
margin: const EdgeInsets.all(2.0),
|
|
|
|
decoration: BoxDecoration(
|
2020-06-19 23:03:26 +00:00
|
|
|
border: _selectedFiles.contains(file)
|
2020-04-17 20:42:38 +00:00
|
|
|
? Border.all(width: 4.0, color: Colors.blue)
|
|
|
|
: null,
|
2020-04-12 12:38:49 +00:00
|
|
|
),
|
2020-06-17 15:09:47 +00:00
|
|
|
child: Hero(
|
2020-06-23 20:13:45 +00:00
|
|
|
tag: file.tag(),
|
2020-06-19 23:03:26 +00:00
|
|
|
child: ThumbnailWidget(file),
|
2020-06-17 15:09:47 +00:00
|
|
|
),
|
2020-04-12 12:38:49 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
void _selectFile(File file) {
|
2020-04-17 20:42:38 +00:00
|
|
|
setState(() {
|
2020-06-19 23:03:26 +00:00
|
|
|
if (_selectedFiles.contains(file)) {
|
|
|
|
_selectedFiles.remove(file);
|
2020-04-17 20:42:38 +00:00
|
|
|
} else {
|
2020-06-19 23:03:26 +00:00
|
|
|
_selectedFiles.add(file);
|
2020-04-17 20:42:38 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
widget.onFileSelectionChange(_selectedFiles);
|
2020-04-17 20:42:38 +00:00
|
|
|
});
|
2020-04-12 12:38:49 +00:00
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
void _routeToDetailPage(File file, BuildContext context) {
|
2020-04-25 22:57:43 +00:00
|
|
|
final page = DetailPage(
|
2020-06-19 23:03:26 +00:00
|
|
|
_files,
|
|
|
|
_files.indexOf(file),
|
2020-04-25 22:57:43 +00:00
|
|
|
);
|
2020-03-28 18:18:27 +00:00
|
|
|
Navigator.of(context).push(
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (BuildContext context) {
|
|
|
|
return page;
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2020-04-17 09:54:42 +00:00
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
void _collateFiles() {
|
|
|
|
final dailyFiles = List<File>();
|
|
|
|
final collatedFiles = List<List<File>>();
|
|
|
|
for (int index = 0; index < _files.length; index++) {
|
2020-04-17 09:54:42 +00:00
|
|
|
if (index > 0 &&
|
2020-06-19 23:03:26 +00:00
|
|
|
!_areFilesFromSameDay(_files[index], _files[index - 1])) {
|
|
|
|
var collatedDailyFiles = List<File>();
|
|
|
|
collatedDailyFiles.addAll(dailyFiles);
|
|
|
|
collatedFiles.add(collatedDailyFiles);
|
|
|
|
dailyFiles.clear();
|
2020-04-17 09:54:42 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
dailyFiles.add(_files[index]);
|
2020-04-17 09:54:42 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
if (dailyFiles.isNotEmpty) {
|
|
|
|
collatedFiles.add(dailyFiles);
|
2020-04-17 09:54:42 +00:00
|
|
|
}
|
2020-06-19 23:03:26 +00:00
|
|
|
_collatedFiles.clear();
|
|
|
|
_collatedFiles.addAll(collatedFiles);
|
2020-04-17 09:54:42 +00:00
|
|
|
}
|
|
|
|
|
2020-06-19 23:03:26 +00:00
|
|
|
bool _areFilesFromSameDay(File first, File second) {
|
2020-07-06 19:09:47 +00:00
|
|
|
var firstDate = DateTime.fromMicrosecondsSinceEpoch(first.creationTime);
|
2020-07-10 18:18:21 +00:00
|
|
|
var secondDate = DateTime.fromMicrosecondsSinceEpoch(second.creationTime);
|
2020-04-17 09:54:42 +00:00
|
|
|
return firstDate.year == secondDate.year &&
|
|
|
|
firstDate.month == secondDate.month &&
|
|
|
|
firstDate.day == secondDate.day;
|
|
|
|
}
|
2020-03-28 18:18:27 +00:00
|
|
|
}
|