mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +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:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:provider/single_child_widget.dart';
|
||||||
|
|
||||||
mixin BlocLogger {
|
mixin BlocLogger {
|
||||||
String? get tag => null;
|
String? get tag => null;
|
||||||
|
@ -6,6 +8,28 @@ mixin BlocLogger {
|
||||||
bool Function(dynamic currentState, dynamic nextState)? get shouldLog => null;
|
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
|
/// Wrap around a string such that two strings with the same value will fail
|
||||||
/// the identical check
|
/// the identical check
|
||||||
class StateMessage {
|
class StateMessage {
|
||||||
|
@ -25,3 +49,11 @@ extension EmitterExtension<State> on Emitter<State> {
|
||||||
onError: (_, __) {},
|
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,
|
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 =>
|
ValueStream<int> get albumBrowserZoomLevel =>
|
||||||
_albumBrowserZoomLevelController.stream;
|
_albumBrowserZoomLevelController.stream;
|
||||||
|
|
||||||
|
@ -237,6 +246,8 @@ class PrefController {
|
||||||
final DiContainer _c;
|
final DiContainer _c;
|
||||||
late final _languageController =
|
late final _languageController =
|
||||||
BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0)));
|
BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0)));
|
||||||
|
late final _homePhotosZoomLevelController =
|
||||||
|
BehaviorSubject.seeded(_c.pref.getHomePhotosZoomLevelOr(0));
|
||||||
late final _albumBrowserZoomLevelController =
|
late final _albumBrowserZoomLevelController =
|
||||||
BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0));
|
BehaviorSubject.seeded(_c.pref.getAlbumBrowserZoomLevelOr(0));
|
||||||
late final _homeAlbumsSortController =
|
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/use_case/import_potential_shared_album.dart';
|
||||||
import 'package:nc_photos/widget/home_collections.dart';
|
import 'package:nc_photos/widget/home_collections.dart';
|
||||||
import 'package:nc_photos/widget/home_photos.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:nc_photos/widget/home_search.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_common/or_null.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) {
|
Widget _buildPage(BuildContext context, int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
return HomePhotos(
|
return const HomePhotos2();
|
||||||
account: widget.account,
|
|
||||||
);
|
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
return HomeSearch(
|
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/k.dart' as k;
|
||||||
import 'package:nc_photos/session_storage.dart';
|
import 'package:nc_photos/session_storage.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.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:nc_photos/widget/selectable.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_collection/np_collection.dart';
|
import 'package:np_collection/np_collection.dart';
|
||||||
|
@ -36,6 +37,7 @@ class SelectableItemList<T extends SelectableItemMetadata>
|
||||||
this.indicatorAlignment = Alignment.topLeft,
|
this.indicatorAlignment = Alignment.topLeft,
|
||||||
this.onItemTap,
|
this.onItemTap,
|
||||||
this.onSelectionChange,
|
this.onSelectionChange,
|
||||||
|
this.onMaxExtentChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@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, int index, T metadata)? onItemTap;
|
||||||
final void Function(BuildContext context, Set<T> selected)? onSelectionChange;
|
final void Function(BuildContext context, Set<T> selected)? onSelectionChange;
|
||||||
|
final ValueChanged<double?>? onMaxExtentChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@npLog
|
@npLog
|
||||||
|
@ -90,32 +93,46 @@ class _SelectableItemListState<T extends SelectableItemMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context) {
|
||||||
return SliverStaggeredGrid.extentBuilder(
|
if (widget.onMaxExtentChange != null) {
|
||||||
key: ObjectKey(widget.maxCrossAxisExtent),
|
return MeasurableItemList(
|
||||||
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
key: _listKey,
|
||||||
itemCount: widget.items.length,
|
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
||||||
itemBuilder: (context, i) {
|
itemCount: widget.items.length,
|
||||||
final meta = widget.items[i];
|
itemBuilder: _buildItem,
|
||||||
if (meta.isSelectable) {
|
staggeredTileBuilder: (i) =>
|
||||||
return Selectable(
|
widget.staggeredTileBuilder(i, widget.items[i]),
|
||||||
isSelected: widget.selectedItems.contains(meta),
|
onMaxExtentChanged: widget.onMaxExtentChange,
|
||||||
iconSize: 32,
|
);
|
||||||
childBorderRadius:
|
} else {
|
||||||
widget.childBorderRadius ?? BorderRadius.circular(24),
|
return SliverStaggeredGrid.extentBuilder(
|
||||||
indicatorAlignment: widget.indicatorAlignment,
|
key: ObjectKey(widget.maxCrossAxisExtent),
|
||||||
onTap: _isSelecting
|
maxCrossAxisExtent: widget.maxCrossAxisExtent,
|
||||||
? () => _onItemSelect(context, i, meta)
|
itemCount: widget.items.length,
|
||||||
: () => _onItemTap(context, i, meta),
|
itemBuilder: _buildItem,
|
||||||
onLongPress: () => _onItemLongPress(i, meta),
|
staggeredTileBuilder: (i) =>
|
||||||
child: widget.itemBuilder(context, i, meta),
|
widget.staggeredTileBuilder(i, widget.items[i]),
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
return widget.itemBuilder(context, i, meta);
|
}
|
||||||
}
|
|
||||||
},
|
Widget _buildItem(BuildContext context, int index) {
|
||||||
staggeredTileBuilder: (i) =>
|
final meta = widget.items[index];
|
||||||
widget.staggeredTileBuilder(i, 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, 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) {
|
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");
|
"[_remapSelected] ${widget.selectedItems.length - newSelected.length} items not found in the new list");
|
||||||
}
|
}
|
||||||
widget.onSelectionChange?.call(context, newSelected);
|
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;
|
bool get _isSelecting => widget.selectedItems.isNotEmpty;
|
||||||
|
@ -222,4 +245,6 @@ class _SelectableItemListState<T extends SelectableItemMetadata>
|
||||||
final _keyboardFocus = FocusNode();
|
final _keyboardFocus = FocusNode();
|
||||||
int? _lastSelectPosition;
|
int? _lastSelectPosition;
|
||||||
bool _isKeyboardRangeSelecting = false;
|
bool _isKeyboardRangeSelecting = false;
|
||||||
|
|
||||||
|
final _listKey = GlobalKey();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1254,7 +1254,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: provider
|
name: provider
|
||||||
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
||||||
|
|
|
@ -136,6 +136,7 @@ dependencies:
|
||||||
page_view_indicators: ^2.0.0
|
page_view_indicators: ^2.0.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.15
|
path_provider: ^2.0.15
|
||||||
|
provider: any
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
screen_brightness: ^0.2.2
|
screen_brightness: ^0.2.2
|
||||||
shared_preferences: ^2.0.8
|
shared_preferences: ^2.0.8
|
||||||
|
|
|
@ -5,3 +5,4 @@ export 'src/iterator_extension.dart';
|
||||||
export 'src/list_extension.dart';
|
export 'src/list_extension.dart';
|
||||||
export 'src/list_util.dart';
|
export 'src/list_util.dart';
|
||||||
export 'src/map_extension.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