mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Build photo list in isolate
This commit is contained in:
parent
42495455b5
commit
e852075b01
17 changed files with 801 additions and 715 deletions
|
@ -1,7 +1,24 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations_en.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/widget/my_app.dart';
|
||||
|
||||
/// Simplify localized string access
|
||||
class L10n {
|
||||
static AppLocalizations global() => AppLocalizations.of(MyApp.globalContext)!;
|
||||
|
||||
static AppLocalizations of(Locale locale) {
|
||||
try {
|
||||
return lookupAppLocalizations(locale);
|
||||
} on FlutterError catch (_) {
|
||||
// unsupported locale, use default (en)
|
||||
return AppLocalizationsEn();
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[of] Failed while lookupAppLocalizations", e, stackTrace);
|
||||
return AppLocalizationsEn();
|
||||
}
|
||||
}
|
||||
|
||||
static final _log = Logger("app_localizations.L10n");
|
||||
}
|
||||
|
|
40
app/lib/compute_queue.dart
Normal file
40
app/lib/compute_queue.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef ComputeQueueCallback<U> = void Function(U result);
|
||||
|
||||
/// Compute the jobs in the queue one by one sequentially in isolate
|
||||
class ComputeQueue<T, U> {
|
||||
void addJob(T event, ComputeCallback<T, U> callback,
|
||||
ComputeQueueCallback<U> onResult) {
|
||||
_queue.addLast(_Job(event, callback, onResult));
|
||||
if (_queue.length == 1) {
|
||||
_startProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isProcessing => _queue.isNotEmpty;
|
||||
|
||||
Future<void> _startProcessing() async {
|
||||
while (_queue.isNotEmpty) {
|
||||
final ev = _queue.first;
|
||||
try {
|
||||
final result = await compute(ev.callback, ev.message);
|
||||
ev.onResult(result);
|
||||
} finally {
|
||||
_queue.removeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _queue = Queue<_Job<T, U>>();
|
||||
}
|
||||
|
||||
class _Job<T, U> {
|
||||
const _Job(this.message, this.callback, this.onResult);
|
||||
|
||||
final T message;
|
||||
final ComputeCallback<T, U> callback;
|
||||
final ComputeQueueCallback<U> onResult;
|
||||
}
|
|
@ -9,6 +9,9 @@ abstract class LocalFile with EquatableMixin {
|
|||
/// careful that this does NOT mean that the two objects are identical
|
||||
bool compareIdentity(LocalFile other);
|
||||
|
||||
/// hashCode to be used with [compareIdentity]
|
||||
int get identityHashCode;
|
||||
|
||||
String get logTag;
|
||||
|
||||
String get filename;
|
||||
|
@ -41,6 +44,9 @@ class LocalUriFile with EquatableMixin implements LocalFile {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
get identityHashCode => uri.hashCode;
|
||||
|
||||
@override
|
||||
toString() {
|
||||
var product = "$runtimeType {"
|
||||
|
|
|
@ -20,4 +20,7 @@ extension ObjectExtension<T> on T {
|
|||
Future<U> runFuture<U>(FutureOr<U> Function(T obj) fn) async {
|
||||
return await fn(this);
|
||||
}
|
||||
|
||||
/// Cast this as U, or null if this is not an object of U
|
||||
U? as<U>() => this is U ? this as U : null;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:nc_photos/event/event.dart';
|
|||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/session_storage.dart';
|
||||
|
@ -117,6 +118,11 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<_ListItem>()?.onTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
get canEdit => _album?.albumFile?.isOwned(widget.account.username) == true;
|
||||
|
@ -888,19 +894,18 @@ enum _SelectionMenuOption {
|
|||
abstract class _ListItem implements SelectableItem, DraggableItem {
|
||||
const _ListItem({
|
||||
required this.index,
|
||||
VoidCallback? onTap,
|
||||
this.onTap,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : _onTap = onTap,
|
||||
_onDropBefore = onDropBefore,
|
||||
}) : _onDropBefore = onDropBefore,
|
||||
_onDropAfter = onDropAfter,
|
||||
_onDragStarted = onDragStarted,
|
||||
_onDragEndedAny = onDragEndedAny;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
get isTappable => onTap != null;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
@ -935,7 +940,7 @@ abstract class _ListItem implements SelectableItem, DraggableItem {
|
|||
|
||||
final int index;
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
final VoidCallback? onTap;
|
||||
final DragTargetAccept<DraggableItem>? _onDropBefore;
|
||||
final DragTargetAccept<DraggableItem>? _onDropAfter;
|
||||
final VoidCallback? _onDragStarted;
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import 'dart:ui';
|
||||
|
||||
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:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_db.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/scan_account_dir.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file/data_source.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/update_property.dart';
|
||||
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
||||
|
@ -81,6 +83,18 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Viewer.routeName,
|
||||
arguments:
|
||||
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ScanAccountDirBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
|
@ -96,7 +110,9 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ScanAccountDirBlocState state) {
|
||||
if (state is ScanAccountDirBlocSuccess && itemStreamListItems.isEmpty) {
|
||||
if (state is ScanAccountDirBlocSuccess &&
|
||||
!_buildItemQueue.isProcessing &&
|
||||
itemStreamListItems.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
AppBar(
|
||||
|
@ -132,7 +148,8 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
),
|
||||
),
|
||||
),
|
||||
if (state is ScanAccountDirBlocLoading)
|
||||
if (state is ScanAccountDirBlocLoading ||
|
||||
_buildItemQueue.isProcessing)
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LinearProgressIndicator(),
|
||||
|
@ -207,11 +224,6 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
}
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
Navigator.pushNamed(context, Viewer.routeName,
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, index));
|
||||
}
|
||||
|
||||
Future<void> _onSelectionAppBarUnarchivePressed() async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global()
|
||||
|
@ -219,7 +231,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -254,37 +266,25 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
}
|
||||
|
||||
void _transformItems(List<File> files) {
|
||||
_backingFiles = files
|
||||
.where((f) => f.isArchived == true)
|
||||
.sorted(compareFileDateTimeDescending);
|
||||
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
|
||||
final previewUrl = api_util.getFilePreviewUrl(widget.account, f,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(f)) {
|
||||
yield _ImageListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||
yield _VideoListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_transformItems] Unsupported file format: ${f.contentType}");
|
||||
_buildItemQueue.addJob(
|
||||
PhotoListItemBuilderArguments(
|
||||
widget.account,
|
||||
files,
|
||||
isArchived: true,
|
||||
sorter: photoListFileDateTimeSorter,
|
||||
locale: language_util.getSelectedLocale() ??
|
||||
PlatformDispatcher.instance.locale,
|
||||
),
|
||||
buildPhotoListItem,
|
||||
(result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
itemStreamListItems = result.listItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _reqQuery() {
|
||||
|
@ -295,83 +295,11 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
||||
var _thumbZoomLevel = 0;
|
||||
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||
|
||||
static final _log = Logger("widget.archive_browser._ArchiveBrowserState");
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is _FileListItem && file.path == other.file.path;
|
||||
}
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
|
183
app/lib/widget/builder/photo_list_item_builder.dart
Normal file
183
app/lib/widget/builder/photo_list_item_builder.dart
Normal file
|
@ -0,0 +1,183 @@
|
|||
import 'package:collection/collection.dart' show compareNatural;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
import 'package:nc_photos/widget/photo_list_util.dart';
|
||||
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||
|
||||
class PhotoListItemBuilderArguments {
|
||||
const PhotoListItemBuilderArguments(
|
||||
this.account,
|
||||
this.files, {
|
||||
this.isArchived = false,
|
||||
required this.sorter,
|
||||
this.grouper,
|
||||
this.shouldBuildSmartAlbums = false,
|
||||
this.shouldShowFavoriteBadge = false,
|
||||
required this.locale,
|
||||
});
|
||||
|
||||
final Account account;
|
||||
final List<File> files;
|
||||
final bool isArchived;
|
||||
final PhotoListItemSorter? sorter;
|
||||
final PhotoListItemGrouper? grouper;
|
||||
final bool shouldBuildSmartAlbums;
|
||||
final bool shouldShowFavoriteBadge;
|
||||
|
||||
/// Locale is needed to get localized string
|
||||
final Locale locale;
|
||||
}
|
||||
|
||||
class PhotoListItemBuilderResult {
|
||||
const PhotoListItemBuilderResult(
|
||||
this.backingFiles,
|
||||
this.listItems, {
|
||||
this.smartAlbums = const [],
|
||||
});
|
||||
|
||||
final List<File> backingFiles;
|
||||
final List<SelectableItem> listItems;
|
||||
final List<Album> smartAlbums;
|
||||
}
|
||||
|
||||
typedef PhotoListItemSorter = int Function(File, File);
|
||||
|
||||
abstract class PhotoListItemGrouper {
|
||||
const PhotoListItemGrouper();
|
||||
|
||||
SelectableItem? onFile(File file);
|
||||
}
|
||||
|
||||
class PhotoListFileDateGrouper implements PhotoListItemGrouper {
|
||||
PhotoListFileDateGrouper({
|
||||
required this.isMonthOnly,
|
||||
}) : helper = DateGroupHelper(isMonthOnly: isMonthOnly);
|
||||
|
||||
@override
|
||||
onFile(File file) => helper
|
||||
.onFile(file)
|
||||
?.run((date) => PhotoListDateItem(date: date, isMonthOnly: isMonthOnly));
|
||||
|
||||
final bool isMonthOnly;
|
||||
final DateGroupHelper helper;
|
||||
}
|
||||
|
||||
int photoListFileDateTimeSorter(File a, File b) =>
|
||||
compareFileDateTimeDescending(a, b);
|
||||
|
||||
int photoListFilenameSorter(File a, File b) =>
|
||||
compareNatural(b.filename, a.filename);
|
||||
|
||||
PhotoListItemBuilderResult buildPhotoListItem(
|
||||
PhotoListItemBuilderArguments arg) {
|
||||
app_init.initLog();
|
||||
return _PhotoListItemBuilder(
|
||||
isArchived: arg.isArchived,
|
||||
sorter: arg.sorter,
|
||||
grouper: arg.grouper,
|
||||
shouldBuildSmartAlbums: arg.shouldBuildSmartAlbums,
|
||||
shouldShowFavoriteBadge: arg.shouldShowFavoriteBadge,
|
||||
locale: arg.locale,
|
||||
)(arg.account, arg.files);
|
||||
}
|
||||
|
||||
class _PhotoListItemBuilder {
|
||||
const _PhotoListItemBuilder({
|
||||
required this.isArchived,
|
||||
required this.sorter,
|
||||
required this.grouper,
|
||||
required this.shouldBuildSmartAlbums,
|
||||
required this.shouldShowFavoriteBadge,
|
||||
required this.locale,
|
||||
});
|
||||
|
||||
PhotoListItemBuilderResult call(Account account, List<File> files) {
|
||||
final s = Stopwatch()..start();
|
||||
try {
|
||||
return _fromSortedItems(account, _sortItems(files));
|
||||
} finally {
|
||||
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||
}
|
||||
}
|
||||
|
||||
List<File> _sortItems(List<File> files) {
|
||||
final filtered = files.where((f) => (f.isArchived ?? false) == isArchived);
|
||||
if (sorter == null) {
|
||||
return filtered.toList();
|
||||
} else {
|
||||
return filtered.stableSorted(sorter);
|
||||
}
|
||||
}
|
||||
|
||||
PhotoListItemBuilderResult _fromSortedItems(
|
||||
Account account, List<File> files) {
|
||||
final today = DateTime.now();
|
||||
final memoryAlbumHelper =
|
||||
shouldBuildSmartAlbums ? MemoryAlbumHelper(today) : null;
|
||||
final listItems = <SelectableItem>[];
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
final f = files[i];
|
||||
grouper?.onFile(f)?.run((item) => listItems.add(item));
|
||||
memoryAlbumHelper?.addFile(f);
|
||||
|
||||
final item = _buildListItem(i, account, f);
|
||||
if (item != null) {
|
||||
listItems.add(item);
|
||||
}
|
||||
}
|
||||
final smartAlbums = memoryAlbumHelper
|
||||
?.build((year) => L10n.of(locale).memoryAlbumName(today.year - year));
|
||||
return PhotoListItemBuilderResult(
|
||||
files,
|
||||
listItems,
|
||||
smartAlbums: smartAlbums ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
SelectableItem? _buildListItem(int i, Account account, File file) {
|
||||
final previewUrl = api_util.getFilePreviewUrl(account, file,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(file)) {
|
||||
return PhotoListImageItem(
|
||||
fileIndex: i,
|
||||
file: file,
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(file)) {
|
||||
return PhotoListVideoItem(
|
||||
fileIndex: i,
|
||||
file: file,
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_buildListItem] Unsupported file format: ${file.contentType}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final bool isArchived;
|
||||
final PhotoListItemSorter? sorter;
|
||||
final PhotoListItemGrouper? grouper;
|
||||
final bool shouldBuildSmartAlbums;
|
||||
final bool shouldShowFavoriteBadge;
|
||||
final Locale locale;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.builder.photo_list_item_builder._PhotoListItemBuilder");
|
||||
}
|
|
@ -20,6 +20,7 @@ import 'package:nc_photos/event/event.dart';
|
|||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
|
@ -111,6 +112,11 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<_ListItem>()?.onTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
get canEdit => _album?.albumFile?.isOwned(widget.account.username) == true;
|
||||
|
@ -654,11 +660,11 @@ enum _SelectionMenuOption {
|
|||
abstract class _ListItem implements SelectableItem {
|
||||
const _ListItem({
|
||||
required this.index,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
get isTappable => onTap != null;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
@ -675,7 +681,7 @@ abstract class _ListItem implements SelectableItem {
|
|||
|
||||
final int index;
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
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:logging/logging.dart';
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/scan_local_dir.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/android_info.dart';
|
||||
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
||||
import 'package:nc_photos/mobile/android/permission_util.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
|
@ -20,6 +21,7 @@ import 'package:nc_photos/theme.dart';
|
|||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
import 'package:nc_photos/widget/handler/delete_local_selection_handler.dart';
|
||||
import 'package:nc_photos/widget/local_file_viewer.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_stream_list_mixin.dart';
|
||||
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||||
|
@ -91,6 +93,17 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListLocalFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
LocalFileViewer.routeName,
|
||||
arguments: LocalFileViewerArguments(_backingFiles, fileItem.fileIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ScanLocalDirBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
|
@ -123,6 +136,7 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
],
|
||||
);
|
||||
} else if (state is ScanLocalDirBlocSuccess &&
|
||||
!_buildItemQueue.isProcessing &&
|
||||
itemStreamListItems.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
|
@ -159,7 +173,7 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
),
|
||||
),
|
||||
),
|
||||
if (state is ScanLocalDirBlocLoading)
|
||||
if (state is ScanLocalDirBlocLoading || _buildItemQueue.isProcessing)
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LinearProgressIndicator(),
|
||||
|
@ -237,7 +251,7 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
|
||||
Future<void> _onSelectionSharePressed(BuildContext context) async {
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListLocalFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
await ShareHandler(
|
||||
|
@ -285,7 +299,7 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
}
|
||||
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListLocalFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -294,30 +308,18 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
await const DeleteLocalSelectionHandler()(selectedFiles: selectedFiles);
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
Navigator.pushNamed(context, LocalFileViewer.routeName,
|
||||
arguments: LocalFileViewerArguments(_backingFiles, index));
|
||||
}
|
||||
|
||||
void _transformItems(List<LocalFile> files) {
|
||||
// we use last modified here to keep newly enhanced photo at the top
|
||||
_backingFiles =
|
||||
files.stableSorted((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
if (file_util.isSupportedImageMime(f.mime ?? "")) {
|
||||
yield _ImageListItem(
|
||||
file: f,
|
||||
onTap: () => _onItemTap(i),
|
||||
_buildItemQueue.addJob(
|
||||
_BuilderArguments(files),
|
||||
_buildPhotoListItem,
|
||||
(result) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
itemStreamListItems = result.listItems;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
_log.info("[_transformItems] Length: ${itemStreamListItems.length}");
|
||||
}
|
||||
|
||||
void _openInitialImage(String filename) {
|
||||
final index = _backingFiles.indexWhere((f) => f.filename == filename);
|
||||
|
@ -361,6 +363,8 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
|
||||
var _backingFiles = <LocalFile>[];
|
||||
|
||||
final _buildItemQueue = ComputeQueue<_BuilderArguments, _BuilderResult>();
|
||||
|
||||
var _isFirstRun = true;
|
||||
var _thumbZoomLevel = 0;
|
||||
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||
|
@ -370,76 +374,67 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
Logger("widget.enhanced_photo_browser._EnhancedPhotoBrowserState");
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
final LocalFile file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
required LocalFile file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
final ImageProvider provider;
|
||||
if (file is LocalUriFile) {
|
||||
provider = ContentUriImage((file as LocalUriFile).uri);
|
||||
} else {
|
||||
throw ArgumentError("Invalid file");
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: Container(
|
||||
// arbitrary size here
|
||||
constraints: BoxConstraints.tight(const Size(128, 128)),
|
||||
color: AppTheme.getListItemBackgroundColor(context),
|
||||
child: Image(
|
||||
image: ResizeImage.resizeIfNeeded(k.photoThumbSize, null, provider),
|
||||
filterQuality: FilterQuality.high,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, e, stackTrace) {
|
||||
return Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 64,
|
||||
color: Colors.white.withOpacity(.8),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _SelectionMenuOption {
|
||||
delete,
|
||||
}
|
||||
|
||||
class _BuilderResult {
|
||||
const _BuilderResult(this.backingFiles, this.listItems);
|
||||
|
||||
final List<LocalFile> backingFiles;
|
||||
final List<SelectableItem> listItems;
|
||||
}
|
||||
|
||||
class _BuilderArguments {
|
||||
const _BuilderArguments(this.files);
|
||||
|
||||
final List<LocalFile> files;
|
||||
}
|
||||
|
||||
class _Builder {
|
||||
_BuilderResult call(List<LocalFile> files) {
|
||||
final s = Stopwatch()..start();
|
||||
try {
|
||||
return _fromSortedItems(_sortItems(files));
|
||||
} finally {
|
||||
_log.info("[call] Elapsed time: ${s.elapsedMilliseconds}ms");
|
||||
}
|
||||
}
|
||||
|
||||
List<LocalFile> _sortItems(List<LocalFile> files) {
|
||||
// we use last modified here to keep newly enhanced photo at the top
|
||||
return files
|
||||
.stableSorted((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
}
|
||||
|
||||
_BuilderResult _fromSortedItems(List<LocalFile> files) {
|
||||
final listItems = <SelectableItem>[];
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
final f = files[i];
|
||||
final item = _buildListItem(i, f);
|
||||
if (item != null) {
|
||||
listItems.add(item);
|
||||
}
|
||||
}
|
||||
return _BuilderResult(files, listItems);
|
||||
}
|
||||
|
||||
SelectableItem? _buildListItem(int i, LocalFile file) {
|
||||
if (file_util.isSupportedImageMime(file.mime ?? "")) {
|
||||
return PhotoListLocalImageItem(
|
||||
fileIndex: i,
|
||||
file: file,
|
||||
);
|
||||
} else {
|
||||
_log.shout("[_buildListItem] Unsupported file format: ${file.mime}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static final _log = Logger("widget.enhanced_photo_browser._Builder");
|
||||
}
|
||||
|
||||
_BuilderResult _buildPhotoListItem(_BuilderArguments arg) {
|
||||
app_init.initLog();
|
||||
return _Builder()(arg.files);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import 'dart:ui';
|
||||
|
||||
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:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/list_favorite.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
|
||||
import 'package:nc_photos/widget/handler/archive_selection_handler.dart';
|
||||
|
@ -84,6 +86,18 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Viewer.routeName,
|
||||
arguments:
|
||||
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ListFavoriteBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
|
@ -99,7 +113,9 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ListFavoriteBlocState state) {
|
||||
if (state is ListFavoriteBlocSuccess && itemStreamListItems.isEmpty) {
|
||||
if (state is ListFavoriteBlocSuccess &&
|
||||
!_buildItemQueue.isProcessing &&
|
||||
itemStreamListItems.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
AppBar(
|
||||
|
@ -142,7 +158,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
),
|
||||
),
|
||||
),
|
||||
if (state is ListFavoriteBlocLoading)
|
||||
if (state is ListFavoriteBlocLoading || _buildItemQueue.isProcessing)
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LinearProgressIndicator(),
|
||||
|
@ -211,9 +227,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
minZoom: -1,
|
||||
maxZoom: 2,
|
||||
onZoomChanged: (value) {
|
||||
setState(() {
|
||||
_setThumbZoomLevel(value.round());
|
||||
});
|
||||
Pref().setHomePhotosZoomLevel(_thumbZoomLevel);
|
||||
},
|
||||
),
|
||||
|
@ -236,11 +250,6 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
}
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
Navigator.pushNamed(context, Viewer.routeName,
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, index));
|
||||
}
|
||||
|
||||
void _onRefreshSelected() {
|
||||
_reqRefresh();
|
||||
}
|
||||
|
@ -265,7 +274,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
|
@ -283,7 +292,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
clearSelection: () {
|
||||
|
@ -298,7 +307,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
|
@ -309,7 +318,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -323,7 +332,7 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -336,46 +345,26 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
void _transformItems(List<File> files) {
|
||||
_backingFiles = files
|
||||
.where((f) => f.isArchived != true)
|
||||
.sorted(compareFileDateTimeDescending);
|
||||
|
||||
final isMonthOnly = _thumbZoomLevel < 0;
|
||||
final dateHelper = photo_list_util.DateGroupHelper(
|
||||
isMonthOnly: isMonthOnly,
|
||||
);
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
final date = dateHelper.onFile(f);
|
||||
if (date != null) {
|
||||
yield _DateListItem(date: date, isMonthOnly: isMonthOnly);
|
||||
void _transformItems(List<File> files, {bool isSorted = false}) {
|
||||
_buildItemQueue.addJob(
|
||||
PhotoListItemBuilderArguments(
|
||||
widget.account,
|
||||
files,
|
||||
sorter: isSorted ? null : photoListFileDateTimeSorter,
|
||||
grouper: PhotoListFileDateGrouper(isMonthOnly: _thumbZoomLevel < 0),
|
||||
locale: language_util.getSelectedLocale() ??
|
||||
PlatformDispatcher.instance.locale,
|
||||
),
|
||||
buildPhotoListItem,
|
||||
(result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
itemStreamListItems = result.listItems;
|
||||
});
|
||||
}
|
||||
|
||||
final previewUrl = api_util.getFilePreviewUrl(widget.account, f,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(f)) {
|
||||
yield _ImageListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
},
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||
yield _VideoListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_transformItems] Unsupported file format: ${f.contentType}");
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _reqQuery() {
|
||||
|
@ -397,9 +386,13 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
void _setThumbZoomLevel(int level) {
|
||||
final prevLevel = _thumbZoomLevel;
|
||||
_thumbZoomLevel = level;
|
||||
if ((prevLevel >= 0) != (level >= 0)) {
|
||||
_transformItems(_backingFiles);
|
||||
_thumbZoomLevel = level;
|
||||
_transformItems(_backingFiles, isSorted: true);
|
||||
} else {
|
||||
setState(() {
|
||||
_thumbZoomLevel = level;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,111 +400,15 @@ class _FavoriteBrowserState extends State<FavoriteBrowser>
|
|||
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
||||
var _thumbZoomLevel = 0;
|
||||
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||
|
||||
static final _log = Logger("widget.archive_browser._FavoriteBrowserState");
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
class _DateListItem extends _ListItem {
|
||||
_DateListItem({
|
||||
required this.date,
|
||||
this.isMonthOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
get isSelectable => false;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.extent(99, 32);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListDate(
|
||||
date: date,
|
||||
isMonthOnly: isMonthOnly,
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime date;
|
||||
final bool isMonthOnly;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is _FileListItem && file.path == other.file.path;
|
||||
}
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
enum _SelectionMenuOption {
|
||||
archive,
|
||||
delete,
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/platform/features.dart' as features;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
@ -82,6 +83,11 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<_ListItem>()?.onTap?.call();
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ListAlbumBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
|
@ -547,6 +553,8 @@ abstract class _ListItem implements SelectableItem {
|
|||
}) : _myOnTap = onTap;
|
||||
|
||||
@override
|
||||
get isTappable => _myOnTap != null;
|
||||
|
||||
get onTap => _myOnTap;
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart' show compareNatural;
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.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:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
|
@ -13,6 +11,7 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
|
|||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
|
||||
import 'package:nc_photos/bloc/scan_account_dir.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
|
@ -21,9 +20,10 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
|
|||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/metadata_task_manager.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/primitive.dart';
|
||||
|
@ -33,6 +33,7 @@ import 'package:nc_photos/snack_bar_manager.dart';
|
|||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/sync_favorite.dart';
|
||||
import 'package:nc_photos/widget/album_browser_util.dart' as album_browser_util;
|
||||
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/handler/add_selection_to_album_handler.dart';
|
||||
import 'package:nc_photos/widget/handler/archive_selection_handler.dart';
|
||||
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
|
||||
|
@ -92,6 +93,18 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Viewer.routeName,
|
||||
arguments:
|
||||
ViewerArguments(widget.account, _backingFiles, fileItem.fileIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
if (_bloc.state is ScanAccountDirBlocInit) {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
|
@ -168,7 +181,8 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state is ScanAccountDirBlocLoading)
|
||||
if (state is ScanAccountDirBlocLoading ||
|
||||
_buildItemQueue.isProcessing)
|
||||
const LinearProgressIndicator(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
@ -254,9 +268,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
minZoom: -1,
|
||||
maxZoom: 2,
|
||||
onZoomChanged: (value) {
|
||||
setState(() {
|
||||
_setThumbZoomLevel(value.round());
|
||||
});
|
||||
Pref().setHomePhotosZoomLevel(_thumbZoomLevel);
|
||||
},
|
||||
),
|
||||
|
@ -329,11 +341,6 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
Navigator.pushNamed(context, Viewer.routeName,
|
||||
arguments: ViewerArguments(widget.account, _backingFiles, index));
|
||||
}
|
||||
|
||||
void _onRefreshSelected() {
|
||||
_hasFiredMetadataTask.value = false;
|
||||
_reqRefresh();
|
||||
|
@ -359,7 +366,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
|
@ -377,7 +384,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
clearSelection: () {
|
||||
|
@ -392,7 +399,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
|
@ -403,7 +410,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -417,7 +424,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -440,9 +447,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
} else if (ev.key == PrefKey.isPhotosTabSortByName) {
|
||||
if (_bloc.state is! ScanAccountDirBlocInit) {
|
||||
_log.info("[_onPrefUpdated] Update view after changing sort option");
|
||||
setState(() {
|
||||
_transformItems(_bloc.state.files);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -491,83 +496,40 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
/// Transform a File list to grid items
|
||||
void _transformItems(List<File> files) {
|
||||
if (!Pref().isPhotosTabSortByNameOr()) {
|
||||
_transformItemsByDate(files);
|
||||
void _transformItems(List<File> files, {bool isSorted = false}) {
|
||||
_log.info("[_transformItems] Queue ${files.length} items");
|
||||
final PhotoListItemSorter? sorter;
|
||||
final PhotoListItemGrouper? grouper;
|
||||
if (Pref().isPhotosTabSortByNameOr()) {
|
||||
sorter = isSorted ? null : photoListFilenameSorter;
|
||||
grouper = null;
|
||||
} else {
|
||||
_transformItemsByName(files);
|
||||
}
|
||||
sorter = isSorted ? null : photoListFileDateTimeSorter;
|
||||
grouper = PhotoListFileDateGrouper(isMonthOnly: _thumbZoomLevel < 0);
|
||||
}
|
||||
|
||||
void _transformItemsByName(List<File> files) {
|
||||
_backingFiles = files
|
||||
.where((f) => f.isArchived != true)
|
||||
.sorted((a, b) => compareNatural(b.filename, a.filename));
|
||||
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final item = _transformItemToListItem(i, _backingFiles[i]);
|
||||
if (item != null) {
|
||||
yield item;
|
||||
_buildItemQueue.addJob(
|
||||
PhotoListItemBuilderArguments(
|
||||
widget.account,
|
||||
files,
|
||||
sorter: sorter,
|
||||
grouper: grouper,
|
||||
shouldBuildSmartAlbums: true,
|
||||
shouldShowFavoriteBadge: true,
|
||||
locale: language_util.getSelectedLocale() ??
|
||||
PlatformDispatcher.instance.locale,
|
||||
),
|
||||
buildPhotoListItem,
|
||||
(result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
itemStreamListItems = result.listItems;
|
||||
_smartAlbums = result.smartAlbums;
|
||||
});
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _transformItemsByDate(List<File> files) {
|
||||
_backingFiles = files
|
||||
.where((f) => f.isArchived != true)
|
||||
.sorted(compareFileDateTimeDescending);
|
||||
|
||||
final isMonthOnly = _thumbZoomLevel < 0;
|
||||
final dateHelper = photo_list_util.DateGroupHelper(
|
||||
isMonthOnly: isMonthOnly,
|
||||
},
|
||||
);
|
||||
final today = DateTime.now();
|
||||
final memoryAlbumHelper = photo_list_util.MemoryAlbumHelper(today);
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
final date = dateHelper.onFile(f);
|
||||
if (date != null) {
|
||||
yield _DateListItem(date: date, isMonthOnly: isMonthOnly);
|
||||
}
|
||||
memoryAlbumHelper.addFile(f);
|
||||
|
||||
final item = _transformItemToListItem(i, f);
|
||||
if (item != null) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
_smartAlbums = memoryAlbumHelper
|
||||
.build((year) => L10n.global().memoryAlbumName(today.year - year));
|
||||
}
|
||||
|
||||
_ListItem? _transformItemToListItem(int i, File f) {
|
||||
final previewUrl = api_util.getFilePreviewUrl(widget.account, f,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(f)) {
|
||||
return _ImageListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||
return _VideoListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_transformItemToListItem] Unsupported file format: ${f.contentType}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _reqQuery() {
|
||||
|
@ -589,9 +551,13 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
void _setThumbZoomLevel(int level) {
|
||||
final prevLevel = _thumbZoomLevel;
|
||||
_thumbZoomLevel = level;
|
||||
if ((prevLevel >= 0) != (level >= 0)) {
|
||||
_transformItems(_backingFiles);
|
||||
_thumbZoomLevel = level;
|
||||
_transformItems(_backingFiles, isSorted: true);
|
||||
} else {
|
||||
setState(() {
|
||||
_thumbZoomLevel = level;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -664,6 +630,9 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
var _backingFiles = <File>[];
|
||||
var _smartAlbums = <Album>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
||||
var _thumbZoomLevel = 0;
|
||||
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||
|
||||
|
@ -854,107 +823,6 @@ class _Web {
|
|||
static const _metadataTaskHeaderHeight = 32.0;
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
class _DateListItem extends _ListItem {
|
||||
_DateListItem({
|
||||
required this.date,
|
||||
this.isMonthOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
get isSelectable => false;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.extent(99, 32);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListDate(
|
||||
date: date,
|
||||
isMonthOnly: isMonthOnly,
|
||||
);
|
||||
}
|
||||
|
||||
final DateTime date;
|
||||
final bool isMonthOnly;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is _FileListItem && file.path == other.file.path;
|
||||
}
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
isFavorite: file.isFavorite == true,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isFavorite: file.isFavorite == true,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _MetadataTaskHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
const _MetadataTaskHeaderDelegate({
|
||||
required this.extent,
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'package:nc_photos/event/event.dart';
|
|||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
@ -106,6 +107,11 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<_ListItem>()?.onTap?.call();
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
_reqQuery();
|
||||
|
@ -463,11 +469,11 @@ class _ListItem implements SelectableItem {
|
|||
required this.file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
get isTappable => onTap != null;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
@ -503,7 +509,7 @@ class _ListItem implements SelectableItem {
|
|||
final File file;
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
final VoidCallback? _onTap;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
enum _SelectionMenuOption {
|
||||
|
|
|
@ -1,12 +1,194 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||
|
||||
abstract class PhotoListFileItem extends SelectableItem {
|
||||
const PhotoListFileItem({
|
||||
required this.fileIndex,
|
||||
required this.file,
|
||||
required this.shouldShowFavoriteBadge,
|
||||
});
|
||||
|
||||
@override
|
||||
get isTappable => true;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
operator ==(Object other) =>
|
||||
other is PhotoListFileItem && file.compareServerIdentity(other.file);
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
|
||||
@override
|
||||
toString() => "$runtimeType {"
|
||||
"fileIndex: $fileIndex, "
|
||||
"file: ${file.path}, "
|
||||
"shouldShowFavoriteBadge: $shouldShowFavoriteBadge, "
|
||||
"}";
|
||||
|
||||
final int fileIndex;
|
||||
final File file;
|
||||
final bool shouldShowFavoriteBadge;
|
||||
}
|
||||
|
||||
class PhotoListImageItem extends PhotoListFileItem {
|
||||
const PhotoListImageItem({
|
||||
required int fileIndex,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
required bool shouldShowFavoriteBadge,
|
||||
}) : super(
|
||||
fileIndex: fileIndex,
|
||||
file: file,
|
||||
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
||||
);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) => PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true,
|
||||
);
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class PhotoListVideoItem extends PhotoListFileItem {
|
||||
const PhotoListVideoItem({
|
||||
required int fileIndex,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
required bool shouldShowFavoriteBadge,
|
||||
}) : super(
|
||||
fileIndex: fileIndex,
|
||||
file: file,
|
||||
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
||||
);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) => PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true,
|
||||
);
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class PhotoListDateItem extends SelectableItem {
|
||||
const PhotoListDateItem({
|
||||
required this.date,
|
||||
this.isMonthOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
get isTappable => false;
|
||||
|
||||
@override
|
||||
get isSelectable => false;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.extent(99, 32);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) => PhotoListDate(
|
||||
date: date,
|
||||
isMonthOnly: isMonthOnly,
|
||||
);
|
||||
|
||||
final DateTime date;
|
||||
final bool isMonthOnly;
|
||||
}
|
||||
|
||||
abstract class PhotoListLocalFileItem extends SelectableItem {
|
||||
const PhotoListLocalFileItem({
|
||||
required this.fileIndex,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
get isTappable => true;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
operator ==(Object other) =>
|
||||
other is PhotoListLocalFileItem && file.compareIdentity(other.file);
|
||||
|
||||
@override
|
||||
get hashCode => file.identityHashCode;
|
||||
|
||||
final int fileIndex;
|
||||
final LocalFile file;
|
||||
}
|
||||
|
||||
class PhotoListLocalImageItem extends PhotoListLocalFileItem {
|
||||
const PhotoListLocalImageItem({
|
||||
required int fileIndex,
|
||||
required LocalFile file,
|
||||
}) : super(
|
||||
fileIndex: fileIndex,
|
||||
file: file,
|
||||
);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
final ImageProvider provider;
|
||||
if (file is LocalUriFile) {
|
||||
provider = ContentUriImage((file as LocalUriFile).uri);
|
||||
} else {
|
||||
throw ArgumentError("Invalid file");
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: Container(
|
||||
// arbitrary size here
|
||||
constraints: BoxConstraints.tight(const Size(128, 128)),
|
||||
color: AppTheme.getListItemBackgroundColor(context),
|
||||
child: Image(
|
||||
image: ResizeImage.resizeIfNeeded(k.photoThumbSize, null, provider),
|
||||
filterQuality: FilterQuality.high,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, e, stackTrace) {
|
||||
return Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 64,
|
||||
color: Colors.white.withOpacity(.8),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PhotoListImage extends StatelessWidget {
|
||||
const PhotoListImage({
|
||||
|
|
|
@ -14,9 +14,11 @@ import 'package:nc_photos/widget/measurable_item_list.dart';
|
|||
import 'package:nc_photos/widget/selectable.dart';
|
||||
|
||||
abstract class SelectableItem {
|
||||
const SelectableItem();
|
||||
|
||||
Widget buildWidget(BuildContext context);
|
||||
|
||||
VoidCallback? get onTap => null;
|
||||
bool get isTappable => false;
|
||||
bool get isSelectable => false;
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
}
|
||||
|
@ -28,6 +30,9 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
_keyboardFocus.requestFocus();
|
||||
}
|
||||
|
||||
@protected
|
||||
void onItemTap(SelectableItem item, int index);
|
||||
|
||||
@protected
|
||||
Widget buildItemStreamListOuter(
|
||||
BuildContext context, {
|
||||
|
@ -170,7 +175,9 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
item.onTap?.call();
|
||||
if (item.isTappable) {
|
||||
onItemTap(item, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/entity/album/provider.dart';
|
|||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/preprocess_album.dart';
|
||||
|
@ -76,6 +77,11 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<_ListItem>()?.onTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
get canEdit => false;
|
||||
|
@ -324,11 +330,11 @@ enum _SelectionMenuOption {
|
|||
abstract class _ListItem implements SelectableItem {
|
||||
const _ListItem({
|
||||
required this.index,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
get isTappable => onTap != null;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
@ -345,7 +351,7 @@ abstract class _ListItem implements SelectableItem {
|
|||
|
||||
final int index;
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import 'dart:ui';
|
||||
|
||||
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:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/ls_trashbin.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/use_case/restore_trashbin.dart';
|
||||
import 'package:nc_photos/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
|
@ -82,6 +84,18 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
onItemTap(SelectableItem item, int index) {
|
||||
item.as<PhotoListFileItem>()?.run((fileItem) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
TrashbinViewer.routeName,
|
||||
arguments: TrashbinViewerArguments(
|
||||
widget.account, _backingFiles, fileItem.fileIndex),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _initBloc() {
|
||||
_bloc = LsTrashbinBloc.of(widget.account);
|
||||
if (_bloc.state is LsTrashbinBlocInit) {
|
||||
|
@ -99,7 +113,9 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, LsTrashbinBlocState state) {
|
||||
if (state is LsTrashbinBlocSuccess && itemStreamListItems.isEmpty) {
|
||||
if (state is LsTrashbinBlocSuccess &&
|
||||
!_buildItemQueue.isProcessing &&
|
||||
itemStreamListItems.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
AppBar(
|
||||
|
@ -135,7 +151,7 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
),
|
||||
),
|
||||
),
|
||||
if (state is LsTrashbinBlocLoading)
|
||||
if (state is LsTrashbinBlocLoading || _buildItemQueue.isProcessing)
|
||||
const Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: LinearProgressIndicator(),
|
||||
|
@ -250,12 +266,6 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
}
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
Navigator.pushNamed(context, TrashbinViewer.routeName,
|
||||
arguments:
|
||||
TrashbinViewerArguments(widget.account, _backingFiles, index));
|
||||
}
|
||||
|
||||
void _onEmptyTrashPressed(BuildContext context) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -282,7 +292,7 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -335,51 +345,29 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
}
|
||||
|
||||
void _transformItems(List<File> files) {
|
||||
_backingFiles = files.sorted((a, b) {
|
||||
if (a.trashbinDeletionTime == null && b.trashbinDeletionTime == null) {
|
||||
// ?
|
||||
return 0;
|
||||
} else if (a.trashbinDeletionTime == null) {
|
||||
return -1;
|
||||
} else if (b.trashbinDeletionTime == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return b.trashbinDeletionTime!.compareTo(a.trashbinDeletionTime!);
|
||||
}
|
||||
_buildItemQueue.addJob(
|
||||
PhotoListItemBuilderArguments(
|
||||
widget.account,
|
||||
files,
|
||||
sorter: _fileSorter,
|
||||
locale: language_util.getSelectedLocale() ??
|
||||
PlatformDispatcher.instance.locale,
|
||||
),
|
||||
buildPhotoListItem,
|
||||
(result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
itemStreamListItems = result.listItems;
|
||||
});
|
||||
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
|
||||
final previewUrl = api_util.getFilePreviewUrl(widget.account, f,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(f)) {
|
||||
yield _ImageListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||
yield _VideoListItem(
|
||||
file: f,
|
||||
account: widget.account,
|
||||
previewUrl: previewUrl,
|
||||
onTap: () => _onItemTap(i),
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_transformItems] Unsupported file format: ${f.contentType}");
|
||||
}
|
||||
}
|
||||
}()
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteSelected() async {
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
|
@ -404,87 +392,15 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
||||
var _thumbZoomLevel = 0;
|
||||
int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel);
|
||||
|
||||
static final _log = Logger("widget.trashbin_browser._TrashbinBrowserState");
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
get onTap => _onTap;
|
||||
|
||||
@override
|
||||
get isSelectable => true;
|
||||
|
||||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is _FileListItem && file.path == other.file.path;
|
||||
}
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
buildWidget(BuildContext context) {
|
||||
return PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
);
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String previewUrl;
|
||||
}
|
||||
|
||||
enum _AppBarMenuOption {
|
||||
empty,
|
||||
}
|
||||
|
@ -492,3 +408,16 @@ enum _AppBarMenuOption {
|
|||
enum _SelectionAppBarMenuOption {
|
||||
delete,
|
||||
}
|
||||
|
||||
int _fileSorter(File a, File b) {
|
||||
if (a.trashbinDeletionTime == null && b.trashbinDeletionTime == null) {
|
||||
// ?
|
||||
return 0;
|
||||
} else if (a.trashbinDeletionTime == null) {
|
||||
return -1;
|
||||
} else if (b.trashbinDeletionTime == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return b.trashbinDeletionTime!.compareTo(a.trashbinDeletionTime!);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue