ente/lib/ui/gallery.dart

392 lines
11 KiB
Dart
Raw Normal View History

2020-06-10 18:17:54 +00:00
import 'dart:async';
2020-11-12 13:25:57 +00:00
import 'dart:math';
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';
import 'package:photos/models/selected_files.dart';
2020-08-09 14:58:41 +00:00
import 'package:photos/ui/common_elements.dart';
2020-05-01 18:20:12 +00:00
import 'package:photos/ui/detail_page.dart';
2020-11-12 13:25:57 +00:00
import 'package:photos/ui/draggable_scrollbar.dart';
import 'package:photos/ui/loading_widget.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-11-12 13:25:57 +00:00
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
2020-03-28 18:18:27 +00:00
class Gallery extends StatefulWidget {
final List<File> Function() syncLoader;
final Future<List<File>> Function(File lastFile, int limit) asyncLoader;
2020-11-16 08:19:47 +00:00
final bool shouldLoadAll;
2020-06-16 12:56:23 +00:00
final Stream<Event> reloadEvent;
final SelectedFiles selectedFiles;
final String tagPrefix;
2020-07-20 13:32:30 +00:00
final Widget headerWidget;
2020-04-14 15:36:18 +00:00
Gallery({
this.syncLoader,
this.asyncLoader,
2020-11-16 08:19:47 +00:00
this.shouldLoadAll = false,
2020-06-16 12:56:23 +00:00
this.reloadEvent,
2020-07-20 13:32:30 +00:00
this.headerWidget,
@required this.selectedFiles,
@required this.tagPrefix,
});
2020-04-14 15:36:18 +00:00
2020-03-28 18:18:27 +00:00
@override
_GalleryState createState() {
return _GalleryState();
2020-03-28 18:18:27 +00:00
}
}
class _GalleryState extends State<Gallery> {
static final int kLoadLimit = 200;
2020-07-15 08:26:31 +00:00
static final int kEagerLoadTrigger = 10;
final Logger _logger = Logger("Gallery");
2020-06-19 23:03:26 +00:00
final List<List<File>> _collatedFiles = List<List<File>>();
final _itemScrollController = ItemScrollController();
2020-11-12 13:25:57 +00:00
final _itemPositionsListener = ItemPositionsListener.create();
final _scrollKey = GlobalKey<DraggableScrollbarState>();
ScrollController _scrollController = ScrollController();
double _scrollOffset = 0;
bool _requiresLoad = false;
2020-07-14 12:15:55 +00:00
bool _hasLoadedAll = false;
2020-07-15 23:02:59 +00:00
bool _isLoadingNext = false;
bool _hasDraggableScrollbar = false;
2020-06-19 23:03:26 +00:00
List<File> _files;
2020-11-12 13:25:57 +00:00
int _lastIndex = 0;
2020-06-10 18:17:54 +00:00
@override
void initState() {
_requiresLoad = true;
2020-06-16 12:56:23 +00:00
if (widget.reloadEvent != null) {
widget.reloadEvent.listen((event) {
if (mounted) {
setState(() {
_requiresLoad = true;
});
}
2020-06-15 19:03:43 +00:00
});
2020-06-16 12:56:23 +00:00
}
widget.selectedFiles.addListener(() {
setState(() {
if (!_hasDraggableScrollbar) {
_saveScrollPosition();
}
});
});
2020-11-16 08:19:47 +00:00
if (widget.asyncLoader == null || widget.shouldLoadAll) {
2020-10-28 19:06:32 +00:00
_hasLoadedAll = true;
}
2020-11-16 08:12:42 +00:00
_itemPositionsListener.itemPositions.addListener(_updateScrollbar);
super.initState();
}
2020-11-12 13:25:57 +00:00
@override
void dispose() {
2020-11-16 08:12:42 +00:00
_itemPositionsListener.itemPositions.removeListener(_updateScrollbar);
2020-11-12 13:25:57 +00:00
super.dispose();
}
2020-03-28 18:18:27 +00:00
@override
Widget build(BuildContext context) {
2020-11-16 08:28:43 +00:00
_logger.info("Building " + widget.tagPrefix);
if (!_requiresLoad) {
2020-07-14 12:15:55 +00:00
return _onDataLoaded();
}
if (widget.syncLoader != null) {
_files = widget.syncLoader();
return _onDataLoaded();
}
2020-06-19 23:03:26 +00:00
return FutureBuilder<List<File>>(
2020-07-19 21:26:26 +00:00
future: widget.asyncLoader(null, kLoadLimit),
builder: (context, snapshot) {
if (snapshot.hasData) {
2020-07-14 12:15:55 +00:00
_requiresLoad = false;
_files = snapshot.data;
return _onDataLoaded();
} else if (snapshot.hasError) {
_requiresLoad = false;
return Center(child: Text(snapshot.error.toString()));
} else {
return Center(child: loadWidget);
}
},
);
}
Widget _onDataLoaded() {
2020-06-19 23:03:26 +00:00
if (_files.isEmpty) {
final children = List<Widget>();
if (widget.headerWidget != null) {
children.add(widget.headerWidget);
}
children.add(Expanded(child: nothingToSeeHere));
return CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
),
)
],
);
}
2020-06-19 23:03:26 +00:00
_collateFiles();
2020-11-12 13:25:57 +00:00
final itemCount =
_collatedFiles.length + (widget.headerWidget == null ? 1 : 2);
_hasDraggableScrollbar = itemCount > 25 || _files.length > 50;
if (!_hasDraggableScrollbar) {
_scrollController = ScrollController(initialScrollOffset: _scrollOffset);
return ListView.builder(
itemCount: itemCount,
itemBuilder: _buildListItem,
controller: _scrollController,
cacheExtent: 1500,
addAutomaticKeepAlives: true,
);
}
2020-11-12 13:25:57 +00:00
return DraggableScrollbar.semicircle(
key: _scrollKey,
2020-11-16 08:12:42 +00:00
initialScrollIndex: _lastIndex,
2020-11-12 13:25:57 +00:00
labelTextBuilder: (position) {
final index =
min((position * itemCount).floor(), _collatedFiles.length - 1);
return Text(
getMonthAndYear(DateTime.fromMicrosecondsSinceEpoch(
_collatedFiles[index][0].creationTime)),
style: TextStyle(
color: Colors.black,
backgroundColor: Colors.white,
fontSize: 14,
),
);
},
labelConstraints: BoxConstraints.tightFor(width: 100.0, height: 36.0),
onChange: (position) {
final index =
min((position * itemCount).floor(), _collatedFiles.length - 1);
if (index == _lastIndex) {
return;
}
_lastIndex = index;
_itemScrollController.jumpTo(index: index);
2020-11-12 13:25:57 +00:00
},
child: ScrollablePositionedList.builder(
itemCount: itemCount,
itemBuilder: _buildListItem,
itemScrollController: _itemScrollController,
2020-11-16 08:12:42 +00:00
initialScrollIndex: _lastIndex,
2020-11-12 13:25:57 +00:00
minCacheExtent: 1500,
addAutomaticKeepAlives: true,
physics: _MaxVelocityPhysics(velocityThreshold: 128),
itemPositionsListener: _itemPositionsListener,
),
itemCount: itemCount,
2020-04-14 15:36:18 +00:00
);
2020-03-28 18:18:27 +00:00
}
2020-04-13 15:01:27 +00:00
Widget _buildListItem(BuildContext context, int index) {
2020-07-15 08:26:31 +00:00
if (_shouldLoadNextItems(index)) {
// Eagerly load next batch
_loadNextItems();
}
2020-10-28 19:06:32 +00:00
var fileIndex;
2020-07-20 13:32:30 +00:00
if (widget.headerWidget != null) {
if (index == 0) {
return widget.headerWidget;
}
2020-10-28 19:06:32 +00:00
fileIndex = index - 1;
} else {
fileIndex = index;
}
if (fileIndex == _collatedFiles.length) {
if (widget.asyncLoader != null) {
if (!_hasLoadedAll) {
return loadWidget;
} else {
return Container();
}
}
2020-07-20 13:32:30 +00:00
}
2020-08-09 14:58:41 +00:00
if (fileIndex < 0 || fileIndex >= _collatedFiles.length) {
return Container();
}
2020-07-20 13:32:30 +00:00
var files = _collatedFiles[fileIndex];
2020-04-13 15:01:27 +00:00
return Column(
children: <Widget>[_getDay(files[0].creationTime), _getGallery(files)],
2020-04-13 15:01:27 +00:00
);
}
2020-07-15 08:26:31 +00:00
bool _shouldLoadNextItems(int index) =>
widget.asyncLoader != null &&
2020-07-15 23:02:59 +00:00
!_isLoadingNext &&
(index >= _collatedFiles.length - kEagerLoadTrigger) &&
!_hasLoadedAll;
2020-07-15 08:26:31 +00:00
void _loadNextItems() {
2020-07-15 23:02:59 +00:00
_isLoadingNext = true;
2020-07-19 21:26:26 +00:00
widget.asyncLoader(_files[_files.length - 1], kLoadLimit).then((files) {
2020-07-15 08:26:31 +00:00
setState(() {
2020-07-15 23:02:59 +00:00
_isLoadingNext = false;
_saveScrollPosition();
2020-07-19 21:26:26 +00:00
if (files.length < kLoadLimit) {
2020-07-15 08:26:31 +00:00
_hasLoadedAll = true;
}
2020-11-16 08:19:47 +00:00
_files.addAll(files);
2020-07-15 08:26:31 +00:00
});
});
}
void _saveScrollPosition() {
_scrollOffset = _scrollController.offset;
}
Widget _getDay(int timestamp) {
2020-12-06 12:07:11 +00:00
final date = DateTime.fromMicrosecondsSinceEpoch(timestamp);
final now = DateTime.now();
var title = getDayAndMonth(date);
2020-12-06 12:07:11 +00:00
if (date.year == now.year && date.month == now.month) {
if (date.day == now.day) {
title = "Today";
} else if (date.day == now.day - 1) {
title = "Yesterday";
}
}
if (date.year != DateTime.now().year) {
title += " " + date.year.toString();
}
2020-04-13 15:01:27 +00:00
return Container(
2020-12-12 00:31:06 +00:00
padding: const EdgeInsets.fromLTRB(14, 8, 0, 8),
2020-04-13 15:01:27 +00:00
alignment: Alignment.centerLeft,
child: Text(
title,
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:
NeverScrollableScrollPhysics(), // to disable GridView's scrolling
2020-04-13 15:01:27 +00:00
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: () {
if (widget.selectedFiles.files.isNotEmpty) {
2020-06-19 23:03:26 +00:00
_selectFile(file);
} else {
2020-06-19 23:03:26 +00:00
_routeToDetailPage(file, context);
}
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
},
child: Container(
margin: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
border: widget.selectedFiles.files.contains(file)
? 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(
tag: widget.tagPrefix + 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) {
widget.selectedFiles.toggleSelection(file);
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),
widget.tagPrefix,
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-07-20 15:39:11 +00:00
!_areFilesFromSameDay(_files[index - 1], _files[index])) {
final collatedDailyFiles = List<File>();
2020-06-19 23:03:26 +00:00
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) {
var firstDate = DateTime.fromMicrosecondsSinceEpoch(first.creationTime);
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-11-12 13:25:57 +00:00
2020-11-16 08:12:42 +00:00
void _updateScrollbar() {
2020-11-12 13:25:57 +00:00
final index = _itemPositionsListener.itemPositions.value.first.index;
2020-11-16 08:12:42 +00:00
_lastIndex = index;
2020-11-12 13:25:57 +00:00
_scrollKey.currentState?.setPosition(index / _collatedFiles.length);
}
}
class _MaxVelocityPhysics extends AlwaysScrollableScrollPhysics {
final double velocityThreshold;
_MaxVelocityPhysics({@required this.velocityThreshold, ScrollPhysics parent})
: super(parent: parent);
@override
bool recommendDeferredLoading(
double velocity, ScrollMetrics metrics, BuildContext context) {
return velocity.abs() > velocityThreshold;
}
@override
_MaxVelocityPhysics applyTo(ScrollPhysics ancestor) {
return _MaxVelocityPhysics(
velocityThreshold: velocityThreshold, parent: buildParent(ancestor));
}
2020-03-28 18:18:27 +00:00
}