mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Rewrite HomePhotos
This commit is contained in:
parent
378da23360
commit
4950bcef8f
15 changed files with 1794 additions and 30 deletions
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/single_child_widget.dart';
|
||||
|
||||
mixin BlocLogger {
|
||||
String? get tag => null;
|
||||
|
@ -6,6 +8,28 @@ mixin BlocLogger {
|
|||
bool Function(dynamic currentState, dynamic nextState)? get shouldLog => null;
|
||||
}
|
||||
|
||||
class BlocListenerT<B extends StateStreamable<S>, S, T>
|
||||
extends SingleChildStatelessWidget {
|
||||
const BlocListenerT({
|
||||
super.key,
|
||||
required this.selector,
|
||||
required this.listener,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget buildWithChild(BuildContext context, Widget? child) {
|
||||
return BlocListener<B, S>(
|
||||
listenWhen: (previous, current) =>
|
||||
selector(previous) != selector(current),
|
||||
listener: (context, state) => listener(context, selector(state)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final BlocWidgetSelector<S, T> selector;
|
||||
final void Function(BuildContext context, T state) listener;
|
||||
}
|
||||
|
||||
/// Wrap around a string such that two strings with the same value will fail
|
||||
/// the identical check
|
||||
class StateMessage {
|
||||
|
@ -25,3 +49,11 @@ extension EmitterExtension<State> on Emitter<State> {
|
|||
onError: (_, __) {},
|
||||
);
|
||||
}
|
||||
|
||||
extension BlocExtension<E, S> on Bloc<E, S> {
|
||||
void safeAdd(E event) {
|
||||
if (!isClosed) {
|
||||
add(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,15 @@ class PrefController {
|
|||
value: value,
|
||||
);
|
||||
|
||||
ValueStream<int> get homePhotosZoomLevel =>
|
||||
_homePhotosZoomLevelController.stream;
|
||||
|
||||
Future<void> setHomePhotosZoomLevel(int value) => _set<int>(
|
||||
controller: _homePhotosZoomLevelController,
|
||||
setter: (pref, value) => pref.setHomePhotosZoomLevel(value),
|
||||
value: value,
|
||||
);
|
||||
|
||||
ValueStream<int> get albumBrowserZoomLevel =>
|
||||
_albumBrowserZoomLevelController.stream;
|
||||
|
||||
|
@ -237,6 +246,8 @@ class PrefController {
|
|||
final DiContainer _c;
|
||||
late final _languageController =
|
||||
BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0)));
|
||||
late final _homePhotosZoomLevelController =
|
||||
BehaviorSubject.seeded(_c.pref.getHomePhotosZoomLevelOr(0));
|
||||
late final _albumBrowserZoomLevelController =
|
||||
BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0));
|
||||
late final _homeAlbumsSortController =
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:nc_photos/theme/dimension.dart';
|
|||
import 'package:nc_photos/use_case/import_potential_shared_album.dart';
|
||||
import 'package:nc_photos/widget/home_collections.dart';
|
||||
import 'package:nc_photos/widget/home_photos.dart';
|
||||
import 'package:nc_photos/widget/home_photos2.dart';
|
||||
import 'package:nc_photos/widget/home_search.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_common/or_null.dart';
|
||||
|
@ -141,9 +142,7 @@ class _HomeState extends State<Home> with TickerProviderStateMixin {
|
|||
Widget _buildPage(BuildContext context, int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return HomePhotos(
|
||||
account: widget.account,
|
||||
);
|
||||
return const HomePhotos2();
|
||||
|
||||
case 1:
|
||||
return HomeSearch(
|
||||
|
|
124
app/lib/widget/home_photos/app_bar.dart
Normal file
124
app/lib/widget/home_photos/app_bar.dart
Normal file
|
@ -0,0 +1,124 @@
|
|||
part of '../home_photos2.dart';
|
||||
|
||||
class _AppBar extends StatelessWidget {
|
||||
const _AppBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) => previous.isLoading != current.isLoading,
|
||||
builder: (context, state) => HomeSliverAppBar(
|
||||
account: context.bloc.account,
|
||||
isShowProgressIcon: state.isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _SelectionAppBar extends StatelessWidget {
|
||||
const _SelectionAppBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.selectedItems != current.selectedItems,
|
||||
builder: (context, state) => SelectionAppBar(
|
||||
count: state.selectedItems.length,
|
||||
onClosePressed: () {
|
||||
context.addEvent(const _SetSelectedItems(items: {}));
|
||||
},
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share_outlined),
|
||||
tooltip: L10n.global().shareTooltip,
|
||||
onPressed: () => _onSharePressed(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: L10n.global().addItemToCollectionTooltip,
|
||||
onPressed: () => _onAddPressed(context),
|
||||
),
|
||||
const _SelectionAppBarMenu(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAddPressed(BuildContext context) async {
|
||||
final collection = await Navigator.of(context)
|
||||
.pushNamed<Collection>(CollectionPicker.routeName);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
context.bloc.add(_AddSelectedItemsToCollection(collection));
|
||||
}
|
||||
|
||||
Future<void> _onSharePressed(BuildContext context) async {
|
||||
final bloc = context.read<_Bloc>();
|
||||
final selected = bloc.state.selectedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
if (selected.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().shareSelectedEmptyNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
final result = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => FileSharerDialog(
|
||||
account: bloc.account,
|
||||
files: selected,
|
||||
),
|
||||
);
|
||||
if (result ?? false) {
|
||||
bloc.add(const _SetSelectedItems(items: {}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _SelectionAppBarMenu extends StatelessWidget {
|
||||
const _SelectionAppBarMenu();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<_SelectionMenuOption>(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _SelectionMenuOption.download,
|
||||
child: Text(L10n.global().downloadTooltip),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SelectionMenuOption.archive,
|
||||
child: Text(L10n.global().archiveTooltip),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SelectionMenuOption.delete,
|
||||
child: Text(L10n.global().deleteTooltip),
|
||||
),
|
||||
],
|
||||
onSelected: (option) {
|
||||
switch (option) {
|
||||
case _SelectionMenuOption.archive:
|
||||
context.addEvent(const _ArchiveSelectedItems());
|
||||
break;
|
||||
case _SelectionMenuOption.delete:
|
||||
context.addEvent(const _DeleteSelectedItems());
|
||||
break;
|
||||
case _SelectionMenuOption.download:
|
||||
context.addEvent(const _DownloadSelectedItems());
|
||||
break;
|
||||
default:
|
||||
_log.shout("[build] Unknown option: $option");
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
334
app/lib/widget/home_photos/bloc.dart
Normal file
334
app/lib/widget/home_photos/bloc.dart
Normal file
|
@ -0,0 +1,334 @@
|
|||
part of '../home_photos2.dart';
|
||||
|
||||
@npLog
|
||||
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||
_Bloc(
|
||||
this._c, {
|
||||
required this.account,
|
||||
required this.controller,
|
||||
required this.prefController,
|
||||
required this.accountPrefController,
|
||||
required this.collectionsController,
|
||||
}) : super(_State.init(
|
||||
zoom: prefController.homePhotosZoomLevel.value,
|
||||
isEnableMemoryCollection:
|
||||
accountPrefController.isEnableMemoryAlbum.value,
|
||||
)) {
|
||||
on<_LoadItems>(_onLoad);
|
||||
on<_Reload>(_onReload);
|
||||
on<_TransformItems>(_onTransformItems);
|
||||
on<_OnItemTransformed>(_onOnItemTransformed);
|
||||
|
||||
on<_SetSelectedItems>(_onSetSelectedItems);
|
||||
on<_AddSelectedItemsToCollection>(_onAddSelectedItemsToCollection);
|
||||
on<_ArchiveSelectedItems>(_onArchiveSelectedItems);
|
||||
on<_DeleteSelectedItems>(_onDeleteSelectedItems);
|
||||
on<_DownloadSelectedItems>(_onDownloadSelectedItems);
|
||||
|
||||
on<_AddVisibleItem>(_onAddVisibleItem);
|
||||
on<_RemoveVisibleItem>(_onRemoveVisibleItem);
|
||||
|
||||
on<_SetContentListMaxExtent>(_onSetContentListMaxExtent);
|
||||
|
||||
on<_StartScaling>(_onStartScaling);
|
||||
on<_EndScaling>(_onEndScaling);
|
||||
on<_SetScale>(_onSetScale);
|
||||
|
||||
on<_SetEnableMemoryCollection>(_onSetEnableMemoryCollection);
|
||||
|
||||
on<_SetError>(_onSetError);
|
||||
|
||||
_subscriptions
|
||||
.add(accountPrefController.isEnableMemoryAlbum.listen((event) {
|
||||
add(_SetEnableMemoryCollection(event));
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
for (final s in _subscriptions) {
|
||||
s.cancel();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
String get tag => _log.fullName;
|
||||
|
||||
@override
|
||||
bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) {
|
||||
currentState = currentState as _State;
|
||||
nextState = nextState as _State;
|
||||
return currentState.scale == nextState.scale &&
|
||||
currentState.visibleItems == nextState.visibleItems;
|
||||
};
|
||||
|
||||
@override
|
||||
void onError(Object error, StackTrace stackTrace) {
|
||||
// we need this to prevent onError being triggered recursively
|
||||
if (!isClosed && !_isHandlingError) {
|
||||
_isHandlingError = true;
|
||||
try {
|
||||
add(_SetError(error, stackTrace));
|
||||
} catch (_) {}
|
||||
_isHandlingError = false;
|
||||
}
|
||||
super.onError(error, stackTrace);
|
||||
}
|
||||
|
||||
Future<void> _onLoad(_LoadItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
return emit.forEach<FilesStreamEvent>(
|
||||
controller.stream,
|
||||
onData: (data) => state.copyWith(
|
||||
files: data.data,
|
||||
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
|
||||
),
|
||||
onError: (e, stackTrace) {
|
||||
_log.severe("[_onLoad] Uncaught exception", e, stackTrace);
|
||||
return state.copyWith(
|
||||
isLoading: _itemTransformerQueue.isProcessing,
|
||||
error: ExceptionEvent(e, stackTrace),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onReload(_Reload ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
unawaited(controller.reload());
|
||||
}
|
||||
|
||||
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
_transformItems(ev.items);
|
||||
emit(state.copyWith(isLoading: true));
|
||||
}
|
||||
|
||||
void _onOnItemTransformed(_OnItemTransformed ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(
|
||||
transformedItems: ev.items,
|
||||
memoryCollections: ev.memoryCollections,
|
||||
isLoading: _itemTransformerQueue.isProcessing,
|
||||
));
|
||||
}
|
||||
|
||||
void _onSetSelectedItems(_SetSelectedItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(selectedItems: ev.items));
|
||||
}
|
||||
|
||||
void _onAddSelectedItemsToCollection(
|
||||
_AddSelectedItemsToCollection ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
final selected = state.selectedItems;
|
||||
_clearSelection(emit);
|
||||
final selectedFiles =
|
||||
selected.whereType<_FileItem>().map((e) => e.file).toList();
|
||||
if (selectedFiles.isNotEmpty) {
|
||||
final targetController = collectionsController.stream.value
|
||||
.itemsControllerByCollection(ev.collection);
|
||||
targetController.addFiles(selectedFiles).onError((e, stackTrace) {
|
||||
if (e != null) {
|
||||
add(_SetError(e, stackTrace));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onArchiveSelectedItems(_ArchiveSelectedItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
final selected = state.selectedItems;
|
||||
_clearSelection(emit);
|
||||
final selectedFiles =
|
||||
selected.whereType<_FileItem>().map((e) => e.file).toList();
|
||||
if (selectedFiles.isNotEmpty) {
|
||||
controller.updateProperty(
|
||||
selectedFiles,
|
||||
isArchived: const OrNull(true),
|
||||
errorBuilder: (fileIds) => _ArchiveFailedError(fileIds.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDeleteSelectedItems(_DeleteSelectedItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
final selected = state.selectedItems;
|
||||
_clearSelection(emit);
|
||||
final selectedFiles =
|
||||
selected.whereType<_FileItem>().map((e) => e.file).toList();
|
||||
if (selectedFiles.isNotEmpty) {
|
||||
controller.remove(
|
||||
selectedFiles,
|
||||
errorBuilder: (fileIds) => _RemoveFailedError(fileIds.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDownloadSelectedItems(
|
||||
_DownloadSelectedItems ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
final selected = state.selectedItems;
|
||||
_clearSelection(emit);
|
||||
final selectedFiles =
|
||||
selected.whereType<_FileItem>().map((e) => e.file).toList();
|
||||
if (selectedFiles.isNotEmpty) {
|
||||
unawaited(DownloadHandler(_c).downloadFiles(account, selectedFiles));
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddVisibleItem(_AddVisibleItem ev, Emitter<_State> emit) {
|
||||
// _log.info(ev);
|
||||
if (state.visibleItems.contains(ev.item)) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(
|
||||
visibleItems: state.visibleItems.added(ev.item),
|
||||
));
|
||||
}
|
||||
|
||||
void _onRemoveVisibleItem(_RemoveVisibleItem ev, Emitter<_State> emit) {
|
||||
// _log.info(ev);
|
||||
if (!state.visibleItems.contains(ev.item)) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(
|
||||
visibleItems: state.visibleItems.removed(ev.item),
|
||||
));
|
||||
}
|
||||
|
||||
void _onSetContentListMaxExtent(
|
||||
_SetContentListMaxExtent ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(contentListMaxExtent: ev.value));
|
||||
}
|
||||
|
||||
void _onStartScaling(_StartScaling ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
}
|
||||
|
||||
void _onEndScaling(_EndScaling ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
if (state.scale == null) {
|
||||
return;
|
||||
}
|
||||
final int newZoom;
|
||||
if (state.scale! >= 1.25) {
|
||||
// scale up
|
||||
newZoom = (state.zoom + 1).clamp(-1, 2);
|
||||
} else if (state.scale! <= 0.75) {
|
||||
newZoom = (state.zoom - 1).clamp(-1, 2);
|
||||
} else {
|
||||
newZoom = state.zoom;
|
||||
}
|
||||
emit(state.copyWith(
|
||||
zoom: newZoom,
|
||||
scale: null,
|
||||
));
|
||||
unawaited(prefController.setHomePhotosZoomLevel(newZoom));
|
||||
}
|
||||
|
||||
void _onSetScale(_SetScale ev, Emitter<_State> emit) {
|
||||
// _log.info(ev);
|
||||
emit(state.copyWith(scale: ev.scale));
|
||||
}
|
||||
|
||||
void _onSetEnableMemoryCollection(
|
||||
_SetEnableMemoryCollection ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(isEnableMemoryCollection: ev.value));
|
||||
}
|
||||
|
||||
void _onSetError(_SetError ev, Emitter<_State> emit) {
|
||||
_log.info(ev);
|
||||
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
|
||||
}
|
||||
|
||||
Future _transformItems(List<FileDescriptor> files) async {
|
||||
_log.info("[_transformItems] Queue ${files.length} items");
|
||||
_itemTransformerQueue.addJob(
|
||||
_ItemTransformerArgument(
|
||||
account: account,
|
||||
files: files,
|
||||
memoriesDayRange: prefController.memoriesRange.value,
|
||||
locale: language_util.getSelectedLocale() ??
|
||||
PlatformDispatcher.instance.locale,
|
||||
),
|
||||
_buildItem,
|
||||
(result) {
|
||||
if (!isClosed) {
|
||||
add(_OnItemTransformed(result.items, result.memoryCollections));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _clearSelection(Emitter<_State> emit) {
|
||||
emit(state.copyWith(selectedItems: const {}));
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final Account account;
|
||||
final FilesController controller;
|
||||
final PrefController prefController;
|
||||
final AccountPrefController accountPrefController;
|
||||
final CollectionsController collectionsController;
|
||||
|
||||
final _itemTransformerQueue =
|
||||
ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>();
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
var _isHandlingError = false;
|
||||
}
|
||||
|
||||
_ItemTransformerResult _buildItem(_ItemTransformerArgument arg) {
|
||||
final sortedFiles = arg.files
|
||||
.where((f) => f.fdIsArchived != true)
|
||||
.sorted(compareFileDescriptorDateTimeDescending);
|
||||
final dateHelper = photo_list_util.DateGroupHelper(isMonthOnly: false);
|
||||
|
||||
final today = clock.now();
|
||||
final memoryCollectionHelper = photo_list_util.MemoryCollectionHelper(
|
||||
arg.account,
|
||||
today: today,
|
||||
dayRange: arg.memoriesDayRange,
|
||||
);
|
||||
final transformed = <_Item>[];
|
||||
for (int i = 0; i < sortedFiles.length; ++i) {
|
||||
final file = sortedFiles[i];
|
||||
final item = _buildSingleItem(arg.account, file);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
final date = dateHelper.onFile(file);
|
||||
if (date != null) {
|
||||
transformed.add(_DateItem(date: date));
|
||||
}
|
||||
transformed.add(item);
|
||||
memoryCollectionHelper.addFile(file);
|
||||
}
|
||||
final memoryCollections = memoryCollectionHelper
|
||||
.build((year) => L10n.of(arg.locale).memoryAlbumName(today.year - year));
|
||||
return _ItemTransformerResult(
|
||||
items: transformed,
|
||||
memoryCollections: memoryCollections,
|
||||
);
|
||||
}
|
||||
|
||||
_Item? _buildSingleItem(Account account, FileDescriptor file) {
|
||||
if (file_util.isSupportedImageFormat(file)) {
|
||||
return _PhotoItem(
|
||||
file: file,
|
||||
account: account,
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(file)) {
|
||||
return _VideoItem(
|
||||
file: file,
|
||||
account: account,
|
||||
);
|
||||
} else {
|
||||
_$__NpLog.log
|
||||
.shout("[_buildSingleItem] Unsupported file format: ${file.fdMime}");
|
||||
return null;
|
||||
}
|
||||
}
|
218
app/lib/widget/home_photos/state_event.dart
Normal file
218
app/lib/widget/home_photos/state_event.dart
Normal file
|
@ -0,0 +1,218 @@
|
|||
part of '../home_photos2.dart';
|
||||
|
||||
@genCopyWith
|
||||
@toString
|
||||
class _State {
|
||||
const _State({
|
||||
required this.files,
|
||||
required this.isLoading,
|
||||
required this.transformedItems,
|
||||
required this.selectedItems,
|
||||
required this.visibleItems,
|
||||
required this.isEnableMemoryCollection,
|
||||
required this.memoryCollections,
|
||||
this.contentListMaxExtent,
|
||||
required this.zoom,
|
||||
this.scale,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory _State.init({
|
||||
required bool isEnableMemoryCollection,
|
||||
required int zoom,
|
||||
}) =>
|
||||
_State(
|
||||
files: const [],
|
||||
isLoading: false,
|
||||
transformedItems: const [],
|
||||
selectedItems: const {},
|
||||
visibleItems: const {},
|
||||
isEnableMemoryCollection: isEnableMemoryCollection,
|
||||
memoryCollections: const [],
|
||||
zoom: zoom,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<FileDescriptor> files;
|
||||
final bool isLoading;
|
||||
final List<_Item> transformedItems;
|
||||
final Set<_Item> selectedItems;
|
||||
final Set<_VisibleItem> visibleItems;
|
||||
|
||||
final bool isEnableMemoryCollection;
|
||||
final List<Collection> memoryCollections;
|
||||
|
||||
final double? contentListMaxExtent;
|
||||
|
||||
final int zoom;
|
||||
final double? scale;
|
||||
|
||||
final ExceptionEvent? error;
|
||||
}
|
||||
|
||||
abstract class _Event {}
|
||||
|
||||
/// Load the files
|
||||
@toString
|
||||
class _LoadItems implements _Event {
|
||||
const _LoadItems();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _Reload implements _Event {
|
||||
const _Reload();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
/// Transform the file list (e.g., filtering, sorting, etc)
|
||||
@toString
|
||||
class _TransformItems implements _Event {
|
||||
const _TransformItems(this.items);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<FileDescriptor> items;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _OnItemTransformed implements _Event {
|
||||
const _OnItemTransformed(this.items, this.memoryCollections);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final List<_Item> items;
|
||||
final List<Collection> memoryCollections;
|
||||
}
|
||||
|
||||
/// Set the currently selected items
|
||||
@toString
|
||||
class _SetSelectedItems implements _Event {
|
||||
const _SetSelectedItems({
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Set<_Item> items;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _AddSelectedItemsToCollection implements _Event {
|
||||
const _AddSelectedItemsToCollection(this.collection);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Collection collection;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _ArchiveSelectedItems implements _Event {
|
||||
const _ArchiveSelectedItems();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _DeleteSelectedItems implements _Event {
|
||||
const _DeleteSelectedItems();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _DownloadSelectedItems implements _Event {
|
||||
const _DownloadSelectedItems();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _AddVisibleItem implements _Event {
|
||||
const _AddVisibleItem(this.item);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final _VisibleItem item;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _RemoveVisibleItem implements _Event {
|
||||
const _RemoveVisibleItem(this.item);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final _VisibleItem item;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetContentListMaxExtent implements _Event {
|
||||
const _SetContentListMaxExtent(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final double? value;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _StartScaling implements _Event {
|
||||
const _StartScaling();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _EndScaling implements _Event {
|
||||
const _EndScaling();
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetScale implements _Event {
|
||||
const _SetScale(this.scale);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final double scale;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetEnableMemoryCollection implements _Event {
|
||||
const _SetEnableMemoryCollection(this.value);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final bool value;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _SetError implements _Event {
|
||||
const _SetError(this.error, [this.stackTrace]);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final Object error;
|
||||
final StackTrace? stackTrace;
|
||||
}
|
167
app/lib/widget/home_photos/type.dart
Normal file
167
app/lib/widget/home_photos/type.dart
Normal file
|
@ -0,0 +1,167 @@
|
|||
part of '../home_photos2.dart';
|
||||
|
||||
abstract class _Item implements SelectableItemMetadata {
|
||||
const _Item();
|
||||
|
||||
StaggeredTile get staggeredTile;
|
||||
|
||||
Widget buildWidget(BuildContext context);
|
||||
}
|
||||
|
||||
abstract class _FileItem extends _Item {
|
||||
const _FileItem({
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get isSelectable => true;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is _FileItem && file.compareServerIdentity(other.file));
|
||||
|
||||
@override
|
||||
int get hashCode => file.identityHashCode;
|
||||
|
||||
final FileDescriptor file;
|
||||
}
|
||||
|
||||
class _PhotoItem extends _FileItem {
|
||||
_PhotoItem({
|
||||
required super.file,
|
||||
required this.account,
|
||||
}) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file);
|
||||
|
||||
@override
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
@override
|
||||
Widget buildWidget(BuildContext context) {
|
||||
return PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: _previewUrl,
|
||||
isGif: file.fdMime == "image/gif",
|
||||
isFavorite: file.fdIsFavorite,
|
||||
heroKey: flutter_util.getImageHeroTag(file),
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String _previewUrl;
|
||||
}
|
||||
|
||||
class _VideoItem extends _FileItem {
|
||||
_VideoItem({
|
||||
required super.file,
|
||||
required this.account,
|
||||
}) : _previewUrl = NetworkRectThumbnail.imageUrlForFile(account, file);
|
||||
|
||||
@override
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
@override
|
||||
Widget buildWidget(BuildContext context) {
|
||||
return PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: _previewUrl,
|
||||
isFavorite: file.fdIsFavorite,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String _previewUrl;
|
||||
}
|
||||
|
||||
class _DateItem extends _Item {
|
||||
const _DateItem({
|
||||
required this.date,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get isSelectable => false;
|
||||
|
||||
@override
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.extent(99, 32);
|
||||
|
||||
@override
|
||||
Widget buildWidget(BuildContext context) {
|
||||
return PhotoListDate(
|
||||
date: date,
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime date;
|
||||
}
|
||||
|
||||
class _ItemTransformerArgument {
|
||||
const _ItemTransformerArgument({
|
||||
required this.account,
|
||||
required this.files,
|
||||
required this.memoriesDayRange,
|
||||
required this.locale,
|
||||
});
|
||||
|
||||
final Account account;
|
||||
final List<FileDescriptor> files;
|
||||
final int memoriesDayRange;
|
||||
final Locale locale;
|
||||
}
|
||||
|
||||
class _ItemTransformerResult {
|
||||
const _ItemTransformerResult({
|
||||
required this.items,
|
||||
required this.memoryCollections,
|
||||
});
|
||||
|
||||
final List<_Item> items;
|
||||
final List<Collection> memoryCollections;
|
||||
}
|
||||
|
||||
class _MemoryCollectionItem {
|
||||
static const width = 96.0;
|
||||
static const height = width * 1.15;
|
||||
}
|
||||
|
||||
class _VisibleItem implements Comparable<_VisibleItem> {
|
||||
const _VisibleItem(this.index, this.item);
|
||||
|
||||
@override
|
||||
bool operator ==(Object? other) =>
|
||||
other is _VisibleItem && index == other.index;
|
||||
|
||||
@override
|
||||
int compareTo(_VisibleItem other) => index.compareTo(other.index);
|
||||
|
||||
@override
|
||||
int get hashCode => index.hashCode;
|
||||
|
||||
final int index;
|
||||
final _Item item;
|
||||
}
|
||||
|
||||
enum _SelectionMenuOption {
|
||||
archive,
|
||||
delete,
|
||||
download,
|
||||
}
|
||||
|
||||
@toString
|
||||
class _ArchiveFailedError implements Exception {
|
||||
const _ArchiveFailedError(this.count);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final int count;
|
||||
}
|
||||
|
||||
@toString
|
||||
class _RemoveFailedError implements Exception {
|
||||
const _RemoveFailedError(this.count);
|
||||
|
||||
@override
|
||||
String toString() => _$toString();
|
||||
|
||||
final int count;
|
||||
}
|
260
app/lib/widget/home_photos/view.dart
Normal file
260
app/lib/widget/home_photos/view.dart
Normal file
|
@ -0,0 +1,260 @@
|
|||
part of '../home_photos2.dart';
|
||||
|
||||
class _ContentList extends StatelessWidget {
|
||||
const _ContentList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocSelector<int>(
|
||||
selector: (state) => state.zoom,
|
||||
builder: (context, zoom) => _ContentListBody(
|
||||
maxCrossAxisExtent: photo_list_util.getThumbSize(zoom).toDouble(),
|
||||
isNeedVisibilityInfo: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScalingList extends StatelessWidget {
|
||||
const _ScalingList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) => previous.scale != current.scale,
|
||||
builder: (context, state) {
|
||||
if (state.scale == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
int nextZoom;
|
||||
if (state.scale! > 1) {
|
||||
nextZoom = state.zoom + 1;
|
||||
} else {
|
||||
nextZoom = state.zoom - 1;
|
||||
}
|
||||
nextZoom = nextZoom.clamp(-1, 2);
|
||||
return _ContentListBody(
|
||||
maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(),
|
||||
isNeedVisibilityInfo: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _ContentListBody extends StatelessWidget {
|
||||
const _ContentListBody({
|
||||
required this.maxCrossAxisExtent,
|
||||
required this.isNeedVisibilityInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.transformedItems != current.transformedItems ||
|
||||
previous.selectedItems != current.selectedItems,
|
||||
builder: (context, state) => SelectableItemList<_Item>(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
items: state.transformedItems,
|
||||
itemBuilder: (context, index, item) {
|
||||
final w = item.buildWidget(context);
|
||||
if (isNeedVisibilityInfo) {
|
||||
return VisibilityDetector(
|
||||
key: Key("${_log.fullName}.$index"),
|
||||
onVisibilityChanged: (info) {
|
||||
if (context.mounted) {
|
||||
if (info.visibleFraction >= 0.2) {
|
||||
context
|
||||
.addEvent(_AddVisibleItem(_VisibleItem(index, item)));
|
||||
} else {
|
||||
context.addEvent(
|
||||
_RemoveVisibleItem(_VisibleItem(index, item)));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: w,
|
||||
);
|
||||
} else {
|
||||
return w;
|
||||
}
|
||||
},
|
||||
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||||
selectedItems: state.selectedItems,
|
||||
onSelectionChange: (_, selected) {
|
||||
context.addEvent(_SetSelectedItems(items: selected.cast()));
|
||||
},
|
||||
onItemTap: (context, index, _) {
|
||||
if (state.transformedItems[index] is! _FileItem) {
|
||||
return;
|
||||
}
|
||||
final actualIndex = index -
|
||||
state.transformedItems
|
||||
.sublist(0, index)
|
||||
.where((e) => e is! _FileItem)
|
||||
.length;
|
||||
Navigator.of(context).pushNamed(
|
||||
Viewer.routeName,
|
||||
arguments: ViewerArguments(
|
||||
context.bloc.account,
|
||||
state.transformedItems
|
||||
.whereType<_FileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
actualIndex,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMaxExtentChange: (value) {
|
||||
context.addEvent(_SetContentListMaxExtent(value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final double maxCrossAxisExtent;
|
||||
final bool isNeedVisibilityInfo;
|
||||
}
|
||||
|
||||
class _MemoryCollectionList extends StatelessWidget {
|
||||
const _MemoryCollectionList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: _MemoryCollectionItem.height,
|
||||
child: _BlocSelector<List<Collection>>(
|
||||
selector: (state) => state.memoryCollections,
|
||||
builder: (context, memoryCollections) => ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
itemCount: memoryCollections.length,
|
||||
itemBuilder: (context, index) {
|
||||
final c = memoryCollections[index];
|
||||
return _MemoryCollectionItemView(
|
||||
coverUrl: c.getCoverUrl(
|
||||
k.photoThumbSize,
|
||||
k.photoThumbSize,
|
||||
isKeepAspectRatio: true,
|
||||
),
|
||||
label: c.name,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
CollectionBrowser.routeName,
|
||||
arguments: CollectionBrowserArguments(c),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MemoryCollectionItemView extends StatelessWidget {
|
||||
static const width = 96.0;
|
||||
static const height = width * 1.15;
|
||||
|
||||
const _MemoryCollectionItemView({
|
||||
required this.coverUrl,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
PhotoListImage(
|
||||
account: context.bloc.account,
|
||||
previewUrl: coverUrl,
|
||||
padding: const EdgeInsets.all(0),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.center,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black87],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||
color: Theme.of(context).onDarkSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onTap != null)
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String? coverUrl;
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _ScrollLabel extends StatelessWidget {
|
||||
const _ScrollLabel();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BlocSelector<Set<_VisibleItem>>(
|
||||
selector: (state) => state.visibleItems,
|
||||
builder: (context, visibleItems) {
|
||||
final firstVisibleItem =
|
||||
visibleItems.sorted().firstWhereOrNull((e) => e.item is _FileItem);
|
||||
final date = firstVisibleItem?.item.as<_FileItem>()?.file.fdDateTime;
|
||||
if (date == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final text = DateFormat(DateFormat.YEAR_ABBR_MONTH,
|
||||
Localizations.localeOf(context).languageCode)
|
||||
.format(date.toLocal());
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface),
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
325
app/lib/widget/home_photos2.dart
Normal file
325
app/lib/widget/home_photos2.dart
Normal file
|
@ -0,0 +1,325 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc_util.dart';
|
||||
import 'package:nc_photos/controller/account_controller.dart';
|
||||
import 'package:nc_photos/controller/account_pref_controller.dart';
|
||||
import 'package:nc_photos/controller/collections_controller.dart';
|
||||
import 'package:nc_photos/controller/files_controller.dart';
|
||||
import 'package:nc_photos/controller/pref_controller.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/collection.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/flutter_util.dart' as flutter_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/theme/dimension.dart';
|
||||
import 'package:nc_photos/widget/collection_browser.dart';
|
||||
import 'package:nc_photos/widget/collection_picker.dart';
|
||||
import 'package:nc_photos/widget/file_sharer_dialog.dart';
|
||||
import 'package:nc_photos/widget/finger_listener.dart';
|
||||
import 'package:nc_photos/widget/home_app_bar.dart';
|
||||
import 'package:nc_photos/widget/navigation_bar_blur_filter.dart';
|
||||
import 'package:nc_photos/widget/network_thumbnail.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
||||
import 'package:nc_photos/widget/selectable_item_list.dart';
|
||||
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||||
import 'package:nc_photos/widget/sliver_visualized_scale.dart';
|
||||
import 'package:nc_photos/widget/viewer.dart';
|
||||
import 'package:np_async/np_async.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_collection/np_collection.dart';
|
||||
import 'package:np_common/object_util.dart';
|
||||
import 'package:np_common/or_null.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
part 'home_photos/app_bar.dart';
|
||||
part 'home_photos/bloc.dart';
|
||||
part 'home_photos/state_event.dart';
|
||||
part 'home_photos/type.dart';
|
||||
part 'home_photos/view.dart';
|
||||
part 'home_photos2.g.dart';
|
||||
|
||||
class HomePhotos2 extends StatelessWidget {
|
||||
const HomePhotos2({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accountController = context.read<AccountController>();
|
||||
return BlocProvider(
|
||||
create: (_) => _Bloc(
|
||||
KiwiContainer().resolve(),
|
||||
account: accountController.account,
|
||||
controller: accountController.filesController,
|
||||
prefController: context.read(),
|
||||
accountPrefController: accountController.accountPrefController,
|
||||
collectionsController: accountController.collectionsController,
|
||||
),
|
||||
child: const _WrappedHomePhotos(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WrappedHomePhotos extends StatefulWidget {
|
||||
const _WrappedHomePhotos();
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _WrappedHomePhotosState();
|
||||
}
|
||||
|
||||
@npLog
|
||||
class _WrappedHomePhotosState extends State<_WrappedHomePhotos> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc.add(const _LoadItems());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VisibilityDetector(
|
||||
key: _key,
|
||||
onVisibilityChanged: (info) {
|
||||
final isVisible = info.visibleFraction >= 0.2;
|
||||
if (isVisible != _isVisible) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVisible = isVisible;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
_BlocListenerT<List<FileDescriptor>>(
|
||||
selector: (state) => state.files,
|
||||
listener: (context, files) {
|
||||
_bloc.add(_TransformItems(files));
|
||||
},
|
||||
),
|
||||
_BlocListenerT<ExceptionEvent?>(
|
||||
selector: (state) => state.error,
|
||||
listener: (context, error) {
|
||||
if (error != null && _isVisible == true) {
|
||||
final String content;
|
||||
if (error.error is _ArchiveFailedError) {
|
||||
content = L10n.global().archiveSelectedFailureNotification(
|
||||
(error.error as _ArchiveFailedError).count);
|
||||
} else if (error.error is _RemoveFailedError) {
|
||||
content = L10n.global().deleteSelectedFailureNotification(
|
||||
(error.error as _RemoveFailedError).count);
|
||||
} else {
|
||||
content = exception_util.toUserString(error.error);
|
||||
}
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: FingerListener(
|
||||
onFingerChanged: (finger) {
|
||||
setState(() {
|
||||
_finger = finger;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onScaleStart: (_) {
|
||||
_bloc.add(const _StartScaling());
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
_bloc.add(_SetScale(details.scale));
|
||||
},
|
||||
onScaleEnd: (_) {
|
||||
_bloc.add(const _EndScaling());
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => _BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.contentListMaxExtent !=
|
||||
current.contentListMaxExtent ||
|
||||
(previous.isEnableMemoryCollection &&
|
||||
previous.memoryCollections.isNotEmpty) !=
|
||||
(current.isEnableMemoryCollection &&
|
||||
current.memoryCollections.isNotEmpty),
|
||||
builder: (context, state) {
|
||||
final scrollExtent = _getScrollViewExtent(
|
||||
context: context,
|
||||
constraints: constraints,
|
||||
hasMemoryCollection: state.isEnableMemoryCollection &&
|
||||
state.memoryCollections.isNotEmpty,
|
||||
contentListMaxExtent: state.contentListMaxExtent,
|
||||
);
|
||||
return Stack(
|
||||
children: [
|
||||
DraggableScrollbar.semicircle(
|
||||
controller: _scrollController,
|
||||
overrideMaxScrollExtent: scrollExtent,
|
||||
// status bar + app bar
|
||||
topOffset: _getAppBarExtent(context),
|
||||
bottomOffset:
|
||||
AppDimension.of(context).homeBottomAppBarHeight,
|
||||
labelTextBuilder: (_) => const _ScrollLabel(),
|
||||
labelPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 40),
|
||||
backgroundColor: Theme.of(context).elevate(
|
||||
Theme.of(context).colorScheme.inverseSurface, 3),
|
||||
heightScrollThumb: 60,
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(scrollbars: false),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
_bloc.add(const _Reload());
|
||||
await _bloc.stream.first;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: _finger >= 2
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
slivers: [
|
||||
_BlocSelector<bool>(
|
||||
selector: (state) =>
|
||||
state.selectedItems.isEmpty,
|
||||
builder: (context, isEmpty) => isEmpty
|
||||
? const _AppBar()
|
||||
: const _SelectionAppBar(),
|
||||
),
|
||||
_BlocBuilder(
|
||||
buildWhen: (previous, current) =>
|
||||
(previous.isEnableMemoryCollection &&
|
||||
previous
|
||||
.memoryCollections.isNotEmpty) !=
|
||||
(current.isEnableMemoryCollection &&
|
||||
current.memoryCollections.isNotEmpty),
|
||||
builder: (context, state) {
|
||||
if (state.isEnableMemoryCollection &&
|
||||
state.memoryCollections.isNotEmpty) {
|
||||
return const _MemoryCollectionList();
|
||||
} else {
|
||||
return const SliverToBoxAdapter();
|
||||
}
|
||||
},
|
||||
),
|
||||
_BlocSelector<double?>(
|
||||
selector: (state) => state.scale,
|
||||
builder: (context, scale) =>
|
||||
SliverTransitionedScale(
|
||||
scale: scale,
|
||||
baseSliver: const _ContentList(),
|
||||
overlaySliver: const _ScalingList(),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: AppDimension.of(context)
|
||||
.homeBottomAppBarHeight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: NavigationBarBlurFilter(
|
||||
height:
|
||||
AppDimension.of(context).homeBottomAppBarHeight,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Return the estimated scroll extent of the custom scroll view, or null
|
||||
double? _getScrollViewExtent({
|
||||
required BuildContext context,
|
||||
required BoxConstraints constraints,
|
||||
required bool hasMemoryCollection,
|
||||
required double? contentListMaxExtent,
|
||||
}) {
|
||||
if (contentListMaxExtent != null && constraints.hasBoundedHeight) {
|
||||
final appBarExtent = _getAppBarExtent(context);
|
||||
final bottomAppBarExtent =
|
||||
AppDimension.of(context).homeBottomAppBarHeight;
|
||||
// final metadataTaskHeaderExtent = _web?.getHeaderHeight() ?? 0;
|
||||
final smartAlbumListHeight =
|
||||
hasMemoryCollection ? _MemoryCollectionItem.height : 0;
|
||||
// scroll extent = list height - widget viewport height
|
||||
// + sliver app bar height + bottom app bar height
|
||||
// + metadata task header height + smart album list height
|
||||
final scrollExtent = contentListMaxExtent -
|
||||
constraints.maxHeight +
|
||||
appBarExtent +
|
||||
bottomAppBarExtent +
|
||||
// metadataTaskHeaderExtent +
|
||||
smartAlbumListHeight;
|
||||
_log.info("[_getScrollViewExtent] $contentListMaxExtent "
|
||||
"- ${constraints.maxHeight} "
|
||||
"+ $appBarExtent "
|
||||
"+ $bottomAppBarExtent "
|
||||
// "+ $metadataTaskHeaderExtent "
|
||||
"+ $smartAlbumListHeight "
|
||||
"= $scrollExtent");
|
||||
return scrollExtent;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double _getAppBarExtent(BuildContext context) =>
|
||||
MediaQuery.of(context).padding.top + kToolbarHeight;
|
||||
|
||||
late final _bloc = context.bloc;
|
||||
|
||||
final _key = GlobalKey();
|
||||
final _scrollController = ScrollController();
|
||||
bool? _isVisible;
|
||||
var _finger = 0;
|
||||
}
|
||||
|
||||
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||
|
||||
extension on BuildContext {
|
||||
_Bloc get bloc => read<_Bloc>();
|
||||
// _State get state => bloc.state;
|
||||
void addEvent(_Event event) => bloc.add(event);
|
||||
}
|
||||
|
||||
@npLog
|
||||
// ignore: camel_case_types
|
||||
class __ {}
|
262
app/lib/widget/home_photos2.g.dart
Normal file
262
app/lib/widget/home_photos2.g.dart
Normal file
|
@ -0,0 +1,262 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'home_photos2.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithLintRuleGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $_StateCopyWithWorker {
|
||||
_State call(
|
||||
{List<FileDescriptor>? files,
|
||||
bool? isLoading,
|
||||
List<_Item>? transformedItems,
|
||||
Set<_Item>? selectedItems,
|
||||
Set<_VisibleItem>? visibleItems,
|
||||
bool? isEnableMemoryCollection,
|
||||
List<Collection>? memoryCollections,
|
||||
double? contentListMaxExtent,
|
||||
int? zoom,
|
||||
double? scale,
|
||||
ExceptionEvent? error});
|
||||
}
|
||||
|
||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||
_$_StateCopyWithWorkerImpl(this.that);
|
||||
|
||||
@override
|
||||
_State call(
|
||||
{dynamic files,
|
||||
dynamic isLoading,
|
||||
dynamic transformedItems,
|
||||
dynamic selectedItems,
|
||||
dynamic visibleItems,
|
||||
dynamic isEnableMemoryCollection,
|
||||
dynamic memoryCollections,
|
||||
dynamic contentListMaxExtent = copyWithNull,
|
||||
dynamic zoom,
|
||||
dynamic scale = copyWithNull,
|
||||
dynamic error = copyWithNull}) {
|
||||
return _State(
|
||||
files: files as List<FileDescriptor>? ?? that.files,
|
||||
isLoading: isLoading as bool? ?? that.isLoading,
|
||||
transformedItems:
|
||||
transformedItems as List<_Item>? ?? that.transformedItems,
|
||||
selectedItems: selectedItems as Set<_Item>? ?? that.selectedItems,
|
||||
visibleItems: visibleItems as Set<_VisibleItem>? ?? that.visibleItems,
|
||||
isEnableMemoryCollection:
|
||||
isEnableMemoryCollection as bool? ?? that.isEnableMemoryCollection,
|
||||
memoryCollections:
|
||||
memoryCollections as List<Collection>? ?? that.memoryCollections,
|
||||
contentListMaxExtent: contentListMaxExtent == copyWithNull
|
||||
? that.contentListMaxExtent
|
||||
: contentListMaxExtent as double?,
|
||||
zoom: zoom as int? ?? that.zoom,
|
||||
scale: scale == copyWithNull ? that.scale : scale as double?,
|
||||
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
|
||||
}
|
||||
|
||||
final _State that;
|
||||
}
|
||||
|
||||
extension $_StateCopyWith on _State {
|
||||
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// NpLogGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_WrappedHomePhotosStateNpLog on _WrappedHomePhotosState {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2._WrappedHomePhotosState");
|
||||
}
|
||||
|
||||
extension _$__NpLog on __ {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2.__");
|
||||
}
|
||||
|
||||
extension _$_SelectionAppBarNpLog on _SelectionAppBar {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2._SelectionAppBar");
|
||||
}
|
||||
|
||||
extension _$_SelectionAppBarMenuNpLog on _SelectionAppBarMenu {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2._SelectionAppBarMenu");
|
||||
}
|
||||
|
||||
extension _$_BlocNpLog on _Bloc {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2._Bloc");
|
||||
}
|
||||
|
||||
extension _$_ContentListBodyNpLog on _ContentListBody {
|
||||
// ignore: unused_element
|
||||
Logger get _log => log;
|
||||
|
||||
static final log = Logger("widget.home_photos2._ContentListBody");
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// ToStringGenerator
|
||||
// **************************************************************************
|
||||
|
||||
extension _$_StateToString on _State {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_LoadItemsToString on _LoadItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_LoadItems {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ReloadToString on _Reload {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_Reload {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_TransformItemsToString on _TransformItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_TransformItems {items: [length: ${items.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_OnItemTransformedToString on _OnItemTransformed {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_OnItemTransformed {items: [length: ${items.length}], memoryCollections: [length: ${memoryCollections.length}]}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetSelectedItemsToString on _SetSelectedItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetSelectedItems {items: {length: ${items.length}}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_AddSelectedItemsToCollectionToString
|
||||
on _AddSelectedItemsToCollection {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_AddSelectedItemsToCollection {collection: $collection}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ArchiveSelectedItemsToString on _ArchiveSelectedItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ArchiveSelectedItems {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_DeleteSelectedItemsToString on _DeleteSelectedItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_DeleteSelectedItems {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_DownloadSelectedItemsToString on _DownloadSelectedItems {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_DownloadSelectedItems {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_AddVisibleItemToString on _AddVisibleItem {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_AddVisibleItem {item: $item}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_RemoveVisibleItemToString on _RemoveVisibleItem {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_RemoveVisibleItem {item: $item}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetContentListMaxExtentToString on _SetContentListMaxExtent {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetContentListMaxExtent {value: ${value == null ? null : "${value!.toStringAsFixed(3)}"}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_StartScalingToString on _StartScaling {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_StartScaling {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_EndScalingToString on _EndScaling {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_EndScaling {}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetScaleToString on _SetScale {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetScale {scale: ${scale.toStringAsFixed(3)}}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetEnableMemoryCollectionToString on _SetEnableMemoryCollection {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetEnableMemoryCollection {value: $value}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_SetErrorToString on _SetError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_SetError {error: $error, stackTrace: $stackTrace}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_ArchiveFailedErrorToString on _ArchiveFailedError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_ArchiveFailedError {count: $count}";
|
||||
}
|
||||
}
|
||||
|
||||
extension _$_RemoveFailedErrorToString on _RemoveFailedError {
|
||||
String _$toString() {
|
||||
// ignore: unnecessary_string_interpolations
|
||||
return "_RemoveFailedError {count: $count}";
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/session_storage.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/widget/measurable_item_list.dart';
|
||||
import 'package:nc_photos/widget/selectable.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
import 'package:np_collection/np_collection.dart';
|
||||
|
@ -36,6 +37,7 @@ class SelectableItemList<T extends SelectableItemMetadata>
|
|||
this.indicatorAlignment = Alignment.topLeft,
|
||||
this.onItemTap,
|
||||
this.onSelectionChange,
|
||||
this.onMaxExtentChange,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -53,6 +55,7 @@ class SelectableItemList<T extends SelectableItemMetadata>
|
|||
|
||||
final void Function(BuildContext context, int index, T metadata)? onItemTap;
|
||||
final void Function(BuildContext context, Set<T> selected)? onSelectionChange;
|
||||
final ValueChanged<double?>? onMaxExtentChange;
|
||||
}
|
||||
|
||||
@npLog
|
||||
|
@ -90,32 +93,46 @@ class _SelectableItemListState<T extends SelectableItemMetadata>
|
|||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
return SliverStaggeredGrid.extentBuilder(
|
||||
key: ObjectKey(widget.maxCrossAxisExtent),
|
||||
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, i) {
|
||||
final meta = widget.items[i];
|
||||
if (meta.isSelectable) {
|
||||
return Selectable(
|
||||
isSelected: widget.selectedItems.contains(meta),
|
||||
iconSize: 32,
|
||||
childBorderRadius:
|
||||
widget.childBorderRadius ?? BorderRadius.circular(24),
|
||||
indicatorAlignment: widget.indicatorAlignment,
|
||||
onTap: _isSelecting
|
||||
? () => _onItemSelect(context, i, meta)
|
||||
: () => _onItemTap(context, i, meta),
|
||||
onLongPress: () => _onItemLongPress(i, meta),
|
||||
child: widget.itemBuilder(context, i, meta),
|
||||
);
|
||||
} else {
|
||||
return widget.itemBuilder(context, i, meta);
|
||||
}
|
||||
},
|
||||
staggeredTileBuilder: (i) =>
|
||||
widget.staggeredTileBuilder(i, widget.items[i]),
|
||||
);
|
||||
if (widget.onMaxExtentChange != null) {
|
||||
return MeasurableItemList(
|
||||
key: _listKey,
|
||||
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: _buildItem,
|
||||
staggeredTileBuilder: (i) =>
|
||||
widget.staggeredTileBuilder(i, widget.items[i]),
|
||||
onMaxExtentChanged: widget.onMaxExtentChange,
|
||||
);
|
||||
} else {
|
||||
return SliverStaggeredGrid.extentBuilder(
|
||||
key: ObjectKey(widget.maxCrossAxisExtent),
|
||||
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: _buildItem,
|
||||
staggeredTileBuilder: (i) =>
|
||||
widget.staggeredTileBuilder(i, widget.items[i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
final meta = widget.items[index];
|
||||
if (meta.isSelectable) {
|
||||
return Selectable(
|
||||
isSelected: widget.selectedItems.contains(meta),
|
||||
iconSize: 32,
|
||||
childBorderRadius:
|
||||
widget.childBorderRadius ?? BorderRadius.circular(24),
|
||||
indicatorAlignment: widget.indicatorAlignment,
|
||||
onTap: _isSelecting
|
||||
? () => _onItemSelect(context, index, meta)
|
||||
: () => _onItemTap(context, index, meta),
|
||||
onLongPress: () => _onItemLongPress(index, meta),
|
||||
child: widget.itemBuilder(context, index, meta),
|
||||
);
|
||||
} else {
|
||||
return widget.itemBuilder(context, index, meta);
|
||||
}
|
||||
}
|
||||
|
||||
void _onItemTap(BuildContext context, int index, T metadata) {
|
||||
|
@ -214,6 +231,12 @@ class _SelectableItemListState<T extends SelectableItemMetadata>
|
|||
"[_remapSelected] ${widget.selectedItems.length - newSelected.length} items not found in the new list");
|
||||
}
|
||||
widget.onSelectionChange?.call(context, newSelected);
|
||||
// TODO remap lastSelectPosition
|
||||
|
||||
_log.info("[_remapSelected] updateListHeight: list item changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||
(_listKey.currentState as MeasurableItemListState?)
|
||||
?.updateListHeight());
|
||||
}
|
||||
|
||||
bool get _isSelecting => widget.selectedItems.isNotEmpty;
|
||||
|
@ -222,4 +245,6 @@ class _SelectableItemListState<T extends SelectableItemMetadata>
|
|||
final _keyboardFocus = FocusNode();
|
||||
int? _lastSelectPosition;
|
||||
bool _isKeyboardRangeSelecting = false;
|
||||
|
||||
final _listKey = GlobalKey();
|
||||
}
|
||||
|
|
|
@ -1254,7 +1254,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
||||
|
|
|
@ -136,6 +136,7 @@ dependencies:
|
|||
page_view_indicators: ^2.0.0
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.15
|
||||
provider: any
|
||||
rxdart: ^0.27.7
|
||||
screen_brightness: ^0.2.2
|
||||
shared_preferences: ^2.0.8
|
||||
|
|
|
@ -5,3 +5,4 @@ export 'src/iterator_extension.dart';
|
|||
export 'src/list_extension.dart';
|
||||
export 'src/list_util.dart';
|
||||
export 'src/map_extension.dart';
|
||||
export 'src/set_util.dart';
|
||||
|
|
5
np_collection/lib/src/set_util.dart
Normal file
5
np_collection/lib/src/set_util.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
extension SetExtension<T> on Set<T> {
|
||||
Set<T> added(T element) => toSet()..add(element);
|
||||
|
||||
Set<T> removed(T element) => toSet()..remove(element);
|
||||
}
|
Loading…
Reference in a new issue