Refactor gallery (#1074)
This commit is contained in:
commit
c77afae652
8 changed files with 955 additions and 663 deletions
|
@ -1,573 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_extension/media_extension.dart';
|
||||
import 'package:media_extension/media_extension_action_types.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/clear_selections_event.dart';
|
||||
import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/extensions/string_ext.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/huge_listview/place_holder_widget.dart';
|
||||
import 'package:photos/ui/viewer/file/detail_page.dart';
|
||||
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
|
||||
import "package:photos/ui/viewer/gallery/component/day_widget.dart";
|
||||
import 'package:photos/ui/viewer/gallery/gallery.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class LazyLoadingGallery extends StatefulWidget {
|
||||
final List<File> files;
|
||||
final int index;
|
||||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tag;
|
||||
final String? logTag;
|
||||
final Stream<int> currentIndexStream;
|
||||
final int photoGirdSize;
|
||||
final bool areFilesCollatedByDay;
|
||||
final bool limitSelectionToOne;
|
||||
LazyLoadingGallery(
|
||||
this.files,
|
||||
this.index,
|
||||
this.reloadEvent,
|
||||
this.removalEventTypes,
|
||||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.tag,
|
||||
this.currentIndexStream,
|
||||
this.areFilesCollatedByDay, {
|
||||
this.logTag = "",
|
||||
this.photoGirdSize = photoGridSizeDefault,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
@override
|
||||
State<LazyLoadingGallery> createState() => _LazyLoadingGalleryState();
|
||||
}
|
||||
|
||||
class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
||||
static const kRecycleLimit = 400;
|
||||
static const kNumberOfDaysToRenderBeforeAndAfter = 8;
|
||||
|
||||
late Logger _logger;
|
||||
|
||||
late List<File> _files;
|
||||
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
|
||||
late StreamSubscription<int> _currentIndexSubscription;
|
||||
bool? _shouldRender;
|
||||
final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _showSelectAllButton = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _areAllFromDaySelected = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
_logger = Logger("LazyLoading_${widget.logTag}");
|
||||
_shouldRender = true;
|
||||
_files = widget.files;
|
||||
_reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
|
||||
|
||||
_currentIndexSubscription =
|
||||
widget.currentIndexStream.listen((currentIndex) {
|
||||
final bool shouldRender = (currentIndex - widget.index).abs() <
|
||||
kNumberOfDaysToRenderBeforeAndAfter;
|
||||
if (mounted && shouldRender != _shouldRender) {
|
||||
setState(() {
|
||||
_shouldRender = shouldRender;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future _onReload(FilesUpdatedEvent event) async {
|
||||
final galleryDate =
|
||||
DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
|
||||
final filesUpdatedThisDay = event.updatedFiles.where((file) {
|
||||
final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
|
||||
return fileDate.year == galleryDate.year &&
|
||||
fileDate.month == galleryDate.month &&
|
||||
fileDate.day == galleryDate.day;
|
||||
});
|
||||
if (filesUpdatedThisDay.isNotEmpty) {
|
||||
if (kDebugMode) {
|
||||
_logger.info(
|
||||
filesUpdatedThisDay.length.toString() +
|
||||
" files were updated due to ${event.reason} on " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(
|
||||
galleryDate.microsecondsSinceEpoch,
|
||||
).toIso8601String(),
|
||||
);
|
||||
}
|
||||
if (event.type == EventType.addedOrUpdated) {
|
||||
final dayStartTime =
|
||||
DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
|
||||
final result = await widget.asyncLoader(
|
||||
dayStartTime.microsecondsSinceEpoch,
|
||||
dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files = result.files;
|
||||
});
|
||||
}
|
||||
} else if (widget.removalEventTypes.contains(event.type)) {
|
||||
// Files were removed
|
||||
final generatedFileIDs = <int?>{};
|
||||
final uploadedFileIds = <int?>{};
|
||||
for (final file in filesUpdatedThisDay) {
|
||||
if (file.generatedID != null) {
|
||||
generatedFileIDs.add(file.generatedID);
|
||||
} else if (file.uploadedFileID != null) {
|
||||
uploadedFileIds.add(file.uploadedFileID);
|
||||
}
|
||||
}
|
||||
final List<File> files = [];
|
||||
files.addAll(_files);
|
||||
files.removeWhere(
|
||||
(file) =>
|
||||
generatedFileIDs.contains(file.generatedID) ||
|
||||
uploadedFileIds.contains(file.uploadedFileID),
|
||||
);
|
||||
if (kDebugMode) {
|
||||
_logger.finest(
|
||||
"removed ${_files.length - files.length} due to ${event.reason}",
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files = files;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
debugPrint("Unexpected event ${event.type.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reloadEventSubscription?.cancel();
|
||||
_currentIndexSubscription.cancel();
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_toggleSelectAllFromDay.dispose();
|
||||
_showSelectAllButton.dispose();
|
||||
_areAllFromDaySelected.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LazyLoadingGallery oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(_files, widget.files)) {
|
||||
_reloadEventSubscription?.cancel();
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_files.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (widget.areFilesCollatedByDay)
|
||||
DayWidget(
|
||||
timestamp: _files[0].creationTime!,
|
||||
gridSize: widget.photoGirdSize,
|
||||
),
|
||||
widget.limitSelectionToOne
|
||||
? const SizedBox.shrink()
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: _showSelectAllButton,
|
||||
builder: (context, dynamic value, _) {
|
||||
return !value
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 44,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _areAllFromDaySelected,
|
||||
builder: (context, dynamic value, _) {
|
||||
return value
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context)
|
||||
.strokeMuted,
|
||||
size: 18,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
//this value has no significance
|
||||
//changing only to notify the listeners
|
||||
_toggleSelectAllFromDay.value =
|
||||
!_toggleSelectAllFromDay.value;
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_shouldRender!
|
||||
? _getGallery()
|
||||
: PlaceHolderWidget(
|
||||
_files.length,
|
||||
widget.photoGirdSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getGallery() {
|
||||
final List<Widget> childGalleries = [];
|
||||
final subGalleryItemLimit = widget.photoGirdSize * subGalleryMultiplier;
|
||||
for (int index = 0; index < _files.length; index += subGalleryItemLimit) {
|
||||
childGalleries.add(
|
||||
LazyLoadingGridView(
|
||||
widget.tag,
|
||||
_files.sublist(
|
||||
index,
|
||||
min(index + subGalleryItemLimit, _files.length),
|
||||
),
|
||||
widget.asyncLoader,
|
||||
widget.selectedFiles,
|
||||
index == 0,
|
||||
_files.length > kRecycleLimit,
|
||||
_toggleSelectAllFromDay,
|
||||
_areAllFromDaySelected,
|
||||
widget.photoGirdSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: childGalleries,
|
||||
);
|
||||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles!.files.isEmpty) {
|
||||
_showSelectAllButton.value = false;
|
||||
} else {
|
||||
_showSelectAllButton.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LazyLoadingGridView extends StatefulWidget {
|
||||
final String tag;
|
||||
final List<File> filesInDay;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool shouldRender;
|
||||
final bool shouldRecycle;
|
||||
final ValueNotifier toggleSelectAllFromDay;
|
||||
final ValueNotifier areAllFilesSelected;
|
||||
final int? photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
LazyLoadingGridView(
|
||||
this.tag,
|
||||
this.filesInDay,
|
||||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.shouldRender,
|
||||
this.shouldRecycle,
|
||||
this.toggleSelectAllFromDay,
|
||||
this.areAllFilesSelected,
|
||||
this.photoGridSize, {
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
@override
|
||||
State<LazyLoadingGridView> createState() => _LazyLoadingGridViewState();
|
||||
}
|
||||
|
||||
class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
||||
bool? _shouldRender;
|
||||
int? _currentUserID;
|
||||
late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
_currentUserID = Configuration.instance.getUserID();
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent =
|
||||
Bus.instance.on<ClearSelectionsEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
widget.toggleSelectAllFromDay.addListener(_toggleSelectAllFromDayListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent.cancel();
|
||||
widget.toggleSelectAllFromDay
|
||||
.removeListener(_toggleSelectAllFromDayListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LazyLoadingGridView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(widget.filesInDay, oldWidget.filesInDay)) {
|
||||
_shouldRender = widget.shouldRender;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.shouldRecycle) {
|
||||
return _getRecyclableView();
|
||||
} else {
|
||||
return _getNonRecyclableView();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getRecyclableView() {
|
||||
return VisibilityDetector(
|
||||
key: Key("gallery" + widget.filesInDay.first.tag),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final shouldRender = visibility.visibleFraction > 0;
|
||||
if (mounted && shouldRender != _shouldRender) {
|
||||
setState(() {
|
||||
_shouldRender = shouldRender;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: _shouldRender!
|
||||
? _getGridView()
|
||||
: PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getNonRecyclableView() {
|
||||
if (!_shouldRender!) {
|
||||
return VisibilityDetector(
|
||||
key: Key("gallery" + widget.filesInDay.first.tag),
|
||||
onVisibilityChanged: (visibility) {
|
||||
if (mounted && visibility.visibleFraction > 0 && !_shouldRender!) {
|
||||
setState(() {
|
||||
_shouldRender = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child:
|
||||
PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize!),
|
||||
);
|
||||
} else {
|
||||
return _getGridView();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getGridView() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// to disable GridView's scrolling
|
||||
itemBuilder: (context, index) {
|
||||
return _buildFile(context, widget.filesInDay[index]);
|
||||
},
|
||||
itemCount: widget.filesInDay.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisSpacing: 2,
|
||||
mainAxisSpacing: 2,
|
||||
crossAxisCount: widget.photoGridSize!,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFile(BuildContext context, File file) {
|
||||
final isFileSelected = widget.selectedFiles?.isFileSelected(file) ?? false;
|
||||
Color selectionColor = Colors.white;
|
||||
if (isFileSelected &&
|
||||
file.isUploaded &&
|
||||
(file.ownerID != _currentUserID ||
|
||||
file.pubMagicMetadata!.uploaderName != null)) {
|
||||
final avatarColors = getEnteColorScheme(context).avatarColors;
|
||||
final int randomID = file.ownerID != _currentUserID
|
||||
? file.ownerID!
|
||||
: file.pubMagicMetadata!.uploaderName.sumAsciiValues;
|
||||
selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.limitSelectionToOne
|
||||
? _onTapWithSelectionLimit(file)
|
||||
: _onTapNoSelectionLimit(file);
|
||||
},
|
||||
onLongPress: () {
|
||||
widget.limitSelectionToOne
|
||||
? _onLongPressWithSelectionLimit(file)
|
||||
: _onLongPressNoSelectionLimit(file);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: widget.tag + file.tag,
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(
|
||||
isFileSelected ? 0.4 : 0,
|
||||
),
|
||||
BlendMode.darken,
|
||||
),
|
||||
child: ThumbnailWidget(
|
||||
file,
|
||||
diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
|
||||
serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
|
||||
shouldShowLivePhotoOverlay: true,
|
||||
key: Key(widget.tag + file.tag),
|
||||
thumbnailSize: widget.photoGridSize! < photoGridSizeDefault
|
||||
? thumbnailLargeSize
|
||||
: thumbnailSmallSize,
|
||||
shouldShowOwnerAvatar: !isFileSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isFileSelected,
|
||||
child: Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 20,
|
||||
color: selectionColor, //same for both themes
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleFileSelection(File file) {
|
||||
widget.selectedFiles!.toggleSelection(file);
|
||||
}
|
||||
|
||||
void _onTapNoSelectionLimit(File file) async {
|
||||
if (widget.selectedFiles?.files.isNotEmpty ?? false) {
|
||||
_toggleFileSelection(file);
|
||||
} else {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapWithSelectionLimit(File file) {
|
||||
if (widget.selectedFiles!.files.isNotEmpty &&
|
||||
widget.selectedFiles!.files.first != file) {
|
||||
widget.selectedFiles!.clearAll();
|
||||
}
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
|
||||
void _onLongPressNoSelectionLimit(File file) {
|
||||
if (widget.selectedFiles!.files.isNotEmpty) {
|
||||
_routeToDetailPage(file, context);
|
||||
} else if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLongPressWithSelectionLimit(File file) async {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
|
||||
void _routeToDetailPage(File file, BuildContext context) {
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(
|
||||
List.unmodifiable(widget.filesInDay),
|
||||
widget.asyncLoader,
|
||||
widget.filesInDay.indexOf(file),
|
||||
widget.tag,
|
||||
),
|
||||
);
|
||||
routeToPage(context, page, forceCustomPageRoute: true);
|
||||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
widget.areAllFilesSelected.value = true;
|
||||
} else {
|
||||
widget.areAllFilesSelected.value = false;
|
||||
}
|
||||
bool shouldRefresh = false;
|
||||
for (final file in widget.filesInDay) {
|
||||
if (widget.selectedFiles!.isPartOfLastSelected(file)) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
if (shouldRefresh && mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleSelectAllFromDayListener() {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
setState(() {
|
||||
widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
|
||||
});
|
||||
} else {
|
||||
widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
|
||||
}
|
||||
}
|
||||
}
|
166
lib/ui/viewer/gallery/component/gallery_file_widget.dart
Normal file
166
lib/ui/viewer/gallery/component/gallery_file_widget.dart
Normal file
|
@ -0,0 +1,166 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:media_extension/media_extension.dart";
|
||||
import "package:media_extension/media_extension_action_types.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/extensions/string_ext.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/services/app_lifecycle_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/viewer/file/detail_page.dart";
|
||||
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class GalleryFileWidget extends StatelessWidget {
|
||||
final File file;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool limitSelectionToOne;
|
||||
final String tag;
|
||||
final int photoGridSize;
|
||||
final int? currentUserID;
|
||||
final List<File> filesInDay;
|
||||
final GalleryLoader asyncLoader;
|
||||
const GalleryFileWidget({
|
||||
required this.file,
|
||||
required this.selectedFiles,
|
||||
required this.limitSelectionToOne,
|
||||
required this.tag,
|
||||
required this.photoGridSize,
|
||||
required this.currentUserID,
|
||||
required this.filesInDay,
|
||||
required this.asyncLoader,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFileSelected = selectedFiles?.isFileSelected(file) ?? false;
|
||||
Color selectionColor = Colors.white;
|
||||
if (isFileSelected &&
|
||||
file.isUploaded &&
|
||||
(file.ownerID != currentUserID ||
|
||||
file.pubMagicMetadata!.uploaderName != null)) {
|
||||
final avatarColors = getEnteColorScheme(context).avatarColors;
|
||||
final int randomID = file.ownerID != currentUserID
|
||||
? file.ownerID!
|
||||
: file.pubMagicMetadata!.uploaderName.sumAsciiValues;
|
||||
selectionColor = avatarColors[(randomID).remainder(avatarColors.length)];
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
limitSelectionToOne
|
||||
? _onTapWithSelectionLimit(file)
|
||||
: _onTapNoSelectionLimit(context, file);
|
||||
},
|
||||
onLongPress: () {
|
||||
limitSelectionToOne
|
||||
? _onLongPressWithSelectionLimit(context, file)
|
||||
: _onLongPressNoSelectionLimit(context, file);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: tag + file.tag,
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(
|
||||
isFileSelected ? 0.4 : 0,
|
||||
),
|
||||
BlendMode.darken,
|
||||
),
|
||||
child: ThumbnailWidget(
|
||||
file,
|
||||
diskLoadDeferDuration: thumbnailDiskLoadDeferDuration,
|
||||
serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
|
||||
shouldShowLivePhotoOverlay: true,
|
||||
key: Key(tag + file.tag),
|
||||
thumbnailSize: photoGridSize < photoGridSizeDefault
|
||||
? thumbnailLargeSize
|
||||
: thumbnailSmallSize,
|
||||
shouldShowOwnerAvatar: !isFileSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isFileSelected,
|
||||
child: Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 20,
|
||||
color: selectionColor, //same for both themes
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleFileSelection(File file) {
|
||||
selectedFiles!.toggleSelection(file);
|
||||
}
|
||||
|
||||
void _onTapWithSelectionLimit(File file) {
|
||||
if (selectedFiles!.files.isNotEmpty && selectedFiles!.files.first != file) {
|
||||
selectedFiles!.clearAll();
|
||||
}
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
|
||||
void _onTapNoSelectionLimit(BuildContext context, File file) async {
|
||||
if (selectedFiles?.files.isNotEmpty ?? false) {
|
||||
_toggleFileSelection(file);
|
||||
} else {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressNoSelectionLimit(BuildContext context, File file) {
|
||||
if (selectedFiles!.files.isNotEmpty) {
|
||||
_routeToDetailPage(file, context);
|
||||
} else if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.main) {
|
||||
HapticFeedback.lightImpact();
|
||||
_toggleFileSelection(file);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLongPressWithSelectionLimit(
|
||||
BuildContext context,
|
||||
File file,
|
||||
) async {
|
||||
if (AppLifecycleService.instance.mediaExtensionAction.action ==
|
||||
IntentAction.pick) {
|
||||
final ioFile = await getFile(file);
|
||||
MediaExtension().setResult("file://${ioFile!.path}");
|
||||
} else {
|
||||
_routeToDetailPage(file, context);
|
||||
}
|
||||
}
|
||||
|
||||
void _routeToDetailPage(File file, BuildContext context) {
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(
|
||||
List.unmodifiable(filesInDay),
|
||||
asyncLoader,
|
||||
filesInDay.indexOf(file),
|
||||
tag,
|
||||
),
|
||||
);
|
||||
routeToPage(context, page, forceCustomPageRoute: true);
|
||||
}
|
||||
}
|
137
lib/ui/viewer/gallery/component/gallery_list_view_widget.dart
Normal file
137
lib/ui/viewer/gallery/component/gallery_list_view_widget.dart
Normal file
|
@ -0,0 +1,137 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/ente_theme_data.dart";
|
||||
import "package:photos/events/files_updated_event.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/huge_listview/huge_listview.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/utils/date_time_util.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import "package:scrollable_positioned_list/scrollable_positioned_list.dart";
|
||||
|
||||
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) {
|
||||
return HugeListView<List<File>>(
|
||||
key: hugeListViewKey,
|
||||
controller: itemScroller,
|
||||
startIndex: 0,
|
||||
totalCount: collatedFiles.length,
|
||||
isDraggableScrollbarEnabled: collatedFiles.length > 10,
|
||||
disableScroll: disableScroll,
|
||||
waitBuilder: (_) {
|
||||
return const EnteLoadingWidget();
|
||||
},
|
||||
emptyResultBuilder: (_) {
|
||||
final List<Widget> children = [];
|
||||
if (header != null) {
|
||||
children.add(header!);
|
||||
}
|
||||
children.add(
|
||||
Expanded(
|
||||
child: emptyState,
|
||||
),
|
||||
);
|
||||
if (footer != null) {
|
||||
children.add(footer!);
|
||||
}
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
Widget gallery;
|
||||
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,
|
||||
photoGridSize: LocalSettings.instance.getPhotoGridSize(),
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
);
|
||||
if (header != null && index == 0) {
|
||||
gallery = Column(children: [header!, gallery]);
|
||||
}
|
||||
if (footer != null && index == collatedFiles.length - 1) {
|
||||
gallery = Column(children: [gallery, footer!]);
|
||||
}
|
||||
return gallery;
|
||||
},
|
||||
labelTextBuilder: (int index) {
|
||||
try {
|
||||
return getMonthAndYear(
|
||||
DateTime.fromMicrosecondsSinceEpoch(
|
||||
collatedFiles[index][0].creationTime!,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
logger.severe("label text builder failed", e);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
thumbBackgroundColor:
|
||||
Theme.of(context).colorScheme.galleryThumbBackgroundColor,
|
||||
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));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
359
lib/ui/viewer/gallery/component/lazy_loading_gallery.dart
Normal file
359
lib/ui/viewer/gallery/component/lazy_loading_gallery.dart
Normal file
|
@ -0,0 +1,359 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/huge_listview/place_holder_widget.dart';
|
||||
import "package:photos/ui/viewer/gallery/component/day_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/gallery_file_widget.dart";
|
||||
import 'package:photos/ui/viewer/gallery/component/lazy_loading_grid_view.dart';
|
||||
import 'package:photos/ui/viewer/gallery/gallery.dart';
|
||||
|
||||
class LazyLoadingGallery extends StatefulWidget {
|
||||
final List<File> files;
|
||||
final int index;
|
||||
final Stream<FilesUpdatedEvent>? reloadEvent;
|
||||
final Set<EventType> removalEventTypes;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final String tag;
|
||||
final String? logTag;
|
||||
final Stream<int> currentIndexStream;
|
||||
final int photoGridSize;
|
||||
final bool areFilesCollatedByDay;
|
||||
final bool limitSelectionToOne;
|
||||
LazyLoadingGallery(
|
||||
this.files,
|
||||
this.index,
|
||||
this.reloadEvent,
|
||||
this.removalEventTypes,
|
||||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.tag,
|
||||
this.currentIndexStream,
|
||||
this.areFilesCollatedByDay, {
|
||||
this.logTag = "",
|
||||
this.photoGridSize = photoGridSizeDefault,
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
@override
|
||||
State<LazyLoadingGallery> createState() => _LazyLoadingGalleryState();
|
||||
}
|
||||
|
||||
class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
||||
static const kNumberOfDaysToRenderBeforeAndAfter = 8;
|
||||
|
||||
late Logger _logger;
|
||||
|
||||
late List<File> _files;
|
||||
late StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
|
||||
late StreamSubscription<int> _currentIndexSubscription;
|
||||
bool? _shouldRender;
|
||||
final ValueNotifier<bool> _toggleSelectAllFromDay = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _showSelectAllButton = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _areAllFromDaySelected = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//this is for removing the 'select all from day' icon on unselecting all files with 'cancel'
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
_logger = Logger("LazyLoading_${widget.logTag}");
|
||||
_shouldRender = true;
|
||||
_files = widget.files;
|
||||
_reloadEventSubscription = widget.reloadEvent?.listen((e) => _onReload(e));
|
||||
|
||||
_currentIndexSubscription =
|
||||
widget.currentIndexStream.listen((currentIndex) {
|
||||
final bool shouldRender = (currentIndex - widget.index).abs() <
|
||||
kNumberOfDaysToRenderBeforeAndAfter;
|
||||
if (mounted && shouldRender != _shouldRender) {
|
||||
setState(() {
|
||||
_shouldRender = shouldRender;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future _onReload(FilesUpdatedEvent event) async {
|
||||
final galleryDate =
|
||||
DateTime.fromMicrosecondsSinceEpoch(_files[0].creationTime!);
|
||||
final filesUpdatedThisDay = event.updatedFiles.where((file) {
|
||||
final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
|
||||
return fileDate.year == galleryDate.year &&
|
||||
fileDate.month == galleryDate.month &&
|
||||
fileDate.day == galleryDate.day;
|
||||
});
|
||||
if (filesUpdatedThisDay.isNotEmpty) {
|
||||
if (kDebugMode) {
|
||||
_logger.info(
|
||||
filesUpdatedThisDay.length.toString() +
|
||||
" files were updated due to ${event.reason} on " +
|
||||
DateTime.fromMicrosecondsSinceEpoch(
|
||||
galleryDate.microsecondsSinceEpoch,
|
||||
).toIso8601String(),
|
||||
);
|
||||
}
|
||||
if (event.type == EventType.addedOrUpdated) {
|
||||
final dayStartTime =
|
||||
DateTime(galleryDate.year, galleryDate.month, galleryDate.day);
|
||||
final result = await widget.asyncLoader(
|
||||
dayStartTime.microsecondsSinceEpoch,
|
||||
dayStartTime.microsecondsSinceEpoch + microSecondsInDay - 1,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files = result.files;
|
||||
});
|
||||
}
|
||||
} else if (widget.removalEventTypes.contains(event.type)) {
|
||||
// Files were removed
|
||||
final generatedFileIDs = <int?>{};
|
||||
final uploadedFileIds = <int?>{};
|
||||
for (final file in filesUpdatedThisDay) {
|
||||
if (file.generatedID != null) {
|
||||
generatedFileIDs.add(file.generatedID);
|
||||
} else if (file.uploadedFileID != null) {
|
||||
uploadedFileIds.add(file.uploadedFileID);
|
||||
}
|
||||
}
|
||||
final List<File> files = [];
|
||||
files.addAll(_files);
|
||||
files.removeWhere(
|
||||
(file) =>
|
||||
generatedFileIDs.contains(file.generatedID) ||
|
||||
uploadedFileIds.contains(file.uploadedFileID),
|
||||
);
|
||||
if (kDebugMode) {
|
||||
_logger.finest(
|
||||
"removed ${_files.length - files.length} due to ${event.reason}",
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files = files;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
debugPrint("Unexpected event ${event.type.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reloadEventSubscription?.cancel();
|
||||
_currentIndexSubscription.cancel();
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_toggleSelectAllFromDay.dispose();
|
||||
_showSelectAllButton.dispose();
|
||||
_areAllFromDaySelected.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LazyLoadingGallery oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(_files, widget.files)) {
|
||||
_reloadEventSubscription?.cancel();
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_files.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (widget.areFilesCollatedByDay)
|
||||
DayWidget(
|
||||
timestamp: _files[0].creationTime!,
|
||||
gridSize: widget.photoGridSize,
|
||||
),
|
||||
widget.limitSelectionToOne
|
||||
? const SizedBox.shrink()
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: _showSelectAllButton,
|
||||
builder: (context, dynamic value, _) {
|
||||
return !value
|
||||
? const SizedBox.shrink()
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 44,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _areAllFromDaySelected,
|
||||
builder: (context, dynamic value, _) {
|
||||
return value
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context)
|
||||
.strokeMuted,
|
||||
size: 18,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
//this value has no significance
|
||||
//changing only to notify the listeners
|
||||
_toggleSelectAllFromDay.value =
|
||||
!_toggleSelectAllFromDay.value;
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
_shouldRender!
|
||||
? GetGallery(
|
||||
photoGridSize: widget.photoGridSize,
|
||||
files: _files,
|
||||
tag: widget.tag,
|
||||
asyncLoader: widget.asyncLoader,
|
||||
selectedFiles: widget.selectedFiles,
|
||||
toggleSelectAllFromDay: _toggleSelectAllFromDay,
|
||||
areAllFromDaySelected: _areAllFromDaySelected,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
)
|
||||
: PlaceHolderWidget(
|
||||
_files.length,
|
||||
widget.photoGridSize,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles!.files.isEmpty) {
|
||||
_showSelectAllButton.value = false;
|
||||
} else {
|
||||
_showSelectAllButton.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetGallery extends StatelessWidget {
|
||||
final int photoGridSize;
|
||||
final List<File> files;
|
||||
final String tag;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final ValueNotifier<bool> toggleSelectAllFromDay;
|
||||
final ValueNotifier<bool> areAllFromDaySelected;
|
||||
final bool limitSelectionToOne;
|
||||
const GetGallery({
|
||||
required this.photoGridSize,
|
||||
required this.files,
|
||||
required this.tag,
|
||||
required this.asyncLoader,
|
||||
required this.selectedFiles,
|
||||
required this.toggleSelectAllFromDay,
|
||||
required this.areAllFromDaySelected,
|
||||
required this.limitSelectionToOne,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const kRecycleLimit = 400;
|
||||
final List<Widget> childGalleries = [];
|
||||
final subGalleryItemLimit = photoGridSize * subGalleryMultiplier;
|
||||
|
||||
for (int index = 0; index < files.length; index += subGalleryItemLimit) {
|
||||
childGalleries.add(
|
||||
LazyLoadingGridView(
|
||||
tag,
|
||||
files.sublist(
|
||||
index,
|
||||
min(index + subGalleryItemLimit, files.length),
|
||||
),
|
||||
asyncLoader,
|
||||
selectedFiles,
|
||||
index == 0,
|
||||
files.length > kRecycleLimit,
|
||||
toggleSelectAllFromDay,
|
||||
areAllFromDaySelected,
|
||||
photoGridSize,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: childGalleries,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryGridViewWidget extends StatelessWidget {
|
||||
final List<File> filesInDay;
|
||||
final int photoGridSize;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool limitSelectionToOne;
|
||||
final String tag;
|
||||
final int? currentUserID;
|
||||
final GalleryLoader asyncLoader;
|
||||
const GalleryGridViewWidget({
|
||||
required this.filesInDay,
|
||||
required this.photoGridSize,
|
||||
this.selectedFiles,
|
||||
required this.limitSelectionToOne,
|
||||
required this.tag,
|
||||
super.key,
|
||||
this.currentUserID,
|
||||
required this.asyncLoader,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// to disable GridView's scrolling
|
||||
itemBuilder: (context, index) {
|
||||
return GalleryFileWidget(
|
||||
file: filesInDay[index],
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
tag: tag,
|
||||
photoGridSize: photoGridSize,
|
||||
currentUserID: currentUserID,
|
||||
filesInDay: filesInDay,
|
||||
asyncLoader: asyncLoader,
|
||||
);
|
||||
},
|
||||
itemCount: filesInDay.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisSpacing: 2,
|
||||
mainAxisSpacing: 2,
|
||||
crossAxisCount: photoGridSize,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: (galleryGridSpacing / 2)),
|
||||
);
|
||||
}
|
||||
}
|
134
lib/ui/viewer/gallery/component/lazy_loading_grid_view.dart
Normal file
134
lib/ui/viewer/gallery/component/lazy_loading_grid_view.dart
Normal file
|
@ -0,0 +1,134 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/clear_selections_event.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/non_recyclable_view_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/recyclable_view_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
|
||||
class LazyLoadingGridView extends StatefulWidget {
|
||||
final String tag;
|
||||
final List<File> filesInDay;
|
||||
final GalleryLoader asyncLoader;
|
||||
final SelectedFiles? selectedFiles;
|
||||
final bool shouldRender;
|
||||
final bool shouldRecycle;
|
||||
final ValueNotifier toggleSelectAllFromDay;
|
||||
final ValueNotifier areAllFilesSelected;
|
||||
final int? photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
|
||||
LazyLoadingGridView(
|
||||
this.tag,
|
||||
this.filesInDay,
|
||||
this.asyncLoader,
|
||||
this.selectedFiles,
|
||||
this.shouldRender,
|
||||
this.shouldRecycle,
|
||||
this.toggleSelectAllFromDay,
|
||||
this.areAllFilesSelected,
|
||||
this.photoGridSize, {
|
||||
this.limitSelectionToOne = false,
|
||||
Key? key,
|
||||
}) : super(key: key ?? UniqueKey());
|
||||
|
||||
@override
|
||||
State<LazyLoadingGridView> createState() => _LazyLoadingGridViewState();
|
||||
}
|
||||
|
||||
class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
|
||||
late bool _shouldRender;
|
||||
int? _currentUserID;
|
||||
late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
_currentUserID = Configuration.instance.getUserID();
|
||||
widget.selectedFiles?.addListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent =
|
||||
Bus.instance.on<ClearSelectionsEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
widget.toggleSelectAllFromDay.addListener(_toggleSelectAllFromDayListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectedFiles?.removeListener(_selectedFilesListener);
|
||||
_clearSelectionsEvent.cancel();
|
||||
widget.toggleSelectAllFromDay
|
||||
.removeListener(_toggleSelectAllFromDayListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LazyLoadingGridView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!listEquals(widget.filesInDay, oldWidget.filesInDay)) {
|
||||
_shouldRender = widget.shouldRender;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.shouldRecycle) {
|
||||
return RecyclableViewWidget(
|
||||
shouldRender: _shouldRender,
|
||||
filesInDay: widget.filesInDay,
|
||||
photoGridSize: widget.photoGridSize!,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
tag: widget.tag,
|
||||
asyncLoader: widget.asyncLoader,
|
||||
selectedFiles: widget.selectedFiles,
|
||||
currentUserID: _currentUserID,
|
||||
);
|
||||
} else {
|
||||
return NonRecyclableViewWidget(
|
||||
shouldRender: _shouldRender,
|
||||
filesInDay: widget.filesInDay,
|
||||
photoGridSize: widget.photoGridSize!,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
tag: widget.tag,
|
||||
asyncLoader: widget.asyncLoader,
|
||||
selectedFiles: widget.selectedFiles,
|
||||
currentUserID: _currentUserID,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _selectedFilesListener() {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
widget.areAllFilesSelected.value = true;
|
||||
} else {
|
||||
widget.areAllFilesSelected.value = false;
|
||||
}
|
||||
bool shouldRefresh = false;
|
||||
for (final file in widget.filesInDay) {
|
||||
if (widget.selectedFiles!.isPartOfLastSelected(file)) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
if (shouldRefresh && mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleSelectAllFromDayListener() {
|
||||
if (widget.selectedFiles!.files.containsAll(widget.filesInDay.toSet())) {
|
||||
setState(() {
|
||||
widget.selectedFiles!.unSelectAll(widget.filesInDay.toSet());
|
||||
});
|
||||
} else {
|
||||
widget.selectedFiles!.selectAll(widget.filesInDay.toSet());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/ui/huge_listview/place_holder_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:visibility_detector/visibility_detector.dart";
|
||||
|
||||
class NonRecyclableViewWidget extends StatefulWidget {
|
||||
final bool shouldRender;
|
||||
final List<File> filesInDay;
|
||||
final int photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
final String tag;
|
||||
final GalleryLoader asyncLoader;
|
||||
final int? currentUserID;
|
||||
final SelectedFiles? selectedFiles;
|
||||
const NonRecyclableViewWidget({
|
||||
required this.shouldRender,
|
||||
required this.filesInDay,
|
||||
required this.photoGridSize,
|
||||
required this.limitSelectionToOne,
|
||||
required this.tag,
|
||||
required this.asyncLoader,
|
||||
this.currentUserID,
|
||||
this.selectedFiles,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NonRecyclableViewWidget> createState() =>
|
||||
_NonRecyclableViewWidgetState();
|
||||
}
|
||||
|
||||
class _NonRecyclableViewWidgetState extends State<NonRecyclableViewWidget> {
|
||||
late bool _shouldRender;
|
||||
@override
|
||||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_shouldRender!) {
|
||||
return VisibilityDetector(
|
||||
key: Key("gallery" + widget.filesInDay.first.tag),
|
||||
onVisibilityChanged: (visibility) {
|
||||
if (mounted && visibility.visibleFraction > 0 && !_shouldRender) {
|
||||
setState(() {
|
||||
_shouldRender = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child:
|
||||
PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
|
||||
);
|
||||
} else {
|
||||
return GalleryGridViewWidget(
|
||||
filesInDay: widget.filesInDay,
|
||||
photoGridSize: widget.photoGridSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
tag: widget.tag,
|
||||
asyncLoader: widget.asyncLoader,
|
||||
selectedFiles: widget.selectedFiles,
|
||||
currentUserID: widget.currentUserID,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
67
lib/ui/viewer/gallery/component/recyclable_view_widget.dart
Normal file
67
lib/ui/viewer/gallery/component/recyclable_view_widget.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/ui/huge_listview/place_holder_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/component/lazy_loading_gallery.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:visibility_detector/visibility_detector.dart";
|
||||
|
||||
class RecyclableViewWidget extends StatefulWidget {
|
||||
final bool shouldRender;
|
||||
final List<File> filesInDay;
|
||||
final int photoGridSize;
|
||||
final bool limitSelectionToOne;
|
||||
final String tag;
|
||||
final GalleryLoader asyncLoader;
|
||||
final int? currentUserID;
|
||||
final SelectedFiles? selectedFiles;
|
||||
const RecyclableViewWidget({
|
||||
required this.shouldRender,
|
||||
required this.filesInDay,
|
||||
required this.photoGridSize,
|
||||
required this.limitSelectionToOne,
|
||||
required this.tag,
|
||||
required this.asyncLoader,
|
||||
this.currentUserID,
|
||||
this.selectedFiles,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecyclableViewWidget> createState() => _RecyclableViewWidgetState();
|
||||
}
|
||||
|
||||
class _RecyclableViewWidgetState extends State<RecyclableViewWidget> {
|
||||
late bool _shouldRender;
|
||||
@override
|
||||
void initState() {
|
||||
_shouldRender = widget.shouldRender;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VisibilityDetector(
|
||||
key: Key("gallery" + widget.filesInDay.first.tag),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final shouldRender = visibility.visibleFraction > 0;
|
||||
if (mounted && shouldRender != _shouldRender) {
|
||||
setState(() {
|
||||
_shouldRender = shouldRender;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: _shouldRender
|
||||
? GalleryGridViewWidget(
|
||||
filesInDay: widget.filesInDay,
|
||||
photoGridSize: widget.photoGridSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
tag: widget.tag,
|
||||
asyncLoader: widget.asyncLoader,
|
||||
selectedFiles: widget.selectedFiles,
|
||||
currentUserID: widget.currentUserID,
|
||||
)
|
||||
: PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/events/event.dart';
|
||||
import 'package:photos/events/files_updated_event.dart';
|
||||
import 'package:photos/events/tab_changed_event.dart';
|
||||
|
@ -14,10 +13,9 @@ import 'package:photos/models/file_load_result.dart';
|
|||
import 'package:photos/models/selected_files.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/huge_listview/huge_listview.dart';
|
||||
import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart';
|
||||
import "package:photos/ui/viewer/gallery/component/gallery_list_view_widget.dart";
|
||||
import 'package:photos/ui/viewer/gallery/empty_state.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
typedef GalleryLoader = Future<FileLoadResult> Function(
|
||||
|
@ -79,18 +77,17 @@ class _GalleryState extends State<Gallery> {
|
|||
late Logger _logger;
|
||||
List<List<File>> _collatedFiles = [];
|
||||
bool _hasLoadedFiles = false;
|
||||
ItemScrollController? _itemScroller;
|
||||
late ItemScrollController _itemScroller;
|
||||
StreamSubscription<FilesUpdatedEvent>? _reloadEventSubscription;
|
||||
StreamSubscription<TabDoubleTapEvent>? _tabDoubleTapEvent;
|
||||
final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
|
||||
String? _logTag;
|
||||
late int _photoGridSize;
|
||||
late String _logTag;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_logTag =
|
||||
"Gallery_${widget.tagPrefix}${kDebugMode ? "_" + widget.albumName! : ""}";
|
||||
_logger = Logger(_logTag!);
|
||||
_logger = Logger(_logTag);
|
||||
_logger.finest("init Gallery");
|
||||
_itemScroller = ItemScrollController();
|
||||
if (widget.reloadEvent != null) {
|
||||
|
@ -112,7 +109,7 @@ class _GalleryState extends State<Gallery> {
|
|||
// todo: Assign ID to Gallery and fire generic event with ID &
|
||||
// target index/date
|
||||
if (mounted && event.selectedIndex == 0) {
|
||||
_itemScroller!.scrollTo(
|
||||
_itemScroller.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
|
@ -209,89 +206,24 @@ class _GalleryState extends State<Gallery> {
|
|||
if (!_hasLoadedFiles) {
|
||||
return widget.loadingWidget;
|
||||
}
|
||||
_photoGridSize = LocalSettings.instance.getPhotoGridSize();
|
||||
return _getListView();
|
||||
}
|
||||
|
||||
Widget _getListView() {
|
||||
return HugeListView<List<File>>(
|
||||
key: _hugeListViewKey,
|
||||
controller: _itemScroller,
|
||||
startIndex: 0,
|
||||
totalCount: _collatedFiles.length,
|
||||
isDraggableScrollbarEnabled: _collatedFiles.length > 10,
|
||||
return GalleryListView(
|
||||
hugeListViewKey: _hugeListViewKey,
|
||||
itemScroller: _itemScroller,
|
||||
collatedFiles: _collatedFiles,
|
||||
disableScroll: widget.disableScroll,
|
||||
waitBuilder: (_) {
|
||||
return const EnteLoadingWidget();
|
||||
},
|
||||
emptyResultBuilder: (_) {
|
||||
final List<Widget> children = [];
|
||||
if (widget.header != null) {
|
||||
children.add(widget.header!);
|
||||
}
|
||||
children.add(
|
||||
Expanded(
|
||||
child: widget.emptyState,
|
||||
),
|
||||
);
|
||||
if (widget.footer != null) {
|
||||
children.add(widget.footer!);
|
||||
}
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
Widget gallery;
|
||||
gallery = LazyLoadingGallery(
|
||||
_collatedFiles[index],
|
||||
index,
|
||||
widget.reloadEvent,
|
||||
widget.removalEventTypes,
|
||||
widget.asyncLoader,
|
||||
widget.selectedFiles,
|
||||
widget.tagPrefix,
|
||||
Bus.instance
|
||||
.on<GalleryIndexUpdatedEvent>()
|
||||
.where((event) => event.tag == widget.tagPrefix)
|
||||
.map((event) => event.index),
|
||||
widget.shouldCollateFilesByDay,
|
||||
logTag: _logTag,
|
||||
photoGirdSize: _photoGridSize,
|
||||
limitSelectionToOne: widget.limitSelectionToOne,
|
||||
);
|
||||
if (widget.header != null && index == 0) {
|
||||
gallery = Column(children: [widget.header!, gallery]);
|
||||
}
|
||||
if (widget.footer != null && index == _collatedFiles.length - 1) {
|
||||
gallery = Column(children: [gallery, widget.footer!]);
|
||||
}
|
||||
return gallery;
|
||||
},
|
||||
labelTextBuilder: (int index) {
|
||||
try {
|
||||
return getMonthAndYear(
|
||||
DateTime.fromMicrosecondsSinceEpoch(
|
||||
_collatedFiles[index][0].creationTime!,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe("label text builder failed", e);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
thumbBackgroundColor:
|
||||
Theme.of(context).colorScheme.galleryThumbBackgroundColor,
|
||||
thumbDrawColor: Theme.of(context).colorScheme.galleryThumbDrawColor,
|
||||
thumbPadding: widget.header != null
|
||||
? const EdgeInsets.only(top: 60)
|
||||
: const EdgeInsets.all(0),
|
||||
bottomSafeArea: widget.scrollBottomSafeArea,
|
||||
firstShown: (int firstIndex) {
|
||||
Bus.instance
|
||||
.fire(GalleryIndexUpdatedEvent(widget.tagPrefix, firstIndex));
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue