ente/lib/ui/gallery.dart

251 lines
6.9 KiB
Dart
Raw Normal View History

2020-06-10 18:17:54 +00:00
import 'dart:async';
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-10 18:17:54 +00:00
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/photo_opened_event.dart';
2020-05-01 18:20:12 +00:00
import 'package:photos/models/photo.dart';
import 'package:photos/ui/detail_page.dart';
import 'package:photos/ui/loading_widget.dart';
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 {
final Future<List<Photo>> Function() loadFunction;
2020-04-27 15:11:29 +00:00
final Set<Photo> selectedPhotos;
2020-04-18 18:46:38 +00:00
final Function(Set<Photo>) photoSelectionChangeCallback;
final Future<void> Function() syncFunction;
2020-04-14 15:36:18 +00:00
Gallery(
this.loadFunction, {
this.selectedPhotos,
this.photoSelectionChangeCallback,
this.syncFunction,
});
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-04-12 12:38:49 +00:00
final ScrollController _scrollController = ScrollController();
2020-04-17 09:54:42 +00:00
final List<List<Photo>> _collatedPhotos = List<List<Photo>>();
bool _requiresLoad = false;
AsyncSnapshot<List<Photo>> _lastSnapshot;
2020-04-27 15:11:29 +00:00
Set<Photo> _selectedPhotos = HashSet<Photo>();
2020-04-25 22:57:43 +00:00
List<Photo> _photos;
RefreshController _refreshController = RefreshController();
StreamSubscription<PhotoOpenedEvent> _photoOpenEventSubscription;
2020-06-10 18:17:54 +00:00
Photo _openedPhoto;
@override
void initState() {
_requiresLoad = true;
_photoOpenEventSubscription =
Bus.instance.on<PhotoOpenedEvent>().listen((event) {
2020-06-10 18:17:54 +00:00
setState(() {
_openedPhoto = event.photo;
});
});
super.initState();
}
@override
void dispose() {
_photoOpenEventSubscription.cancel();
2020-06-10 18:17:54 +00:00
super.dispose();
}
2020-05-04 20:03:06 +00:00
2020-03-28 18:18:27 +00:00
@override
Widget build(BuildContext context) {
if (!_requiresLoad) {
return _onSnapshotAvailable(_lastSnapshot);
}
return FutureBuilder<List<Photo>>(
future: widget.loadFunction(),
builder: (context, snapshot) {
_requiresLoad = false;
_lastSnapshot = snapshot;
return _onSnapshotAvailable(snapshot);
},
);
}
Widget _onSnapshotAvailable(AsyncSnapshot<List<Photo>> snapshot) {
if (snapshot.hasData) {
return _onDataLoaded(snapshot.data);
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: loadWidget);
}
}
Widget _onDataLoaded(List<Photo> photos) {
_photos = photos;
if (_photos.isEmpty) {
return Center(child: Text("Nothing to see here."));
}
_selectedPhotos = widget.selectedPhotos ?? Set<Photo>();
2020-04-17 09:54:42 +00:00
_collatePhotos();
final list = ListView.builder(
itemCount: _collatedPhotos.length,
itemBuilder: _buildListItem,
controller: _scrollController,
cacheExtent: 1000,
2020-04-14 15:36:18 +00:00
);
if (widget.syncFunction != null) {
return SmartRefresher(
controller: _refreshController,
child: list,
header: SyncIndicator(_refreshController),
onRefresh: () {
widget.syncFunction().then((_) {
_refreshController.refreshCompleted();
widget.loadFunction().then((_) => setState(() {
_requiresLoad = true;
}));
}).catchError((e) {
_refreshController.refreshFailed();
setState(() {});
});
},
);
} 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-04-17 09:54:42 +00:00
var photos = _collatedPhotos[index];
2020-04-13 15:01:27 +00:00
return Column(
children: <Widget>[
_getDay(photos[0].createTimestamp),
_getGallery(photos)
],
2020-04-13 15:01:27 +00:00
);
}
Widget _getDay(int timestamp) {
2020-04-13 15:01:27 +00:00
return Container(
padding: const EdgeInsets.all(8.0),
2020-04-13 15:01:27 +00:00
alignment: Alignment.centerLeft,
child: Text(
getDayAndMonth(DateTime.fromMicrosecondsSinceEpoch(timestamp)),
style: TextStyle(fontSize: 16),
),
2020-04-13 15:01:27 +00:00
);
}
Widget _getGallery(List<Photo> photos) {
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 12),
physics: ScrollPhysics(), // to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildPhoto(context, photos[index]);
},
itemCount: photos.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
);
}
Widget _buildPhoto(BuildContext context, Photo photo) {
2020-06-10 21:36:01 +00:00
Widget thumbnail;
if (_openedPhoto == null || _openedPhoto == photo) {
2020-06-15 00:53:12 +00:00
thumbnail = Hero(
tag: photo.generatedId.toString(),
child: ThumbnailWidget(photo),
);
2020-06-10 21:36:01 +00:00
} else {
thumbnail = ThumbnailWidget(photo);
}
2020-03-28 18:18:27 +00:00
return GestureDetector(
2020-04-05 14:00:44 +00:00
onTap: () {
2020-04-27 15:11:29 +00:00
if (_selectedPhotos.isNotEmpty) {
_selectPhoto(photo);
} else {
2020-06-10 21:36:01 +00:00
_routeToDetailPage(photo, context);
}
2020-03-30 10:57:04 +00:00
},
onLongPress: () {
2020-04-18 18:46:38 +00:00
HapticFeedback.lightImpact();
_selectPhoto(photo);
2020-03-28 18:18:27 +00:00
},
child: Container(
margin: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
border: _selectedPhotos.contains(photo)
? Border.all(width: 4.0, color: Colors.blue)
: null,
2020-04-12 12:38:49 +00:00
),
2020-06-10 21:36:01 +00:00
child: thumbnail,
2020-04-12 12:38:49 +00:00
),
);
}
void _selectPhoto(Photo photo) {
setState(() {
if (_selectedPhotos.contains(photo)) {
_selectedPhotos.remove(photo);
} else {
_selectedPhotos.add(photo);
}
widget.photoSelectionChangeCallback(_selectedPhotos);
});
2020-04-12 12:38:49 +00:00
}
2020-06-10 21:36:01 +00:00
void _routeToDetailPage(Photo photo, BuildContext context) {
2020-04-25 22:57:43 +00:00
final page = DetailPage(
_photos,
_photos.indexOf(photo),
);
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
void _collatePhotos() {
final dailyPhotos = List<Photo>();
final collatedPhotos = List<List<Photo>>();
2020-04-25 22:57:43 +00:00
for (int index = 0; index < _photos.length; index++) {
2020-04-17 09:54:42 +00:00
if (index > 0 &&
2020-04-25 22:57:43 +00:00
!_arePhotosFromSameDay(_photos[index], _photos[index - 1])) {
2020-04-17 09:54:42 +00:00
var collatedDailyPhotos = List<Photo>();
collatedDailyPhotos.addAll(dailyPhotos);
collatedPhotos.add(collatedDailyPhotos);
dailyPhotos.clear();
}
2020-04-25 22:57:43 +00:00
dailyPhotos.add(_photos[index]);
2020-04-17 09:54:42 +00:00
}
if (dailyPhotos.isNotEmpty) {
collatedPhotos.add(dailyPhotos);
}
_collatedPhotos.clear();
_collatedPhotos.addAll(collatedPhotos);
}
bool _arePhotosFromSameDay(Photo firstPhoto, Photo secondPhoto) {
var firstDate =
DateTime.fromMicrosecondsSinceEpoch(firstPhoto.createTimestamp);
var secondDate =
DateTime.fromMicrosecondsSinceEpoch(secondPhoto.createTimestamp);
return firstDate.year == secondDate.year &&
firstDate.month == secondDate.month &&
firstDate.day == secondDate.day;
}
2020-03-28 18:18:27 +00:00
}