ente/lib/ui/viewer/gallery/gallery.dart

386 lines
12 KiB
Dart
Raw Normal View History

2020-06-10 18:17:54 +00:00
import 'dart:async';
2020-04-12 12:38:49 +00:00
2022-11-11 09:30:07 +00:00
import 'package:flutter/foundation.dart';
2020-03-28 18:18:27 +00:00
import 'package:flutter/material.dart';
2020-06-17 15:09:47 +00:00
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
2022-04-28 13:08:27 +00:00
import 'package:photos/ente_theme_data.dart';
2021-05-13 11:17:13 +00:00
import 'package:photos/events/event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/tab_changed_event.dart';
2020-06-19 23:03:26 +00:00
import 'package:photos/models/file.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/ui/common/loading_widget.dart';
2021-04-20 20:11:39 +00:00
import 'package:photos/ui/huge_listview/huge_listview.dart';
import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart';
2022-09-12 12:16:34 +00:00
import 'package:photos/ui/viewer/gallery/empty_state.dart';
2020-05-01 18:20:12 +00:00
import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/local_settings.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
2022-07-03 06:56:43 +00:00
typedef GalleryLoader = Future<FileLoadResult> Function(
int creationStartTime,
int creationEndTime, {
int? limit,
bool? asc,
2022-07-03 06:56:43 +00:00
});
2020-03-28 18:18:27 +00:00
class Gallery extends StatefulWidget {
final GalleryLoader asyncLoader;
final List<File>? initialFiles;
final Stream<FilesUpdatedEvent>? reloadEvent;
final List<Stream<Event>>? forceReloadEvents;
final Set<EventType> removalEventTypes;
final SelectedFiles? selectedFiles;
final String tagPrefix;
final Widget? header;
final Widget? footer;
final Widget emptyState;
final String? albumName;
final double scrollBottomSafeArea;
final bool shouldCollateFilesByDay;
final Widget loadingWidget;
final bool disableScroll;
final bool limitSelectionToOne;
2020-04-14 15:36:18 +00:00
const Gallery({
required this.asyncLoader,
required this.tagPrefix,
this.selectedFiles,
2021-05-13 18:05:32 +00:00
this.initialFiles,
2021-04-20 20:11:39 +00:00
this.reloadEvent,
2021-09-23 11:09:56 +00:00
this.forceReloadEvents,
this.removalEventTypes = const {},
2021-05-12 20:54:44 +00:00
this.header,
2022-08-12 07:13:48 +00:00
this.footer = const SizedBox(height: 120),
this.emptyState = const EmptyState(),
this.scrollBottomSafeArea = 120.0,
2022-04-30 12:18:26 +00:00
this.albumName = '',
this.shouldCollateFilesByDay = true,
this.loadingWidget = const EnteLoadingWidget(),
this.disableScroll = false,
this.limitSelectionToOne = false,
Key? key,
2021-09-23 11:09:56 +00:00
}) : super(key: key);
2020-04-14 15:36:18 +00:00
2020-03-28 18:18:27 +00:00
@override
2022-07-03 09:45:00 +00:00
State<Gallery> createState() {
return _GalleryState();
2020-03-28 18:18:27 +00:00
}
}
class _GalleryState extends State<Gallery> {
2021-04-25 12:17:44 +00:00
static const int kInitialLoadLimit = 100;
final _hugeListViewKey = GlobalKey<HugeListViewState>();
late Logger _logger;
List<List<File>> _collatedFiles = [];
bool _hasLoadedFiles = false;
late ItemScrollController _itemScroller;
StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
StreamSubscription<TabDoubleTapEvent>? _tabDoubleTapEvent;
2021-09-23 11:09:56 +00:00
final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
late String _logTag;
2020-06-10 18:17:54 +00:00
@override
void initState() {
2022-11-11 09:30:07 +00:00
_logTag =
"Gallery_${widget.tagPrefix}${kDebugMode ? "_" + widget.albumName! : ""}";
2023-05-03 09:52:06 +00:00
_logger = Logger(_logTag);
2022-11-11 09:30:07 +00:00
_logger.finest("init Gallery");
_itemScroller = ItemScrollController();
2020-06-16 12:56:23 +00:00
if (widget.reloadEvent != null) {
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
2022-11-12 05:19:38 +00:00
// In soft refresh, setState is called for entire gallery only when
// number of child change
_logger.finest("Soft refresh all files on ${event.reason} ");
final result = await _loadFiles();
2022-11-12 05:19:38 +00:00
final bool hasReloaded = _onFilesLoaded(result.files);
if (hasReloaded && kDebugMode) {
_logger.finest(
"Reloaded gallery on soft refresh all files on ${event.reason}",
);
}
2020-06-15 19:03:43 +00:00
});
2020-06-16 12:56:23 +00:00
}
_tabDoubleTapEvent =
Bus.instance.on<TabDoubleTapEvent>().listen((event) async {
// todo: Assign ID to Gallery and fire generic event with ID &
// target index/date
if (mounted && event.selectedIndex == 0) {
_itemScroller.scrollTo(
2022-10-25 07:20:15 +00:00
index: 0,
duration: const Duration(milliseconds: 150),
);
}
});
2021-09-23 11:09:56 +00:00
if (widget.forceReloadEvents != null) {
for (final event in widget.forceReloadEvents!) {
2022-06-11 08:23:52 +00:00
_forceReloadEventSubscriptions.add(
event.listen((event) async {
2022-11-12 05:19:38 +00:00
_logger.finest("Force refresh all files on ${event.reason}");
2022-06-11 08:23:52 +00:00
final result = await _loadFiles();
_setFilesAndReload(result.files);
}),
);
2021-09-23 11:09:56 +00:00
}
2021-05-13 11:17:13 +00:00
}
2021-05-13 18:05:32 +00:00
if (widget.initialFiles != null) {
_onFilesLoaded(widget.initialFiles!);
2021-05-13 18:05:32 +00:00
}
_loadFiles(limit: kInitialLoadLimit).then((result) async {
_setFilesAndReload(result.files);
if (result.hasMore) {
final result = await _loadFiles();
_setFilesAndReload(result.files);
}
});
super.initState();
}
void _setFilesAndReload(List<File> files) {
final hasReloaded = _onFilesLoaded(files);
if (!hasReloaded && mounted) {
setState(() {});
}
}
Future<FileLoadResult> _loadFiles({int? limit}) async {
2022-11-12 05:19:38 +00:00
_logger.info("Loading ${limit ?? "all"} files");
try {
2021-09-23 07:31:01 +00:00
final startTime = DateTime.now().microsecondsSinceEpoch;
final result = await widget.asyncLoader(
galleryLoadStartTime,
galleryLoadEndTime,
2022-06-11 08:23:52 +00:00
limit: limit,
);
2021-09-23 07:31:01 +00:00
final endTime = DateTime.now().microsecondsSinceEpoch;
final duration = Duration(microseconds: endTime - startTime);
2022-06-11 08:23:52 +00:00
_logger.info(
"Time taken to load " +
result.files.length.toString() +
" files :" +
duration.inMilliseconds.toString() +
"ms",
);
return result;
2021-09-23 07:31:01 +00:00
} catch (e, s) {
_logger.severe("failed to load files", e, s);
rethrow;
}
}
// Collates files and returns `true` if it resulted in a gallery reload
bool _onFilesLoaded(List<File> files) {
final updatedCollatedFiles =
widget.shouldCollateFilesByDay ? _collateFiles(files) : [files];
2022-11-11 09:30:07 +00:00
if (_collatedFiles.length != updatedCollatedFiles.length ||
2022-07-03 09:45:00 +00:00
_collatedFiles.isEmpty) {
if (mounted) {
setState(() {
_hasLoadedFiles = true;
2022-11-11 09:30:07 +00:00
_collatedFiles = updatedCollatedFiles;
});
}
2021-05-13 11:17:13 +00:00
return true;
} else {
2022-11-11 09:30:07 +00:00
_collatedFiles = updatedCollatedFiles;
2021-05-13 11:17:13 +00:00
return false;
}
}
2020-11-12 13:25:57 +00:00
@override
void dispose() {
2021-05-13 11:17:13 +00:00
_reloadEventSubscription?.cancel();
_tabDoubleTapEvent?.cancel();
2021-09-23 11:09:56 +00:00
for (final subscription in _forceReloadEventSubscriptions) {
subscription.cancel();
}
2020-11-12 13:25:57 +00:00
super.dispose();
}
2020-03-28 18:18:27 +00:00
@override
Widget build(BuildContext context) {
2022-11-11 09:30:07 +00:00
_logger.finest("Building Gallery ${widget.tagPrefix}");
if (!_hasLoadedFiles) {
return widget.loadingWidget;
}
return GalleryListView(
hugeListViewKey: _hugeListViewKey,
itemScroller: _itemScroller,
collatedFiles: _collatedFiles,
disableScroll: widget.disableScroll,
emptyState: widget.emptyState,
asyncLoader: widget.asyncLoader,
removalEventTypes: widget.removalEventTypes,
tagPrefix: widget.tagPrefix,
scrollBottomSafeArea: widget.scrollBottomSafeArea,
limitSelectionToOne: widget.limitSelectionToOne,
shouldCollateFilesByDay: widget.shouldCollateFilesByDay,
logTag: _logTag,
logger: _logger,
reloadEvent: widget.reloadEvent,
header: widget.header,
footer: widget.footer,
selectedFiles: widget.selectedFiles,
);
}
List<List<File>> _collateFiles(List<File> files) {
final List<File> dailyFiles = [];
final List<List<File>> collatedFiles = [];
for (int index = 0; index < files.length; index++) {
if (index > 0 &&
!areFromSameDay(
files[index - 1].creationTime!,
files[index].creationTime!,
)) {
final List<File> collatedDailyFiles = [];
collatedDailyFiles.addAll(dailyFiles);
collatedFiles.add(collatedDailyFiles);
dailyFiles.clear();
}
dailyFiles.add(files[index]);
}
if (dailyFiles.isNotEmpty) {
collatedFiles.add(dailyFiles);
}
collatedFiles
.sort((a, b) => b[0].creationTime!.compareTo(a[0].creationTime!));
return collatedFiles;
2020-04-12 12:38:49 +00:00
}
}
class GalleryIndexUpdatedEvent {
final String tag;
final int index;
GalleryIndexUpdatedEvent(this.tag, this.index);
}
2020-04-12 12:38:49 +00:00
class GalleryListView extends StatelessWidget {
final GlobalKey<HugeListViewState<dynamic>> hugeListViewKey;
final ItemScrollController itemScroller;
final List<List<File>> collatedFiles;
final bool disableScroll;
final Widget? header;
final Widget? footer;
final Widget emptyState;
final GalleryLoader asyncLoader;
final Stream<FilesUpdatedEvent>? reloadEvent;
final Set<EventType> removalEventTypes;
final String tagPrefix;
final double scrollBottomSafeArea;
final bool limitSelectionToOne;
final SelectedFiles? selectedFiles;
final bool shouldCollateFilesByDay;
final String logTag;
final Logger logger;
const GalleryListView({
required this.hugeListViewKey,
required this.itemScroller,
required this.collatedFiles,
required this.disableScroll,
this.header,
this.footer,
required this.emptyState,
required this.asyncLoader,
this.reloadEvent,
required this.removalEventTypes,
required this.tagPrefix,
required this.scrollBottomSafeArea,
required this.limitSelectionToOne,
this.selectedFiles,
required this.shouldCollateFilesByDay,
required this.logTag,
required this.logger,
super.key,
});
@override
Widget build(BuildContext context) {
2021-04-21 13:09:18 +00:00
return HugeListView<List<File>>(
key: hugeListViewKey,
controller: itemScroller,
2021-09-23 11:09:56 +00:00
startIndex: 0,
totalCount: collatedFiles.length,
isDraggableScrollbarEnabled: collatedFiles.length > 10,
disableScroll: disableScroll,
2021-04-21 13:09:18 +00:00
waitBuilder: (_) {
return const EnteLoadingWidget();
2021-04-21 13:09:18 +00:00
},
emptyResultBuilder: (_) {
2022-08-29 14:43:31 +00:00
final List<Widget> children = [];
if (header != null) {
children.add(header!);
}
2022-06-11 08:23:52 +00:00
children.add(
Expanded(
child: emptyState,
2022-06-11 08:23:52 +00:00
),
);
if (footer != null) {
children.add(footer!);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
);
2021-04-21 13:09:18 +00:00
},
itemBuilder: (context, index) {
2021-09-23 07:31:01 +00:00
Widget gallery;
2021-04-21 13:09:18 +00:00
gallery = LazyLoadingGallery(
collatedFiles[index],
index,
reloadEvent,
removalEventTypes,
asyncLoader,
selectedFiles,
tagPrefix,
Bus.instance
.on<GalleryIndexUpdatedEvent>()
.where((event) => event.tag == tagPrefix)
.map((event) => event.index),
shouldCollateFilesByDay,
logTag: logTag,
2023-05-03 09:52:06 +00:00
photoGridSize: LocalSettings.instance.getPhotoGridSize(),
limitSelectionToOne: limitSelectionToOne,
2021-04-23 19:55:50 +00:00
);
if (header != null && index == 0) {
gallery = Column(children: [header!, gallery]);
2021-04-21 13:09:18 +00:00
}
if (footer != null && index == collatedFiles.length - 1) {
gallery = Column(children: [gallery, footer!]);
2021-05-12 21:12:18 +00:00
}
2021-04-21 13:09:18 +00:00
return gallery;
},
labelTextBuilder: (int index) {
2023-01-09 10:49:48 +00:00
try {
return getMonthAndYear(
DateTime.fromMicrosecondsSinceEpoch(
collatedFiles[index][0].creationTime!,
2023-01-09 10:49:48 +00:00
),
);
} catch (e) {
logger.severe("label text builder failed", e);
2023-01-09 10:49:48 +00:00
return "";
}
2021-04-21 13:09:18 +00:00
},
2022-07-03 09:45:00 +00:00
thumbBackgroundColor:
Theme.of(context).colorScheme.galleryThumbBackgroundColor,
2022-06-07 08:03:35 +00:00
thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor,
thumbPadding: header != null
? const EdgeInsets.only(top: 60)
: const EdgeInsets.all(0),
bottomSafeArea: scrollBottomSafeArea,
firstShown: (int firstIndex) {
Bus.instance.fire(GalleryIndexUpdatedEvent(tagPrefix, firstIndex));
},
2021-04-21 13:09:18 +00:00
);
}
}