Merge branch 'query-partial-file' into dev

This commit is contained in:
Ming Ming 2022-10-17 00:16:15 +08:00
commit b84ff7fbf9
78 changed files with 1288 additions and 631 deletions

View file

@ -2,7 +2,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
@ -12,7 +12,7 @@ const reservedFilenameChars = "<>:\"/\\|?*";
/// Return the preview image URL for [file]. See [getFilePreviewUrlRelative] /// Return the preview image URL for [file]. See [getFilePreviewUrlRelative]
String getFilePreviewUrl( String getFilePreviewUrl(
Account account, Account account,
File file, { FileDescriptor file, {
required int width, required int width,
required int height, required int height,
String? mode, String? mode,
@ -27,7 +27,7 @@ String getFilePreviewUrl(
/// cropped /// cropped
String getFilePreviewUrlRelative( String getFilePreviewUrlRelative(
Account account, Account account,
File file, { FileDescriptor file, {
required int width, required int width,
required int height, required int height,
String? mode, String? mode,
@ -36,14 +36,9 @@ String getFilePreviewUrlRelative(
String url; String url;
if (file_util.isTrash(account, file)) { if (file_util.isTrash(account, file)) {
// trashbin does not support preview.png endpoint // trashbin does not support preview.png endpoint
url = "index.php/apps/files_trashbin/preview?fileId=${file.fileId}"; url = "index.php/apps/files_trashbin/preview?fileId=${file.fdId}";
} else { } else {
if (file.fileId != null) { url = "index.php/core/preview?fileId=${file.fdId}";
url = "index.php/core/preview?fileId=${file.fileId}";
} else {
final filePath = Uri.encodeQueryComponent(file.strippedPath);
url = "index.php/core/preview.png?file=$filePath";
}
} }
url = "$url&x=$width&y=$height"; url = "$url&x=$width&y=$height";
@ -76,12 +71,12 @@ String getFilePreviewUrlByFileId(
return url; return url;
} }
String getFileUrl(Account account, File file) { String getFileUrl(Account account, FileDescriptor file) {
return "${account.url}/${getFileUrlRelative(file)}"; return "${account.url}/${getFileUrlRelative(file)}";
} }
String getFileUrlRelative(File file) { String getFileUrlRelative(FileDescriptor file) {
return file.path; return file.fdPath;
} }
String getWebdavRootUrlRelative(Account account) => String getWebdavRootUrlRelative(Account account) =>

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';

View file

@ -8,17 +8,17 @@ import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/event/native_event.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/throttler.dart'; import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/ls.dart'; import 'package:nc_photos/use_case/ls.dart';
import 'package:nc_photos/use_case/scan_dir.dart'; import 'package:nc_photos/use_case/scan_dir.dart';
import 'package:nc_photos/use_case/scan_dir_offline.dart'; import 'package:nc_photos/use_case/scan_dir_offline.dart';
import 'package:nc_photos/use_case/sync_dir.dart';
abstract class ScanAccountDirBlocEvent { abstract class ScanAccountDirBlocEvent {
const ScanAccountDirBlocEvent(); const ScanAccountDirBlocEvent();
@ -63,7 +63,7 @@ abstract class ScanAccountDirBlocState {
"}"; "}";
} }
final List<File> files; final List<FileDescriptor> files;
} }
class ScanAccountDirBlocInit extends ScanAccountDirBlocState { class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
@ -71,15 +71,15 @@ class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
} }
class ScanAccountDirBlocLoading extends ScanAccountDirBlocState { class ScanAccountDirBlocLoading extends ScanAccountDirBlocState {
const ScanAccountDirBlocLoading(List<File> files) : super(files); const ScanAccountDirBlocLoading(List<FileDescriptor> files) : super(files);
} }
class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState { class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState {
const ScanAccountDirBlocSuccess(List<File> files) : super(files); const ScanAccountDirBlocSuccess(List<FileDescriptor> files) : super(files);
} }
class ScanAccountDirBlocFailure extends ScanAccountDirBlocState { class ScanAccountDirBlocFailure extends ScanAccountDirBlocState {
const ScanAccountDirBlocFailure(List<File> files, this.exception) const ScanAccountDirBlocFailure(List<FileDescriptor> files, this.exception)
: super(files); : super(files);
@override @override
@ -96,7 +96,8 @@ class ScanAccountDirBlocFailure extends ScanAccountDirBlocState {
/// The state of this bloc is inconsistent. This typically means that the data /// The state of this bloc is inconsistent. This typically means that the data
/// may have been changed externally /// may have been changed externally
class ScanAccountDirBlocInconsistent extends ScanAccountDirBlocState { class ScanAccountDirBlocInconsistent extends ScanAccountDirBlocState {
const ScanAccountDirBlocInconsistent(List<File> files) : super(files); const ScanAccountDirBlocInconsistent(List<FileDescriptor> files)
: super(files);
} }
/// A bloc that return all files under a dir recursively /// A bloc that return all files under a dir recursively
@ -206,7 +207,45 @@ class ScanAccountDirBloc
cacheFiles.where((f) => file_util.isSupportedFormat(f)).toList())); cacheFiles.where((f) => file_util.isSupportedFormat(f)).toList()));
} }
await _queryOnline(ev, emit, cacheFiles); stopwatch.reset();
final hasUpdate = await _syncOnline(ev);
_log.info(
"[_onEventQuery] Elapsed time (_syncOnline): ${stopwatch.elapsedMilliseconds}ms, hasUpdate: $hasUpdate");
if (hasUpdate) {
// content updated, reload from db
stopwatch.reset();
final newFiles = await _queryOffline(ev);
_log.info(
"[_onEventQuery] Elapsed time (_queryOffline) 2nd pass: ${stopwatch.elapsedMilliseconds}ms, ${newFiles.length} files");
emit(ScanAccountDirBlocSuccess(newFiles));
} else {
emit(ScanAccountDirBlocSuccess(cacheFiles));
}
}
Future<bool> _syncOnline(ScanAccountDirBlocQueryBase ev) async {
final settings = AccountPref.of(account);
final shareDir =
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
bool isShareDirIncluded = false;
bool hasUpdate = false;
for (final r in account.roots) {
final dirPath = file_util.unstripPath(account, r);
hasUpdate |= await SyncDir(_c)(account, dirPath);
isShareDirIncluded |=
file_util.isOrUnderDir(shareDir, File(path: dirPath));
}
if (!isShareDirIncluded) {
_log.info("[_syncOnline] Explicitly scanning share folder");
hasUpdate |= await SyncDir(_c)(
account,
file_util.unstripPath(account, settings.getShareFolderOr()),
isRecursive: false,
);
}
return hasUpdate;
} }
Future<void> _onExternalEvent(_ScanAccountDirBlocExternalEvent ev, Future<void> _onExternalEvent(_ScanAccountDirBlocExternalEvent ev,
@ -360,13 +399,20 @@ class ScanAccountDirBloc
); );
} }
Future<List<File>> _queryOffline(ScanAccountDirBlocQueryBase ev) async { Future<List<FileDescriptor>> _queryOffline(
final files = <File>[]; ScanAccountDirBlocQueryBase ev) async {
final settings = AccountPref.of(account);
final shareDir =
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
bool isShareDirIncluded = false;
final files = <FileDescriptor>[];
for (final r in account.roots) { for (final r in account.roots) {
try { try {
final dir = File(path: file_util.unstripPath(account, r)); final dir = File(path: file_util.unstripPath(account, r));
files.addAll(await ScanDirOffline(_c)(account, dir, files.addAll(await ScanDirOffline(_c)(account, dir,
isOnlySupportedFormat: false)); isOnlySupportedFormat: true));
isShareDirIncluded |= file_util.isOrUnderDir(shareDir, dir);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[_queryOffline] Failed while ScanDirOffline: ${logFilename(r)}", "[_queryOffline] Failed while ScanDirOffline: ${logFilename(r)}",
@ -374,81 +420,12 @@ class ScanAccountDirBloc
stackTrace); stackTrace);
} }
} }
return files;
}
Future<void> _queryOnline(ScanAccountDirBlocQueryBase ev,
Emitter<ScanAccountDirBlocState> emit, List<File> cache) async {
// 1st pass: scan for new files
var files = <File>[];
final cacheMap = FileForwardCacheManager.prepareFileMap(cache);
final stopwatch = Stopwatch()..start();
_c.touchManager.clearTouchCache();
final fileRepo = FileRepo(FileCachedDataSource(
_c,
forwardCacheManager: FileForwardCacheManager(_c, cacheMap),
shouldCheckCache: true,
));
await for (final event
in _queryWithFileRepo(fileRepo, ev, fileRepoForShareDir: _c.fileRepo)) {
if (event is ExceptionEvent) {
_log.shout("[_queryOnline] Exception while request", event.error,
event.stackTrace);
emit(ScanAccountDirBlocFailure(
cache.isEmpty
? files
: cache.where((f) => file_util.isSupportedFormat(f)).toList(),
event.error));
return;
}
files.addAll(event);
if (cache.isEmpty) {
// only emit partial results if there's no cache
emit(ScanAccountDirBlocLoading(files.toList()));
}
}
_log.info(
"[_queryOnline] Elapsed time (_queryOnline): ${stopwatch.elapsedMilliseconds}ms, ${files.length} files");
emit(ScanAccountDirBlocSuccess(files));
}
/// Emit all files under this account
///
/// Emit List<File> or ExceptionEvent
Stream<dynamic> _queryWithFileRepo(
FileRepo fileRepo,
ScanAccountDirBlocQueryBase ev, {
FileRepo? fileRepoForShareDir,
}) async* {
final settings = AccountPref.of(account);
final shareDir =
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
bool isShareDirIncluded = false;
for (final r in account.roots) {
final dir = File(path: file_util.unstripPath(account, r));
yield* ScanDir(fileRepo)(account, dir);
isShareDirIncluded |= file_util.isOrUnderDir(shareDir, dir);
}
if (!isShareDirIncluded) { if (!isShareDirIncluded) {
_log.info("[_queryWithFileRepo] Explicitly scanning share folder"); _log.info("[_queryOffline] Explicitly scanning share folder");
try { files.addAll(await Ls(_c.fileRepoLocal)(account, shareDir));
final files = await Ls(fileRepoForShareDir ?? fileRepo)(
account,
File(
path: file_util.unstripPath(account, settings.getShareFolderOr()),
),
);
yield files
.where((f) =>
file_util.isSupportedFormat(f) && !f.isOwned(account.userId))
.toList();
} catch (e, stackTrace) {
yield ExceptionEvent(e, stackTrace);
}
} }
return files;
} }
bool _isFileOfInterest(File file) { bool _isFileOfInterest(File file) {

View file

@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -16,15 +18,23 @@ import 'package:nc_photos/mobile/platform.dart'
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/use_case/download_file.dart'; import 'package:nc_photos/use_case/download_file.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos_plugin/nc_photos_plugin.dart'; import 'package:nc_photos_plugin/nc_photos_plugin.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class DownloadHandler { class DownloadHandler {
DownloadHandler(this._c)
: assert(require(_c)),
assert(InflateFileDescriptor.require(_c));
static bool require(DiContainer c) => true;
Future<void> downloadFiles( Future<void> downloadFiles(
Account account, Account account,
List<File> files, { List<FileDescriptor> fds, {
String? parentDir, String? parentDir,
}) { }) async {
final files = await InflateFileDescriptor(_c)(account, fds);
final _DownloadHandlerBase handler; final _DownloadHandlerBase handler;
if (platform_k.isAndroid) { if (platform_k.isAndroid) {
handler = _DownlaodHandlerAndroid(); handler = _DownlaodHandlerAndroid();
@ -37,6 +47,8 @@ class DownloadHandler {
parentDir: parentDir, parentDir: parentDir,
); );
} }
final DiContainer _c;
} }
abstract class _DownloadHandlerBase { abstract class _DownloadHandlerBase {

View file

@ -5,6 +5,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';
@ -20,6 +21,9 @@ abstract class AlbumCoverProvider with EquatableMixin {
case AlbumManualCoverProvider._type: case AlbumManualCoverProvider._type:
return AlbumManualCoverProvider.fromJson( return AlbumManualCoverProvider.fromJson(
content.cast<String, dynamic>()); content.cast<String, dynamic>());
case AlbumMemoryCoverProvider._type:
return AlbumMemoryCoverProvider.fromJson(
content.cast<String, dynamic>());
default: default:
_log.shout("[fromJson] Unknown type: $type"); _log.shout("[fromJson] Unknown type: $type");
throw ArgumentError.value(type, "type"); throw ArgumentError.value(type, "type");
@ -46,7 +50,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
@override @override
toString(); toString();
File? getCover(Album album); FileDescriptor? getCover(Album album);
JsonObj _toContentJson(); JsonObj _toContentJson();
@ -151,3 +155,39 @@ class AlbumManualCoverProvider extends AlbumCoverProvider {
static const _type = "manual"; static const _type = "manual";
} }
/// Cover selected when building a Memory album
class AlbumMemoryCoverProvider extends AlbumCoverProvider {
AlbumMemoryCoverProvider({
required this.coverFile,
});
factory AlbumMemoryCoverProvider.fromJson(JsonObj json) {
return AlbumMemoryCoverProvider(
coverFile:
FileDescriptor.fromJson(json["coverFile"].cast<String, dynamic>()),
);
}
@override
toString() => "$runtimeType {"
"coverFile: '${coverFile.fdPath}', "
"}";
@override
getCover(Album album) => coverFile;
@override
get props => [
coverFile,
];
@override
_toContentJson() => {
"coverFile": coverFile.toJson(),
};
final FileDescriptor coverFile;
static const _type = "memory";
}

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/upgrader.dart'; import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql; import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;

View file

@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';

View file

@ -5,21 +5,14 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/exif.dart'; import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/json_util.dart' as json_util; import 'package:nc_photos/json_util.dart' as json_util;
import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/string_extension.dart'; import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';
import 'package:path/path.dart' as path_lib;
int compareFileDateTimeDescending(File x, File y) { int compareFileDateTimeDescending(File x, File y) =>
final tmp = y.bestDateTime.compareTo(x.bestDateTime); compareFileDescriptorDateTimeDescending(x, y);
if (tmp != 0) {
return tmp;
} else {
// compare file name if files are modified at the same time
return x.path.compareTo(y.path);
}
}
class ImageLocation with EquatableMixin { class ImageLocation with EquatableMixin {
const ImageLocation({ const ImageLocation({
@ -293,7 +286,7 @@ class MetadataUpgraderV2 implements MetadataUpgrader {
static final _log = Logger("entity.file.MetadataUpgraderV2"); static final _log = Logger("entity.file.MetadataUpgraderV2");
} }
class File with EquatableMixin { class File with EquatableMixin implements FileDescriptor {
File({ File({
required String path, required String path,
this.contentLength, this.contentLength,
@ -435,6 +428,7 @@ class File with EquatableMixin {
return product + "}"; return product + "}";
} }
@override
JsonObj toJson() { JsonObj toJson() {
return { return {
"path": path, "path": path,
@ -533,6 +527,24 @@ class File with EquatableMixin {
location, location,
]; ];
@override
get fdPath => path;
@override
get fdId => fileId!;
@override
get fdMime => contentType;
@override
get fdIsArchived => isArchived ?? false;
@override
get fdIsFavorite => isFavorite ?? false;
@override
get fdDateTime => bestDateTime;
final String path; final String path;
final int? contentLength; final int? contentLength;
final String? contentType; final String? contentType;
@ -563,54 +575,6 @@ extension FileExtension on File {
DateTime.now().toUtc(); DateTime.now().toUtc();
bool isOwned(CiString userId) => ownerId == null || ownerId == userId; bool isOwned(CiString userId) => ownerId == null || ownerId == userId;
/// Return the path of this file with the DAV part stripped
///
/// WebDAV file path: remote.php/dav/files/{username}/{strippedPath}. If this
/// file points to the user's root dir, return "."
///
/// See: [strippedPathWithEmpty]
String get strippedPath {
if (path.contains("remote.php/dav/files")) {
final position = path.indexOf("/", "remote.php/dav/files/".length) + 1;
if (position == 0) {
// root dir path
return ".";
} else {
return path.substring(position);
}
} else {
return path;
}
}
/// Return the path of this file with the DAV part stripped
///
/// WebDAV file path: remote.php/dav/files/{username}/{strippedPath}. If this
/// file points to the user's root dir, return an empty string
///
/// See: [strippedPath]
String get strippedPathWithEmpty {
final path = strippedPath;
return path == "." ? "" : path;
}
String get filename => path_lib.basename(path);
/// Compare the server identity of two Files
///
/// Return true if two Files point to the same file on server. Be careful that
/// this does NOT mean that the two Files are identical
bool compareServerIdentity(File other) {
if (fileId != null && other.fileId != null) {
return fileId == other.fileId;
} else {
return path == other.path;
}
}
/// hashCode to be used with [compareServerIdentity]
int get identityHashCode => (fileId ?? path).hashCode;
} }
class FileServerIdentityComparator { class FileServerIdentityComparator {

View file

@ -9,6 +9,7 @@ import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/file_cache_manager.dart'; import 'package:nc_photos/entity/file/file_cache_manager.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite_table.dart' as sql; import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
@ -575,6 +576,17 @@ class FileCachedDataSource implements FileDataSource {
} }
// no cache or outdated // no cache or outdated
return await sync(account, dir,
remoteTouchEtag: cacheLoader.remoteTouchEtag);
}
/// Sync [dir] with remote content, and set the local touch etag as
/// [remoteTouchEtag]
Future<List<File>> sync(
Account account,
File dir, {
required String? remoteTouchEtag,
}) async {
try { try {
final remote = await _remoteSrc.list(account, dir); final remote = await _remoteSrc.list(account, dir);
await FileSqliteCacheUpdater(_c)(account, dir, remote: remote); await FileSqliteCacheUpdater(_c)(account, dir, remote: remote);
@ -582,8 +594,7 @@ class FileCachedDataSource implements FileDataSource {
// update our local touch token to match the remote one // update our local touch token to match the remote one
try { try {
_log.info("[list] Update outdated local etag: ${dir.path}"); _log.info("[list] Update outdated local etag: ${dir.path}");
await _c.touchManager await _c.touchManager.setLocalEtag(account, dir, remoteTouchEtag);
.setLocalEtag(account, dir, cacheLoader.remoteTouchEtag);
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout("[list] Failed while setLocalToken", e, stacktrace); _log.shout("[list] Failed while setLocalToken", e, stacktrace);
// ignore error // ignore error
@ -593,16 +604,22 @@ class FileCachedDataSource implements FileDataSource {
} on ApiException catch (e) { } on ApiException catch (e) {
if (e.response.statusCode == 404) { if (e.response.statusCode == 404) {
_log.info("[list] File removed: $dir"); _log.info("[list] File removed: $dir");
if (cache != null) { try {
await _sqliteDbSrc.remove(account, dir); await _sqliteDbSrc.remove(account, dir);
} catch (e) {
_log.warning(
"[list] Failed while remove from db, file not cached?", e);
} }
return []; return [];
} else if (e.response.statusCode == 403) { } else if (e.response.statusCode == 403) {
_log.info("[list] E2E encrypted dir: $dir"); _log.info("[list] E2E encrypted dir: $dir");
if (cache != null) { try {
// we need to keep the dir itself as it'll be inserted again on next // we need to keep the dir itself as it'll be inserted again on next
// listing of its parent // listing of its parent
await _sqliteDbSrc.emptyDir(account, dir); await _sqliteDbSrc.emptyDir(account, dir);
} catch (e) {
_log.warning(
"[list] Failed while emptying from db, file not cached?", e);
} }
return []; return [];
} else { } else {

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql; import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;

View file

@ -0,0 +1,110 @@
import 'package:equatable/equatable.dart';
import 'package:nc_photos/type.dart';
import 'package:path/path.dart' as path_lib;
int compareFileDescriptorDateTimeDescending(
FileDescriptor x, FileDescriptor y) {
final tmp = y.fdDateTime.compareTo(x.fdDateTime);
if (tmp != 0) {
return tmp;
} else {
// compare file name if files are modified at the same time
return x.fdPath.compareTo(y.fdPath);
}
}
class FileDescriptor with EquatableMixin {
const FileDescriptor({
required this.fdPath,
required this.fdId,
required this.fdMime,
required this.fdIsArchived,
required this.fdIsFavorite,
required this.fdDateTime,
});
static FileDescriptor fromJson(JsonObj json) => FileDescriptor(
fdPath: json["fdPath"],
fdId: json["fdId"],
fdMime: json["fdMime"],
fdIsArchived: json["fdIsArchived"],
fdIsFavorite: json["fdIsFavorite"],
fdDateTime: json["fdDateTime"],
);
JsonObj toJson() => {
"fdPath": fdPath,
"fdId": fdId,
"fdMime": fdMime,
"fdIsArchived": fdIsArchived,
"fdIsFavorite": fdIsFavorite,
"fdDateTime": fdDateTime,
};
@override
get props => [
fdPath,
fdId,
fdMime,
fdIsArchived,
fdIsFavorite,
fdDateTime,
];
final String fdPath;
final int fdId;
final String? fdMime;
final bool fdIsArchived;
final bool fdIsFavorite;
final DateTime fdDateTime;
}
extension FileDescriptorExtension on FileDescriptor {
/// Return the path of this file with the DAV part stripped
///
/// WebDAV file path: remote.php/dav/files/{username}/{strippedPath}. If this
/// file points to the user's root dir, return "."
///
/// See: [strippedPathWithEmpty]
String get strippedPath {
if (fdPath.contains("remote.php/dav/files")) {
final position = fdPath.indexOf("/", "remote.php/dav/files/".length) + 1;
if (position == 0) {
// root dir path
return ".";
} else {
return fdPath.substring(position);
}
} else {
return fdPath;
}
}
/// Return the path of this file with the DAV part stripped
///
/// WebDAV file path: remote.php/dav/files/{username}/{strippedPath}. If this
/// file points to the user's root dir, return an empty string
///
/// See: [strippedPath]
String get strippedPathWithEmpty {
final path = strippedPath;
return path == "." ? "" : path;
}
String get filename => path_lib.basename(fdPath);
/// Compare the server identity of two Files
///
/// Return true if two Files point to the same file on server. Be careful that
/// this does NOT mean that the two Files are identical
bool compareServerIdentity(FileDescriptor other) {
try {
return fdId == other.fdId;
} catch (_) {
return fdPath == other.fdPath;
}
}
/// hashCode to be used with [compareServerIdentity]
int get identityHashCode => fdId.hashCode;
}

View file

@ -2,32 +2,34 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/string_extension.dart'; import 'package:nc_photos/string_extension.dart';
import 'package:path/path.dart' as path_lib; import 'package:path/path.dart' as path_lib;
bool isSupportedMime(String mime) => _supportedFormatMimes.contains(mime); bool isSupportedMime(String mime) => supportedFormatMimes.contains(mime);
bool isSupportedFormat(File file) => isSupportedMime(file.contentType ?? ""); bool isSupportedFormat(FileDescriptor file) =>
isSupportedMime(file.fdMime ?? "");
bool isSupportedImageMime(String mime) => bool isSupportedImageMime(String mime) =>
isSupportedMime(mime) && mime.startsWith("image/") == true; supportedImageFormatMimes.contains(mime);
bool isSupportedImageFormat(File file) => bool isSupportedImageFormat(FileDescriptor file) =>
isSupportedImageMime(file.contentType ?? ""); isSupportedImageMime(file.fdMime ?? "");
bool isSupportedVideoFormat(File file) => bool isSupportedVideoFormat(FileDescriptor file) =>
isSupportedFormat(file) && file.contentType?.startsWith("video/") == true; isSupportedFormat(file) && file.fdMime?.startsWith("video/") == true;
bool isMetadataSupportedMime(String mime) => bool isMetadataSupportedMime(String mime) =>
_metadataSupportedFormatMimes.contains(mime); _metadataSupportedFormatMimes.contains(mime);
bool isMetadataSupportedFormat(File file) => bool isMetadataSupportedFormat(FileDescriptor file) =>
isMetadataSupportedMime(file.contentType ?? ""); isMetadataSupportedMime(file.fdMime ?? "");
bool isTrash(Account account, File file) => bool isTrash(Account account, FileDescriptor file) =>
file.path.startsWith(api_util.getTrashbinPath(account)); file.fdPath.startsWith(api_util.getTrashbinPath(account));
bool isAlbumFile(Account account, File file) => bool isAlbumFile(Account account, File file) =>
file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account)); file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
@ -94,7 +96,7 @@ bool isMissingMetadata(File file) =>
isSupportedImageFormat(file) && isSupportedImageFormat(file) &&
(file.metadata == null || file.location == null); (file.metadata == null || file.location == null);
final _supportedFormatMimes = [ final supportedFormatMimes = [
"image/jpeg", "image/jpeg",
"image/png", "image/png",
"image/webp", "image/webp",
@ -105,6 +107,9 @@ final _supportedFormatMimes = [
if (platform_k.isAndroid || platform_k.isWeb) "video/webm", if (platform_k.isAndroid || platform_k.isWeb) "video/webm",
]; ];
final supportedImageFormatMimes =
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
const _metadataSupportedFormatMimes = [ const _metadataSupportedFormatMimes = [
"image/jpeg", "image/jpeg",
"image/heic", "image/heic",

View file

@ -4,6 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/search.dart'; import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/entity/search_util.dart' as search_util; import 'package:nc_photos/entity/search_util.dart' as search_util;
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';

View file

@ -5,6 +5,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/exif.dart'; import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql; import 'package:nc_photos/entity/sqlite_table.dart' as sql;
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;

View file

@ -3,6 +3,8 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart' as app; import 'package:nc_photos/account.dart' as app;
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/file.dart' as app; import 'package:nc_photos/entity/file.dart' as app;
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/sqlite_table.dart'; import 'package:nc_photos/entity/sqlite_table.dart';
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';
import 'package:nc_photos/entity/sqlite_table_isolate.dart'; import 'package:nc_photos/entity/sqlite_table_isolate.dart';
@ -508,6 +510,48 @@ extension SqliteDbExtension on SqliteDb {
} }
} }
Future<int> countMissingMetadataByFileIds({
Account? sqlAccount,
app.Account? appAccount,
required List<int> fileIds,
}) async {
assert((sqlAccount != null) != (appAccount != null));
final counts = await fileIds.withPartition((sublist) async {
final count = countAll(
filter:
images.lastUpdated.isNull() | imageLocations.version.isNull());
final query = selectOnly(files).join([
innerJoin(accountFiles, accountFiles.file.equalsExp(files.rowId),
useColumns: false),
if (appAccount != null) ...[
innerJoin(accounts, accounts.rowId.equalsExp(accountFiles.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
],
leftOuterJoin(images, images.accountFile.equalsExp(accountFiles.rowId),
useColumns: false),
leftOuterJoin(imageLocations,
imageLocations.accountFile.equalsExp(accountFiles.rowId),
useColumns: false),
]);
query.addColumns([count]);
if (sqlAccount != null) {
query.where(accountFiles.account.equals(sqlAccount.rowId));
} else if (appAccount != null) {
query
..where(servers.address.equals(appAccount.url))
..where(accounts.userId
.equals(appAccount.userId.toCaseInsensitiveString()));
}
query
..where(files.fileId.isIn(sublist))
..where(whereFileIsSupportedImageMime());
return [await query.map((r) => r.read(count)).getSingle()];
}, maxByFileIdsSize);
return counts.reduce((value, element) => value + element);
}
Future<void> truncate() async { Future<void> truncate() async {
await delete(servers).go(); await delete(servers).go();
// technically deleting Servers table is enough to clear the followings, but // technically deleting Servers table is enough to clear the followings, but
@ -528,6 +572,18 @@ extension SqliteDbExtension on SqliteDb {
await customStatement("UPDATE sqlite_sequence SET seq=0;"); await customStatement("UPDATE sqlite_sequence SET seq=0;");
} }
Expression<bool?> whereFileIsSupportedMime() {
return file_util.supportedFormatMimes
.map<Expression<bool?>>((m) => files.contentType.equals(m))
.reduce((value, element) => value | element);
}
Expression<bool?> whereFileIsSupportedImageMime() {
return file_util.supportedImageFormatMimes
.map<Expression<bool?>>((m) => files.contentType.equals(m))
.reduce((value, element) => value | element);
}
static final _log = Logger("entity.sqlite_table_extension.SqliteDbExtension"); static final _log = Logger("entity.sqlite_table_extension.SqliteDbExtension");
} }

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
class CustomizableMaterialPageRoute extends MaterialPageRoute { class CustomizableMaterialPageRoute extends MaterialPageRoute {
CustomizableMaterialPageRoute({ CustomizableMaterialPageRoute({
@ -18,4 +18,4 @@ class CustomizableMaterialPageRoute extends MaterialPageRoute {
final Duration reverseTransitionDuration; final Duration reverseTransitionDuration;
} }
String getImageHeroTag(File file) => "imageHero(${file.path})"; String getImageHeroTag(FileDescriptor file) => "imageHero(${file.fdPath})";

View file

@ -14,4 +14,6 @@ String getRemoteLinkSharesDir(Account account) =>
"${getRemoteStorageDir(account)}/link_shares"; "${getRemoteStorageDir(account)}/link_shares";
String getRemoteStorageDir(Account account) => String getRemoteStorageDir(Account account) =>
"${api_util.getWebdavRootUrlRelative(account)}/.com.nkming.nc_photos"; "${api_util.getWebdavRootUrlRelative(account)}/$remoteStorageDirRelativePath";
const remoteStorageDirRelativePath = ".com.nkming.nc_photos";

View file

@ -13,6 +13,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/event/native_event.dart';

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
@ -26,6 +27,7 @@ import 'package:nc_photos/use_case/create_dir.dart';
import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/create_share.dart';
import 'package:nc_photos/use_case/download_file.dart'; import 'package:nc_photos/use_case/download_file.dart';
import 'package:nc_photos/use_case/download_preview.dart'; import 'package:nc_photos/use_case/download_preview.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/share_local.dart'; import 'package:nc_photos/use_case/share_local.dart';
import 'package:nc_photos/widget/processing_dialog.dart'; import 'package:nc_photos/widget/processing_dialog.dart';
import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart'; import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart';
@ -36,10 +38,14 @@ import 'package:tuple/tuple.dart';
/// Handle sharing to other apps /// Handle sharing to other apps
class ShareHandler { class ShareHandler {
ShareHandler({ ShareHandler(
this._c, {
required this.context, required this.context,
this.clearSelection, this.clearSelection,
}); }) : assert(require(_c)),
assert(InflateFileDescriptor.require(_c));
static bool require(DiContainer c) => true;
Future<void> shareLocalFiles(List<LocalFile> files) async { Future<void> shareLocalFiles(List<LocalFile> files) async {
if (!isSelectionCleared) { if (!isSelectionCleared) {
@ -67,7 +73,8 @@ class ShareHandler {
); );
} }
Future<void> shareFiles(Account account, List<File> files) async { Future<void> shareFiles(Account account, List<FileDescriptor> fds) async {
final files = await InflateFileDescriptor(_c)(account, fds);
try { try {
final method = await _askShareMethod(files); final method = await _askShareMethod(files);
if (method == null) { if (method == null) {
@ -336,6 +343,7 @@ class ShareHandler {
} }
} }
final DiContainer _c;
final BuildContext context; final BuildContext context;
final VoidCallback? clearSelection; final VoidCallback? clearSelection;
var isSelectionCleared = false; var isSelectionCleared = false;

View file

@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/mobile/platform.dart' import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/override_comparator.dart'; import 'package:nc_photos/override_comparator.dart';
import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/create_share.dart';

View file

@ -2,6 +2,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/create_dir.dart'; import 'package:nc_photos/use_case/create_dir.dart';

View file

@ -1,6 +1,7 @@
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/mobile/platform.dart' import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
import 'package:nc_photos/platform/download.dart'; import 'package:nc_photos/platform/download.dart';

View file

@ -1,7 +1,7 @@
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/ls_single_file.dart'; import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:nc_photos/use_case/move.dart'; import 'package:nc_photos/use_case/move.dart';

View file

@ -2,7 +2,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/list_potential_shared_album.dart'; import 'package:nc_photos/use_case/list_potential_shared_album.dart';

View file

@ -0,0 +1,32 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/use_case/find_file.dart';
class InflateFileDescriptor {
InflateFileDescriptor(this._c)
: assert(require(_c)),
assert(FindFile.require(_c));
static bool require(DiContainer c) => true;
/// Turn a list of FileDescriptors to the corresponding Files
///
/// The conversion is done by looking up the files in the database. No lookup
/// will be done for File objects in [fds]
Future<List<File>> call(Account account, List<FileDescriptor> fds) async {
final found = await FindFile(_c)(
account, fds.where((e) => e is! File).map((e) => e.fdId).toList());
final foundMap = Map.fromEntries(found.map((e) => MapEntry(e.fileId!, e)));
return fds.map((e) {
if (e is File) {
return e;
} else {
return foundMap[e.fdId]!;
}
}).toList();
}
final DiContainer _c;
}

View file

@ -2,6 +2,7 @@ import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/location_util.dart' as location_util; import 'package:nc_photos/location_util.dart' as location_util;

View file

@ -1,6 +1,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/use_case/ls.dart'; import 'package:nc_photos/use_case/ls.dart';

View file

@ -9,6 +9,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos/stream_extension.dart';

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/use_case/preprocess_album.dart'; import 'package:nc_photos/use_case/preprocess_album.dart';
import 'package:nc_photos/use_case/unshare_file_from_album.dart'; import 'package:nc_photos/use_case/unshare_file_from_album.dart';

View file

@ -3,15 +3,15 @@ import 'dart:convert';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception.dart'; import 'package:nc_photos/exception.dart';
import 'package:nc_photos/type.dart'; import 'package:nc_photos/type.dart';
class RequestPublicLink { class RequestPublicLink {
/// Request a temporary unique public link to [file] /// Request a temporary unique public link to [file]
Future<String> call(Account account, File file) async { Future<String> call(Account account, FileDescriptor file) async {
final response = final response =
await Api(account).ocs().dav().direct().post(fileId: file.fileId!); await Api(account).ocs().dav().direct().post(fileId: file.fdId);
if (!response.isGood) { if (!response.isGood) {
_log.severe("[call] Failed requesting server: $response"); _log.severe("[call] Failed requesting server: $response");
throw ApiException( throw ApiException(

View file

@ -3,6 +3,7 @@ import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/use_case/move.dart'; import 'package:nc_photos/use_case/move.dart';

View file

@ -2,16 +2,18 @@ import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table_converter.dart'; import 'package:nc_photos/entity/sqlite_table_converter.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
class ScanDirOffline { class ScanDirOffline {
ScanDirOffline(this._c) : assert(require(_c)); ScanDirOffline(this._c) : assert(require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb); static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
Future<List<File>> call( Future<List<FileDescriptor>> call(
Account account, Account account,
File root, { File root, {
bool isOnlySupportedFormat = true, bool isOnlySupportedFormat = true,
@ -23,36 +25,57 @@ class ScanDirOffline {
}, (db, Map args) async { }, (db, Map args) async {
final Account account = args["account"]; final Account account = args["account"];
final File root = args["root"]; final File root = args["root"];
final strippedPath = root.strippedPathWithEmpty;
final bool isOnlySupportedFormat = args["isOnlySupportedFormat"]; final bool isOnlySupportedFormat = args["isOnlySupportedFormat"];
final dbFiles = await db.useInIsolate((db) async { final dbFiles = await db.useInIsolate((db) async {
final query = db.queryFiles().run((q) { final query = db.queryFiles().run((q) {
q q
..setQueryMode(sql.FilesQueryMode.completeFile) ..setQueryMode(
sql.FilesQueryMode.expression,
expressions: [
db.accountFiles.relativePath,
db.files.fileId,
db.files.contentType,
db.accountFiles.isArchived,
db.accountFiles.isFavorite,
db.accountFiles.bestDateTime,
],
)
..setAppAccount(account); ..setAppAccount(account);
root.strippedPathWithEmpty.run((p) { if (strippedPath.isNotEmpty) {
if (p.isNotEmpty) { q.byOrRelativePathPattern("$strippedPath/%");
q.byOrRelativePathPattern("$p/%");
}
});
if (isOnlySupportedFormat) {
q
..byMimePattern("image/%")
..byMimePattern("video/%");
} }
return q.build(); return q.build();
}); });
if (isOnlySupportedFormat) {
query.where(db.whereFileIsSupportedMime());
}
if (strippedPath.isEmpty) {
query.where(db.accountFiles.relativePath
.like("${remote_storage_util.remoteStorageDirRelativePath}/%")
.not());
}
return await query return await query
.map((r) => sql.CompleteFile( .map((r) => <String, dynamic>{
r.readTable(db.files), "relativePath": r.read(db.accountFiles.relativePath)!,
r.readTable(db.accountFiles), "fileId": r.read(db.files.fileId)!,
r.readTableOrNull(db.images), "contentType": r.read(db.files.contentType)!,
r.readTableOrNull(db.imageLocations), "isArchived": r.read(db.accountFiles.isArchived),
r.readTableOrNull(db.trashes), "isFavorite": r.read(db.accountFiles.isFavorite),
)) "bestDateTime": r.read(db.accountFiles.bestDateTime)!.toUtc(),
})
.get(); .get();
}); });
return dbFiles return dbFiles
.map((f) => SqliteFileConverter.fromSql(account.userId.toString(), f)) .map((f) => FileDescriptor(
fdPath:
"remote.php/dav/files/${account.userId.toString()}/${f["relativePath"]}",
fdId: f["fileId"],
fdMime: f["contentType"],
fdIsArchived: f["isArchived"] ?? false,
fdIsFavorite: f["isFavorite"] ?? false,
fdDateTime: f["bestDateTime"],
))
.toList(); .toList();
}); });
} }
@ -83,13 +106,11 @@ class ScanDirOfflineMini {
} }
q.byOrRelativePathPattern("$path/%"); q.byOrRelativePathPattern("$path/%");
} }
if (isOnlySupportedFormat) {
q
..byMimePattern("image/%")
..byMimePattern("video/%");
}
return q.build(); return q.build();
}); });
if (isOnlySupportedFormat) {
query.where(db.whereFileIsSupportedMime());
}
query query
..orderBy([sql.OrderingTerm.desc(db.accountFiles.bestDateTime)]) ..orderBy([sql.OrderingTerm.desc(db.accountFiles.bestDateTime)])
..limit(limit); ..limit(limit);

View file

@ -0,0 +1,109 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.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/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:tuple/tuple.dart';
class SyncDir {
SyncDir(this._c) : assert(require(_c));
static bool require(DiContainer c) =>
DiContainer.has(c, DiType.fileRepoRemote) &&
DiContainer.has(c, DiType.sqliteDb) &&
DiContainer.has(c, DiType.touchManager);
/// Sync local SQLite DB with remote content
///
/// Return true if some of the files have changed
Future<bool> call(
Account account,
String dirPath, {
bool isRecursive = true,
}) async {
final dirCache = await _queryAllSubDirEtags(account, dirPath);
final remoteRoot =
await LsSingleFile(_c.withRemoteFileRepo())(account, dirPath);
return await _syncDir(account, remoteRoot, dirCache,
isRecursive: isRecursive);
}
Future<bool> _syncDir(
Account account,
File remoteDir,
Map<int, String> dirCache, {
required bool isRecursive,
}) async {
final status = await _checkContentUpdated(account, remoteDir, dirCache);
if (!status.item1) {
_log.finer("[_syncDir] Dir unchanged: ${remoteDir.path}");
return false;
}
_log.info("[_syncDir] Dir changed: ${remoteDir.path}");
final children = await FileCachedDataSource(_c, shouldCheckCache: true)
.sync(account, remoteDir, remoteTouchEtag: status.item2);
if (!isRecursive) {
return true;
}
for (final d in children.where((c) =>
c.isCollection == true &&
!remoteDir.compareServerIdentity(c) &&
!c.path.endsWith(remote_storage_util.getRemoteStorageDir(account)))) {
try {
await _syncDir(account, d, dirCache, isRecursive: isRecursive);
} catch (e, stackTrace) {
_log.severe("[_syncDir] Failed while _syncDir: ${logFilename(d.path)}",
e, stackTrace);
}
}
return true;
}
Future<Tuple2<bool, String?>> _checkContentUpdated(
Account account, File remoteDir, Map<int, String> dirCache) async {
String? touchResult;
try {
touchResult = await _c.touchManager.checkTouchEtag(account, remoteDir);
if (touchResult == null &&
dirCache[remoteDir.fileId!] == remoteDir.etag!) {
return const Tuple2(false, null);
}
} catch (e, stackTrace) {
_log.severe("[_isContentUpdated] Uncaught exception", e, stackTrace);
}
return Tuple2(true, touchResult);
}
Future<Map<int, String>> _queryAllSubDirEtags(
Account account, String dirPath) async {
final dir = File(path: dirPath);
return await _c.sqliteDb.use((db) async {
final query = db.queryFiles().run((q) {
q
..setQueryMode(sql.FilesQueryMode.expression,
expressions: [db.files.fileId, db.files.etag])
..setAppAccount(account);
if (dir.strippedPathWithEmpty.isNotEmpty) {
q.byOrRelativePathPattern("${dir.strippedPathWithEmpty}/%");
}
return q.build();
});
query.where(db.files.isCollection.equals(true));
return Map.fromEntries(await query
.map(
(r) => MapEntry(r.read(db.files.fileId)!, r.read(db.files.etag)!))
.get());
});
}
final DiContainer _c;
static final _log = Logger("use_case.sync_dir.SyncDir");
}

View file

@ -1,7 +1,7 @@
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/move.dart'; import 'package:nc_photos/use_case/move.dart';

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos/stream_extension.dart';
import 'package:nc_photos/use_case/list_album.dart'; import 'package:nc_photos/use_case/list_album.dart';

View file

@ -2,7 +2,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart'; import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
class UpdateAutoAlbumCover { class UpdateAutoAlbumCover {

View file

@ -389,7 +389,8 @@ class _AlbumBrowserState extends State<AlbumBrowser>
} }
void _onDownloadPressed() { void _onDownloadPressed() {
DownloadHandler().downloadFiles( final c = KiwiContainer().resolve<DiContainer>();
DownloadHandler(c).downloadFiles(
widget.account, widget.account,
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(), _sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
parentDir: _album!.name, parentDir: _album!.name,
@ -419,6 +420,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
@ -431,6 +433,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
return; return;
} }
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -441,10 +444,11 @@ class _AlbumBrowserState extends State<AlbumBrowser>
} }
Future<void> _onSelectionAddPressed(BuildContext context) async { Future<void> _onSelectionAddPressed(BuildContext context) async {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -493,11 +497,12 @@ class _AlbumBrowserState extends State<AlbumBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });

View file

@ -4,6 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';

View file

@ -14,6 +14,7 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; 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/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';

View file

@ -14,6 +14,7 @@ import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/entity/share/data_source.dart';

View file

@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/language_util.dart' as language_util;
@ -18,6 +19,7 @@ import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/update_property.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/builder/photo_list_item_builder.dart';
import 'package:nc_photos/widget/empty_list_indicator.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart';
@ -230,7 +232,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
.unarchiveSelectedProcessingNotification(selectedListItems.length)), .unarchiveSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
final selectedFiles = selectedListItems final selection = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
@ -238,6 +240,8 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
clearSelectedItems(); clearSelectedItems();
}); });
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles =
await InflateFileDescriptor(c)(widget.account, selection);
final failures = <File>[]; final failures = <File>[];
for (final f in selectedFiles) { for (final f in selectedFiles) {
try { try {
@ -265,7 +269,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
} }
} }
void _transformItems(List<File> files) { void _transformItems(List<FileDescriptor> files) {
_buildItemQueue.addJob( _buildItemQueue.addJob(
PhotoListItemBuilderArguments( PhotoListItemBuilderArguments(
widget.account, widget.account,
@ -293,7 +297,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
late final _bloc = ScanAccountDirBloc.of(widget.account); late final _bloc = ScanAccountDirBloc.of(widget.account);
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
final _buildItemQueue = final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>(); ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();

View file

@ -8,7 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/album_grid_item.dart'; import 'package:nc_photos/widget/album_grid_item.dart';

View file

@ -1,4 +1,4 @@
import 'package:collection/collection.dart' show compareNatural; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
@ -6,9 +6,8 @@ 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_init.dart' as app_init;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; 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/k.dart' as k;
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/photo_list_item.dart';
@ -32,7 +31,7 @@ class PhotoListItemBuilderArguments {
}); });
final Account account; final Account account;
final List<File> files; final List<FileDescriptor> files;
final bool isArchived; final bool isArchived;
final PhotoListItemSorter? sorter; final PhotoListItemSorter? sorter;
final PhotoListItemGrouper? grouper; final PhotoListItemGrouper? grouper;
@ -50,17 +49,17 @@ class PhotoListItemBuilderResult {
this.smartAlbums = const [], this.smartAlbums = const [],
}); });
final List<File> backingFiles; final List<FileDescriptor> backingFiles;
final List<SelectableItem> listItems; final List<SelectableItem> listItems;
final List<Album> smartAlbums; final List<Album> smartAlbums;
} }
typedef PhotoListItemSorter = int Function(File, File); typedef PhotoListItemSorter = int Function(FileDescriptor, FileDescriptor);
abstract class PhotoListItemGrouper { abstract class PhotoListItemGrouper {
const PhotoListItemGrouper(); const PhotoListItemGrouper();
SelectableItem? onFile(File file); SelectableItem? onFile(FileDescriptor file);
} }
class PhotoListFileDateGrouper implements PhotoListItemGrouper { class PhotoListFileDateGrouper implements PhotoListItemGrouper {
@ -69,7 +68,7 @@ class PhotoListFileDateGrouper implements PhotoListItemGrouper {
}) : helper = DateGroupHelper(isMonthOnly: isMonthOnly); }) : helper = DateGroupHelper(isMonthOnly: isMonthOnly);
@override @override
onFile(File file) => helper onFile(FileDescriptor file) => helper
.onFile(file) .onFile(file)
?.run((date) => PhotoListDateItem(date: date, isMonthOnly: isMonthOnly)); ?.run((date) => PhotoListDateItem(date: date, isMonthOnly: isMonthOnly));
@ -83,10 +82,10 @@ class PhotoListItemSmartAlbumConfig {
final int memoriesDayRange; final int memoriesDayRange;
} }
int photoListFileDateTimeSorter(File a, File b) => int photoListFileDateTimeSorter(FileDescriptor a, FileDescriptor b) =>
compareFileDateTimeDescending(a, b); compareFileDescriptorDateTimeDescending(a, b);
int photoListFilenameSorter(File a, File b) => int photoListFilenameSorter(FileDescriptor a, FileDescriptor b) =>
compareNatural(b.filename, a.filename); compareNatural(b.filename, a.filename);
PhotoListItemBuilderResult buildPhotoListItem( PhotoListItemBuilderResult buildPhotoListItem(
@ -112,7 +111,7 @@ class _PhotoListItemBuilder {
required this.locale, required this.locale,
}); });
PhotoListItemBuilderResult call(Account account, List<File> files) { PhotoListItemBuilderResult call(Account account, List<FileDescriptor> files) {
final s = Stopwatch()..start(); final s = Stopwatch()..start();
try { try {
return _fromSortedItems(account, _sortItems(files)); return _fromSortedItems(account, _sortItems(files));
@ -121,17 +120,17 @@ class _PhotoListItemBuilder {
} }
} }
List<File> _sortItems(List<File> files) { List<FileDescriptor> _sortItems(List<FileDescriptor> files) {
final filtered = files.where((f) => (f.isArchived ?? false) == isArchived); final filtered = files.where((f) => f.fdIsArchived == isArchived);
if (sorter == null) { if (sorter == null) {
return filtered.toList(); return filtered.toList();
} else { } else {
return filtered.stableSorted(sorter); return filtered.sorted(sorter!);
} }
} }
PhotoListItemBuilderResult _fromSortedItems( PhotoListItemBuilderResult _fromSortedItems(
Account account, List<File> files) { Account account, List<FileDescriptor> files) {
final today = DateTime.now(); final today = DateTime.now();
final memoryAlbumHelper = smartAlbumConfig != null final memoryAlbumHelper = smartAlbumConfig != null
? MemoryAlbumHelper( ? MemoryAlbumHelper(
@ -156,7 +155,7 @@ class _PhotoListItemBuilder {
); );
} }
SelectableItem? _buildListItem(int i, Account account, File file) { SelectableItem? _buildListItem(int i, Account account, FileDescriptor file) {
final previewUrl = api_util.getFilePreviewUrl(account, file, final previewUrl = api_util.getFilePreviewUrl(account, file,
width: k.photoThumbSize, height: k.photoThumbSize); width: k.photoThumbSize, height: k.photoThumbSize);
if (file_util.isSupportedImageFormat(file)) { if (file_util.isSupportedImageFormat(file)) {
@ -176,8 +175,7 @@ class _PhotoListItemBuilder {
shouldShowFavoriteBadge: shouldShowFavoriteBadge, shouldShowFavoriteBadge: shouldShowFavoriteBadge,
); );
} else { } else {
_log.shout( _log.shout("[_buildListItem] Unsupported file format: ${file.fdMime}");
"[_buildListItem] Unsupported file format: ${file.contentType}");
return null; return null;
} }
} }

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/ls_dir.dart'; import 'package:nc_photos/bloc/ls_dir.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; 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/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;

View file

@ -15,6 +15,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -388,7 +389,8 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
} }
void _onDownloadPressed() { void _onDownloadPressed() {
DownloadHandler().downloadFiles( final c = KiwiContainer().resolve<DiContainer>();
DownloadHandler(c).downloadFiles(
widget.account, widget.account,
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(), _sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
parentDir: _album!.name, parentDir: _album!.name,
@ -411,11 +413,13 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -477,11 +481,12 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });

View file

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/app_init.dart' as app_init; import 'package:nc_photos/app_init.dart' as app_init;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/scan_local_dir.dart'; import 'package:nc_photos/bloc/scan_local_dir.dart';
import 'package:nc_photos/compute_queue.dart'; import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -244,11 +246,13 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
} }
Future<void> _onSelectionSharePressed(BuildContext context) async { Future<void> _onSelectionSharePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListLocalFileItem>() .whereType<PhotoListLocalFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
await ShareHandler( await ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {

View file

@ -7,16 +7,23 @@ import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/notified_action.dart';
import 'package:nc_photos/use_case/add_to_album.dart'; import 'package:nc_photos/use_case/add_to_album.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/widget/album_picker.dart'; import 'package:nc_photos/widget/album_picker.dart';
class AddSelectionToAlbumHandler { class AddSelectionToAlbumHandler {
AddSelectionToAlbumHandler(this._c)
: assert(require(_c)),
assert(InflateFileDescriptor.require(_c));
static bool require(DiContainer c) => true;
Future<void> call({ Future<void> call({
required BuildContext context, required BuildContext context,
required Account account, required Account account,
required List<File> selectedFiles, required List<FileDescriptor> selection,
required VoidCallback clearSelection, required VoidCallback clearSelection,
}) async { }) async {
try { try {
@ -32,6 +39,8 @@ class AddSelectionToAlbumHandler {
await NotifiedAction( await NotifiedAction(
() async { () async {
assert(value.provider is AlbumStaticProvider); assert(value.provider is AlbumStaticProvider);
final selectedFiles =
await InflateFileDescriptor(_c)(account, selection);
final selected = selectedFiles final selected = selectedFiles
.map((f) => AlbumFileItem( .map((f) => AlbumFileItem(
addedBy: account.userId, addedBy: account.userId,
@ -52,6 +61,8 @@ class AddSelectionToAlbumHandler {
} }
} }
final DiContainer _c;
static final _log = Logger( static final _log = Logger(
"widget.handler.add_selection_to_album_handler.AddSelectionToAlbumHandler"); "widget.handler.add_selection_to_album_handler.AddSelectionToAlbumHandler");
} }

View file

@ -4,19 +4,24 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/notified_action.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
class ArchiveSelectionHandler { class ArchiveSelectionHandler {
ArchiveSelectionHandler(this._c) : assert(require(_c)); ArchiveSelectionHandler(this._c)
: assert(require(_c)),
assert(InflateFileDescriptor.require(_c));
static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo); static bool require(DiContainer c) => DiContainer.has(c, DiType.fileRepo);
/// Archive [selectedFiles] and return the archived count /// Archive [selectedFiles] and return the archived count
Future<int> call({ Future<int> call({
required Account account, required Account account,
required List<File> selectedFiles, required List<FileDescriptor> selection,
}) { }) async {
final selectedFiles = await InflateFileDescriptor(_c)(account, selection);
return NotifiedListAction<File>( return NotifiedListAction<File>(
list: selectedFiles, list: selectedFiles,
action: (file) async { action: (file) async {

View file

@ -5,22 +5,30 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/navigation_manager.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/widget/trashbin_browser.dart'; import 'package:nc_photos/widget/trashbin_browser.dart';
class RemoveSelectionHandler { class RemoveSelectionHandler {
RemoveSelectionHandler(this._c)
: assert(require(_c)),
assert(InflateFileDescriptor.require(_c));
static bool require(DiContainer c) => true;
/// Remove [selectedFiles] and return the removed count /// Remove [selectedFiles] and return the removed count
Future<int> call({ Future<int> call({
required Account account, required Account account,
required List<File> selectedFiles, required List<FileDescriptor> selection,
bool shouldCleanupAlbum = true, bool shouldCleanupAlbum = true,
bool isRemoveOpened = false, bool isRemoveOpened = false,
bool isMoveToTrash = false, bool isMoveToTrash = false,
}) async { }) async {
final selectedFiles = await InflateFileDescriptor(_c)(account, selection);
final String processingText, successText; final String processingText, successText;
final String Function(int) failureText; final String Function(int) failureText;
if (isRemoveOpened) { if (isRemoveOpened) {
@ -81,6 +89,8 @@ class RemoveSelectionHandler {
return selectedFiles.length - failureCount; return selectedFiles.length - failureCount;
} }
final DiContainer _c;
static final _log = static final _log =
Logger("widget.handler.remove_selection_handler.RemoveSelectionHandler"); Logger("widget.handler.remove_selection_handler.RemoveSelectionHandler");
} }

View file

@ -19,8 +19,8 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/event/native_event.dart'; import 'package:nc_photos/event/native_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -359,7 +359,7 @@ class _HomePhotosState extends State<HomePhotos>
?.item ?.item
.as<PhotoListFileItem>() .as<PhotoListFileItem>()
?.file ?.file
.bestDateTime; .fdDateTime;
if (date != null) { if (date != null) {
final text = DateFormat(DateFormat.YEAR_ABBR_MONTH, final text = DateFormat(DateFormat.YEAR_ABBR_MONTH,
Localizations.localeOf(context).languageCode) Localizations.localeOf(context).languageCode)
@ -424,11 +424,13 @@ class _HomePhotosState extends State<HomePhotos>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -439,10 +441,11 @@ class _HomePhotosState extends State<HomePhotos>
} }
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) { Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -457,17 +460,19 @@ class _HomePhotosState extends State<HomePhotos>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
} }
Future<void> _onSelectionArchivePressed(BuildContext context) async { Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -475,13 +480,14 @@ class _HomePhotosState extends State<HomePhotos>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
); );
} }
Future<void> _onSelectionDeletePressed(BuildContext context) async { Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -489,9 +495,9 @@ class _HomePhotosState extends State<HomePhotos>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await RemoveSelectionHandler()( await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
isMoveToTrash: true, isMoveToTrash: true,
); );
} }
@ -523,23 +529,34 @@ class _HomePhotosState extends State<HomePhotos>
_hasFiredMetadataTask.value = false; _hasFiredMetadataTask.value = false;
} }
void _tryStartMetadataTask({ Future<void> _tryStartMetadataTask({
bool ignoreFired = false, bool ignoreFired = false,
}) { }) async {
if (_bloc.state is ScanAccountDirBlocSuccess && if (_bloc.state is ScanAccountDirBlocSuccess &&
Pref().isEnableExifOr() && Pref().isEnableExifOr() &&
(!_hasFiredMetadataTask.value || ignoreFired)) { (!_hasFiredMetadataTask.value || ignoreFired)) {
try {
final c = KiwiContainer().resolve<DiContainer>();
final missingMetadataCount = final missingMetadataCount =
_backingFiles.where(file_util.isMissingMetadata).length; await c.sqliteDb.countMissingMetadataByFileIds(
appAccount: widget.account,
fileIds: _backingFiles.map((e) => e.fdId).toList(),
);
_log.info(
"[_tryStartMetadataTask] Missing count: $missingMetadataCount");
if (missingMetadataCount > 0) { if (missingMetadataCount > 0) {
if (_web != null) { if (_web != null) {
_web!.startMetadataTask(missingMetadataCount); _web!.startMetadataTask(missingMetadataCount);
} else { } else {
service.startService(); unawaited(service.startService());
} }
} }
_hasFiredMetadataTask.value = true; _hasFiredMetadataTask.value = true;
} catch (e, stackTrace) {
_log.shout("[_tryStartMetadataTask] Failed starting metadata task", e,
stackTrace);
}
} }
} }
@ -558,7 +575,7 @@ class _HomePhotosState extends State<HomePhotos>
/// Transform a File list to grid items /// Transform a File list to grid items
void _transformItems( void _transformItems(
List<File> files, { List<FileDescriptor> files, {
bool isSorted = false, bool isSorted = false,
bool isPostSuccess = false, bool isPostSuccess = false,
}) { }) {
@ -700,7 +717,7 @@ class _HomePhotosState extends State<HomePhotos>
late final _bloc = ScanAccountDirBloc.of(widget.account); late final _bloc = ScanAccountDirBloc.of(widget.account);
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
var _smartAlbums = <Album>[]; var _smartAlbums = <Album>[];
final _buildItemQueue = final _buildItemQueue =

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/search.dart'; import 'package:nc_photos/entity/search.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -438,11 +439,13 @@ class _HomeSearchState extends State<HomeSearch>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -453,10 +456,11 @@ class _HomeSearchState extends State<HomeSearch>
} }
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) { Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -471,17 +475,19 @@ class _HomeSearchState extends State<HomeSearch>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
} }
Future<void> _onSelectionArchivePressed(BuildContext context) async { Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -489,13 +495,14 @@ class _HomeSearchState extends State<HomeSearch>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
); );
} }
Future<void> _onSelectionDeletePressed(BuildContext context) async { Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -503,9 +510,9 @@ class _HomeSearchState extends State<HomeSearch>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await RemoveSelectionHandler()( await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
isMoveToTrash: true, isMoveToTrash: true,
); );
} }
@ -583,7 +590,7 @@ class _HomeSearchState extends State<HomeSearch>
late final _thumbSize = late final _thumbSize =
photo_list_util.getThumbSize(_thumbZoomLevel).toDouble(); photo_list_util.getThumbSize(_thumbZoomLevel).toDouble();
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
static final _log = Logger("widget.home_search._HomeSearchState"); static final _log = Logger("widget.home_search._HomeSearchState");
} }

View file

@ -9,7 +9,7 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/help_utils.dart' as help_util; import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/object_extension.dart';
@ -27,7 +27,7 @@ class ImageEditorArguments {
const ImageEditorArguments(this.account, this.file); const ImageEditorArguments(this.account, this.file);
final Account account; final Account account;
final File file; final FileDescriptor file;
} }
class ImageEditor extends StatefulWidget { class ImageEditor extends StatefulWidget {
@ -54,7 +54,7 @@ class ImageEditor extends StatefulWidget {
createState() => _ImageEditorState(); createState() => _ImageEditorState();
final Account account; final Account account;
final File file; final FileDescriptor file;
} }
class _ImageEditorState extends State<ImageEditor> { class _ImageEditorState extends State<ImageEditor> {
@ -275,7 +275,7 @@ class _ImageEditorState extends State<ImageEditor> {
Future<void> _onSavePressed(BuildContext context) async { Future<void> _onSavePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
await ImageProcessor.filter( await ImageProcessor.filter(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
4096, 4096,
3072, 3072,

View file

@ -11,7 +11,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/help_utils.dart'; import 'package:nc_photos/help_utils.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -34,7 +34,7 @@ class ImageEnhancerArguments {
const ImageEnhancerArguments(this.account, this.file, this.isSaveToServer); const ImageEnhancerArguments(this.account, this.file, this.isSaveToServer);
final Account account; final Account account;
final File file; final FileDescriptor file;
final bool isSaveToServer; final bool isSaveToServer;
} }
@ -45,8 +45,8 @@ class ImageEnhancer extends StatefulWidget {
builder: (context) => ImageEnhancer.fromArgs(args), builder: (context) => ImageEnhancer.fromArgs(args),
); );
static bool isSupportedFormat(File file) => static bool isSupportedFormat(FileDescriptor file) =>
file_util.isSupportedImageFormat(file) && file.contentType != "image/gif"; file_util.isSupportedImageFormat(file) && file.fdMime != "image/gif";
const ImageEnhancer({ const ImageEnhancer({
super.key, super.key,
@ -67,7 +67,7 @@ class ImageEnhancer extends StatefulWidget {
createState() => _ImageEnhancerState(); createState() => _ImageEnhancerState();
final Account account; final Account account;
final File file; final FileDescriptor file;
final bool isSaveToServer; final bool isSaveToServer;
} }
@ -181,7 +181,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
switch (_selectedOption.algorithm) { switch (_selectedOption.algorithm) {
case _Algorithm.zeroDce: case _Algorithm.zeroDce:
await ImageProcessor.zeroDce( await ImageProcessor.zeroDce(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(), _c.pref.getEnhanceMaxHeightOr(),
@ -195,7 +195,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
case _Algorithm.deepLab3Portrait: case _Algorithm.deepLab3Portrait:
await ImageProcessor.deepLab3Portrait( await ImageProcessor.deepLab3Portrait(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(), _c.pref.getEnhanceMaxHeightOr(),
@ -209,7 +209,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
case _Algorithm.esrgan: case _Algorithm.esrgan:
await ImageProcessor.esrgan( await ImageProcessor.esrgan(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(), _c.pref.getEnhanceMaxHeightOr(),
@ -222,7 +222,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
case _Algorithm.arbitraryStyleTransfer: case _Algorithm.arbitraryStyleTransfer:
await ImageProcessor.arbitraryStyleTransfer( await ImageProcessor.arbitraryStyleTransfer(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
math.min( math.min(
_c.pref.getEnhanceMaxWidthOr(), _isAtLeast5GbRam() ? 1600 : 1280), _c.pref.getEnhanceMaxWidthOr(), _isAtLeast5GbRam() ? 1600 : 1280),
@ -239,7 +239,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
case _Algorithm.deepLab3ColorPop: case _Algorithm.deepLab3ColorPop:
await ImageProcessor.deepLab3ColorPop( await ImageProcessor.deepLab3ColorPop(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(), _c.pref.getEnhanceMaxHeightOr(),
@ -253,7 +253,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
case _Algorithm.neurOp: case _Algorithm.neurOp:
await ImageProcessor.neurOp( await ImageProcessor.neurOp(
"${widget.account.url}/${widget.file.path}", "${widget.account.url}/${widget.file.fdPath}",
widget.file.filename, widget.file.filename,
_c.pref.getEnhanceMaxWidthOr(), _c.pref.getEnhanceMaxWidthOr(),
_c.pref.getEnhanceMaxHeightOr(), _c.pref.getEnhanceMaxHeightOr(),

View file

@ -6,9 +6,8 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/entity/file.dart' as app; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/flutter_util.dart' as flutter_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod; import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
@ -91,7 +90,7 @@ class RemoteImageViewer extends StatefulWidget {
@override @override
createState() => _RemoteImageViewerState(); createState() => _RemoteImageViewerState();
static void preloadImage(Account account, app.File file) { static void preloadImage(Account account, FileDescriptor file) {
LargeImageCacheManager.inst.getFileStream( LargeImageCacheManager.inst.getFileStream(
_getImageUrl(account, file), _getImageUrl(account, file),
headers: { headers: {
@ -101,7 +100,7 @@ class RemoteImageViewer extends StatefulWidget {
} }
final Account account; final Account account;
final app.File file; final FileDescriptor file;
final bool canZoom; final bool canZoom;
final VoidCallback? onLoaded; final VoidCallback? onLoaded;
final ValueChanged<double>? onHeightChanged; final ValueChanged<double>? onHeightChanged;
@ -116,8 +115,6 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
onHeightChanged: widget.onHeightChanged, onHeightChanged: widget.onHeightChanged,
onZoomStarted: widget.onZoomStarted, onZoomStarted: widget.onZoomStarted,
onZoomEnded: widget.onZoomEnded, onZoomEnded: widget.onZoomEnded,
child: Hero(
tag: flutter_util.getImageHeroTag(widget.file),
child: mod.CachedNetworkImage( child: mod.CachedNetworkImage(
cacheManager: LargeImageCacheManager.inst, cacheManager: LargeImageCacheManager.inst,
imageUrl: _getImageUrl(widget.account, widget.file), imageUrl: _getImageUrl(widget.account, widget.file),
@ -136,7 +133,6 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
return child; return child;
}, },
), ),
),
); );
void _onItemLoaded() { void _onItemLoaded() {
@ -333,8 +329,8 @@ class _ImageViewerState extends State<_ImageViewer>
static final _log = Logger("widget.image_viewer._ImageViewerState"); static final _log = Logger("widget.image_viewer._ImageViewerState");
} }
String _getImageUrl(Account account, app.File file) { String _getImageUrl(Account account, FileDescriptor file) {
if (file.contentType == "image/gif") { if (file.fdMime == "image/gif") {
return api_util.getFileUrl(account, file); return api_util.getFileUrl(account, file);
} else { } else {
return api_util.getFilePreviewUrl( return api_util.getFilePreviewUrl(

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/share_handler.dart';
@ -135,9 +137,10 @@ class _LocalFileViewerState extends State<LocalFileViewer> {
} }
Future<void> _onSharePressed(BuildContext context) async { Future<void> _onSharePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onSharePressed] Sharing file: ${file.logTag}"); _log.info("[_onSharePressed] Sharing file: ${file.logTag}");
await ShareHandler(context: context).shareLocalFiles([file]); await ShareHandler(c, context: context).shareLocalFiles([file]);
} }
void _onMenuSelected(BuildContext context, _AppBarMenuOption option) { void _onMenuSelected(BuildContext context, _AppBarMenuOption option) {

View file

@ -16,6 +16,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -351,11 +352,13 @@ class _PersonBrowserState extends State<PersonBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -366,10 +369,11 @@ class _PersonBrowserState extends State<PersonBrowser>
} }
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) { Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -384,17 +388,19 @@ class _PersonBrowserState extends State<PersonBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
} }
Future<void> _onSelectionArchivePressed(BuildContext context) async { Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -402,13 +408,14 @@ class _PersonBrowserState extends State<PersonBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
); );
} }
Future<void> _onSelectionDeletePressed(BuildContext context) async { Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -416,16 +423,15 @@ class _PersonBrowserState extends State<PersonBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await RemoveSelectionHandler()( await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
isMoveToTrash: true, isMoveToTrash: true,
); );
} }
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) { void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
if (_backingFiles.containsIf(ev.file, (a, b) => a.fileId == b.fileId) != if (_backingFiles.containsIf(ev.file, (a, b) => a.fdId == b.fdId) != true) {
true) {
return; return;
} }
_refreshThrottler.trigger( _refreshThrottler.trigger(
@ -474,7 +480,7 @@ class _PersonBrowserState extends State<PersonBrowser>
late final DiContainer _c; late final DiContainer _c;
late final ListFaceFileBloc _bloc = ListFaceFileBloc(_c); late final ListFaceFileBloc _bloc = ListFaceFileBloc(_c);
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
final _buildItemQueue = final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>(); ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();

View file

@ -7,8 +7,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/flutter_util.dart'as flutter_util; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart'; import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
@ -33,24 +32,24 @@ abstract class PhotoListFileItem extends SelectableItem {
other is PhotoListFileItem && file.compareServerIdentity(other.file); other is PhotoListFileItem && file.compareServerIdentity(other.file);
@override @override
get hashCode => file.path.hashCode; get hashCode => file.fdPath.hashCode;
@override @override
toString() => "$runtimeType {" toString() => "$runtimeType {"
"fileIndex: $fileIndex, " "fileIndex: $fileIndex, "
"file: ${file.path}, " "file: ${file.fdPath}, "
"shouldShowFavoriteBadge: $shouldShowFavoriteBadge, " "shouldShowFavoriteBadge: $shouldShowFavoriteBadge, "
"}"; "}";
final int fileIndex; final int fileIndex;
final File file; final FileDescriptor file;
final bool shouldShowFavoriteBadge; final bool shouldShowFavoriteBadge;
} }
class PhotoListImageItem extends PhotoListFileItem { class PhotoListImageItem extends PhotoListFileItem {
const PhotoListImageItem({ const PhotoListImageItem({
required int fileIndex, required int fileIndex,
required File file, required FileDescriptor file,
required this.account, required this.account,
required this.previewUrl, required this.previewUrl,
required bool shouldShowFavoriteBadge, required bool shouldShowFavoriteBadge,
@ -64,9 +63,8 @@ class PhotoListImageItem extends PhotoListFileItem {
buildWidget(BuildContext context) => PhotoListImage( buildWidget(BuildContext context) => PhotoListImage(
account: account, account: account,
previewUrl: previewUrl, previewUrl: previewUrl,
isGif: file.contentType == "image/gif", isGif: file.fdMime == "image/gif",
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true, isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
heroKey: flutter_util.getImageHeroTag(file),
); );
final Account account; final Account account;
@ -76,7 +74,7 @@ class PhotoListImageItem extends PhotoListFileItem {
class PhotoListVideoItem extends PhotoListFileItem { class PhotoListVideoItem extends PhotoListFileItem {
const PhotoListVideoItem({ const PhotoListVideoItem({
required int fileIndex, required int fileIndex,
required File file, required FileDescriptor file,
required this.account, required this.account,
required this.previewUrl, required this.previewUrl,
required bool shouldShowFavoriteBadge, required bool shouldShowFavoriteBadge,
@ -90,7 +88,7 @@ class PhotoListVideoItem extends PhotoListFileItem {
buildWidget(BuildContext context) => PhotoListVideo( buildWidget(BuildContext context) => PhotoListVideo(
account: account, account: account,
previewUrl: previewUrl, previewUrl: previewUrl,
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true, isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
); );
final Account account; final Account account;

View file

@ -7,15 +7,15 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart'; import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
class DateGroupHelper { class DateGroupHelper {
DateGroupHelper({ DateGroupHelper({
required this.isMonthOnly, required this.isMonthOnly,
}); });
DateTime? onFile(File file) { DateTime? onFile(FileDescriptor file) {
final newDate = file.bestDateTime.toLocal(); final newDate = file.fdDateTime.toLocal();
if (newDate.year != _currentDate?.year || if (newDate.year != _currentDate?.year ||
newDate.month != _currentDate?.month || newDate.month != _currentDate?.month ||
(!isMonthOnly && newDate.day != _currentDate?.day)) { (!isMonthOnly && newDate.day != _currentDate?.day)) {
@ -40,8 +40,8 @@ class MemoryAlbumHelper {
}) : today = (today?.toLocal() ?? DateTime.now()).toMidnight(), }) : today = (today?.toLocal() ?? DateTime.now()).toMidnight(),
dayRange = math.max(dayRange, 0); dayRange = math.max(dayRange, 0);
void addFile(File f) { void addFile(FileDescriptor f) {
final date = f.bestDateTime.toLocal().toMidnight(); final date = f.fdDateTime.toLocal().toMidnight();
final diff = today.difference(date).inDays; final diff = today.difference(date).inDays;
if (diff < 300) { if (diff < 300) {
return; return;
@ -49,8 +49,7 @@ class MemoryAlbumHelper {
for (final dy in [0, -1, 1]) { for (final dy in [0, -1, 1]) {
if (today.copyWith(year: date.year + dy).difference(date).abs().inDays <= if (today.copyWith(year: date.year + dy).difference(date).abs().inDays <=
dayRange) { dayRange) {
_log.fine( _log.fine("[addFile] Add file (${f.fdDateTime}) to ${date.year + dy}");
"[addFile] Add file (${f.bestDateTime}) to ${date.year + dy}");
_addFileToYear(f, date.year + dy); _addFileToYear(f, date.year + dy);
break; break;
} }
@ -69,13 +68,13 @@ class MemoryAlbumHelper {
provider: AlbumMemoryProvider( provider: AlbumMemoryProvider(
year: e.key, month: today.month, day: today.day), year: e.key, month: today.month, day: today.day),
coverProvider: coverProvider:
AlbumManualCoverProvider(coverFile: e.value.coverFile), AlbumMemoryCoverProvider(coverFile: e.value.coverFile),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
)) ))
.toList(); .toList();
} }
void _addFileToYear(File f, int year) { void _addFileToYear(FileDescriptor f, int year) {
final item = _data[year]; final item = _data[year];
final date = today.copyWith(year: year); final date = today.copyWith(year: year);
if (item == null) { if (item == null) {
@ -117,10 +116,10 @@ class _MemoryAlbumHelperItem {
_MemoryAlbumHelperItem(this.date, this.coverFile) _MemoryAlbumHelperItem(this.date, this.coverFile)
: coverDiff = getCoverDiff(date, coverFile); : coverDiff = getCoverDiff(date, coverFile);
static Duration getCoverDiff(DateTime date, File f) => static Duration getCoverDiff(DateTime date, FileDescriptor f) =>
f.bestDateTime.difference(date.copyWith(hour: 12)).abs(); f.fdDateTime.difference(date.copyWith(hour: 12)).abs();
final DateTime date; final DateTime date;
File coverFile; FileDescriptor coverFile;
Duration coverDiff; Duration coverDiff;
} }

View file

@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/language_util.dart' as language_util;
@ -295,11 +296,13 @@ class _PlaceBrowserState extends State<PlaceBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -310,10 +313,11 @@ class _PlaceBrowserState extends State<PlaceBrowser>
} }
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) { Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -328,17 +332,19 @@ class _PlaceBrowserState extends State<PlaceBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
} }
Future<void> _onSelectionArchivePressed(BuildContext context) async { Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -346,13 +352,14 @@ class _PlaceBrowserState extends State<PlaceBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
); );
} }
Future<void> _onSelectionDeletePressed(BuildContext context) async { Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -360,9 +367,9 @@ class _PlaceBrowserState extends State<PlaceBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await RemoveSelectionHandler()( await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
isMoveToTrash: true, isMoveToTrash: true,
); );
} }
@ -408,7 +415,7 @@ class _PlaceBrowserState extends State<PlaceBrowser>
late final DiContainer _c; late final DiContainer _c;
late final ListLocationFileBloc _bloc = ListLocationFileBloc(_c); late final ListLocationFileBloc _bloc = ListLocationFileBloc(_c);
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
final _buildItemQueue = final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>(); ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';

View file

@ -3,6 +3,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart'; import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/entity/share/data_source.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
@ -26,7 +26,7 @@ class SlideshowViewerArguments {
); );
final Account account; final Account account;
final List<File> streamFiles; final List<FileDescriptor> streamFiles;
final int startIndex; final int startIndex;
final SlideshowConfig config; final SlideshowConfig config;
} }
@ -59,7 +59,7 @@ class SlideshowViewer extends StatefulWidget {
createState() => _SlideshowViewerState(); createState() => _SlideshowViewerState();
final Account account; final Account account;
final List<File> streamFiles; final List<FileDescriptor> streamFiles;
final int startIndex; final int startIndex;
final SlideshowConfig config; final SlideshowConfig config;
} }
@ -185,7 +185,7 @@ class _SlideshowViewerState extends State<SlideshowViewer>
} else if (file_util.isSupportedVideoFormat(file)) { } else if (file_util.isSupportedVideoFormat(file)) {
return _buildVideoView(context, index); return _buildVideoView(context, index);
} else { } else {
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}"); _log.shout("[_buildItemView] Unknown file format: ${file.fdMime}");
return Container(); return Container();
} }
} }

View file

@ -216,7 +216,8 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
} }
void _onDownloadPressed() { void _onDownloadPressed() {
DownloadHandler().downloadFiles( final c = KiwiContainer().resolve<DiContainer>();
DownloadHandler(c).downloadFiles(
widget.account, widget.account,
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(), _sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
parentDir: _album!.name, parentDir: _album!.name,
@ -236,11 +237,13 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -251,10 +254,11 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
} }
Future<void> _onSelectionAddPressed(BuildContext context) async { Future<void> _onSelectionAddPressed(BuildContext context) async {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -269,11 +273,12 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<_FileListItem>() .whereType<_FileListItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });

View file

@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/tag.dart'; import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
@ -304,11 +305,13 @@ class _TagBrowserState extends State<TagBrowser>
} }
void _onSelectionSharePressed(BuildContext context) { void _onSelectionSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
ShareHandler( ShareHandler(
c,
context: context, context: context,
clearSelection: () { clearSelection: () {
setState(() { setState(() {
@ -319,10 +322,11 @@ class _TagBrowserState extends State<TagBrowser>
} }
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) { Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: selectedListItems selection: selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(), .toList(),
@ -337,17 +341,19 @@ class _TagBrowserState extends State<TagBrowser>
} }
void _onSelectionDownloadPressed() { void _onSelectionDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final selected = selectedListItems final selected = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
DownloadHandler().downloadFiles(widget.account, selected); DownloadHandler(c).downloadFiles(widget.account, selected);
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
} }
Future<void> _onSelectionArchivePressed(BuildContext context) async { Future<void> _onSelectionArchivePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -355,13 +361,14 @@ class _TagBrowserState extends State<TagBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
); );
} }
Future<void> _onSelectionDeletePressed(BuildContext context) async { Future<void> _onSelectionDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles = selectedListItems final selectedFiles = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
@ -369,16 +376,15 @@ class _TagBrowserState extends State<TagBrowser>
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
await RemoveSelectionHandler()( await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: selectedFiles, selection: selectedFiles,
isMoveToTrash: true, isMoveToTrash: true,
); );
} }
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) { void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
if (_backingFiles.containsIf(ev.file, (a, b) => a.fileId == b.fileId) != if (_backingFiles.containsIf(ev.file, (a, b) => a.fdId == b.fdId) != true) {
true) {
return; return;
} }
_refreshThrottler.trigger( _refreshThrottler.trigger(
@ -427,7 +433,7 @@ class _TagBrowserState extends State<TagBrowser>
late final DiContainer _c; late final DiContainer _c;
late final ListTagFileBloc _bloc = ListTagFileBloc(_c); late final ListTagFileBloc _bloc = ListTagFileBloc(_c);
var _backingFiles = <File>[]; var _backingFiles = <FileDescriptor>[];
final _buildItemQueue = final _buildItemQueue =
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>(); ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/language_util.dart' as language_util;
@ -19,6 +20,7 @@ import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/restore_trashbin.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/builder/photo_list_item_builder.dart';
import 'package:nc_photos/widget/empty_list_indicator.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart';
@ -294,18 +296,20 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
.restoreSelectedProcessingNotification(selectedListItems.length)), .restoreSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
final selectedFiles = selectedListItems final selection = selectedListItems
.whereType<PhotoListFileItem>() .whereType<PhotoListFileItem>()
.map((e) => e.file) .map((e) => e.file)
.toList(); .toList();
setState(() { setState(() {
clearSelectedItems(); clearSelectedItems();
}); });
final c = KiwiContainer().resolve<DiContainer>();
final selectedFiles =
await InflateFileDescriptor(c)(widget.account, selection);
final failures = <File>[]; final failures = <File>[];
for (final f in selectedFiles) { for (final f in selectedFiles) {
try { try {
await RestoreTrashbin(KiwiContainer().resolve<DiContainer>())( await RestoreTrashbin(c)(widget.account, f);
widget.account, f);
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(
"[_onSelectionAppBarRestorePressed] Failed while restoring file: ${logFilename(f.path)}", "[_onSelectionAppBarRestorePressed] Failed while restoring file: ${logFilename(f.path)}",
@ -363,7 +367,7 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
(result) { (result) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_backingFiles = result.backingFiles; _backingFiles = result.backingFiles.cast();
itemStreamListItems = result.listItems; itemStreamListItems = result.listItems;
}); });
} }
@ -382,10 +386,11 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
return _deleteFiles(selectedFiles); return _deleteFiles(selectedFiles);
} }
Future<void> _deleteFiles(List<File> files) async { Future<void> _deleteFiles(List<FileDescriptor> files) async {
await RemoveSelectionHandler()( final c = KiwiContainer().resolve<DiContainer>();
await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: files, selection: files,
shouldCleanupAlbum: false, shouldCleanupAlbum: false,
); );
} }
@ -415,7 +420,9 @@ enum _SelectionAppBarMenuOption {
delete, delete,
} }
int _fileSorter(File a, File b) { int _fileSorter(FileDescriptor fdA, FileDescriptor fdB) {
final a = fdA as File;
final b = fdB as File;
if (a.trashbinDeletionTime == null && b.trashbinDeletionTime == null) { if (a.trashbinDeletionTime == null && b.trashbinDeletionTime == null) {
// ? // ?
return 0; return 0;

View file

@ -311,11 +311,12 @@ class _TrashbinViewerState extends State<TrashbinViewer> {
} }
Future<void> _delete(BuildContext context) async { Future<void> _delete(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_delete] Removing file: ${file.path}"); _log.info("[_delete] Removing file: ${file.path}");
final count = await RemoveSelectionHandler()( final count = await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: [file], selection: [file],
shouldCleanupAlbum: false, shouldCleanupAlbum: false,
isRemoveOpened: true, isRemoveOpened: true,
); );

View file

@ -4,7 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
@ -33,7 +33,7 @@ class VideoViewer extends StatefulWidget {
createState() => _VideoViewerState(); createState() => _VideoViewerState();
final Account account; final Account account;
final File file; final FileDescriptor file;
final VoidCallback? onLoaded; final VoidCallback? onLoaded;
final VoidCallback? onLoadFailure; final VoidCallback? onLoadFailure;
final ValueChanged<double>? onHeightChanged; final ValueChanged<double>? onHeightChanged;

View file

@ -12,7 +12,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/flutter_util.dart'; import 'package:nc_photos/flutter_util.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -21,6 +21,7 @@ import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/share_handler.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/animated_visibility.dart'; import 'package:nc_photos/widget/animated_visibility.dart';
import 'package:nc_photos/widget/disposable.dart'; import 'package:nc_photos/widget/disposable.dart';
@ -45,7 +46,7 @@ class ViewerArguments {
}); });
final Account account; final Account account;
final List<File> streamFiles; final List<FileDescriptor> streamFiles;
final int startIndex; final int startIndex;
final Album? album; final Album? album;
} }
@ -81,7 +82,7 @@ class Viewer extends StatefulWidget {
createState() => _ViewerState(); createState() => _ViewerState();
final Account account; final Account account;
final List<File> streamFiles; final List<FileDescriptor> streamFiles;
final int startIndex; final int startIndex;
/// The album these files belongs to, or null /// The album these files belongs to, or null
@ -166,7 +167,8 @@ class _ViewerState extends State<Viewer>
foregroundColor: Colors.white.withOpacity(.87), foregroundColor: Colors.white.withOpacity(.87),
actions: [ actions: [
if (!_isDetailPaneActive && _canOpenDetailPane()) ...[ if (!_isDetailPaneActive && _canOpenDetailPane()) ...[
(_pageStates[index]?.favoriteOverride ?? file.isFavorite) == (_pageStates[index]?.favoriteOverride ??
file.fdIsFavorite) ==
true true
? IconButton( ? IconButton(
icon: const Icon(Icons.star), icon: const Icon(Icons.star),
@ -313,7 +315,7 @@ class _ViewerState extends State<Viewer>
visible: _isShowDetailPane, visible: _isShowDetailPane,
child: ViewerDetailPane( child: ViewerDetailPane(
account: widget.account, account: widget.account,
file: widget.streamFiles[index], fd: widget.streamFiles[index],
album: widget.album, album: widget.album,
onSlideshowPressed: _onSlideshowPressed, onSlideshowPressed: _onSlideshowPressed,
), ),
@ -336,7 +338,7 @@ class _ViewerState extends State<Viewer>
} else if (file_util.isSupportedVideoFormat(file)) { } else if (file_util.isSupportedVideoFormat(file)) {
return _buildVideoView(context, index); return _buildVideoView(context, index);
} else { } else {
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}"); _log.shout("[_buildItemView] Unknown file format: ${file.fdMime}");
_pageStates[index]!.itemHeight = 0; _pageStates[index]!.itemHeight = 0;
return Container(); return Container();
} }
@ -483,11 +485,17 @@ class _ViewerState extends State<Viewer>
_pageStates[index] = _PageState(ScrollController( _pageStates[index] = _PageState(ScrollController(
initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane
? _calcDetailPaneOpenedScrollPosition(index) ? _calcDetailPaneOpenedScrollPosition(index)
: 0)); : 0,
));
} }
/// Called when the page is being built after previously moved out of view /// Called when the page is being built after previously moved out of view
void _onRecreatePageAfterMovedOut(BuildContext context, int index) { void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
_pageStates[index]!.setScrollController(ScrollController(
initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane
? _calcDetailPaneOpenedScrollPosition(index)
: 0,
));
if (_isShowDetailPane && !_isClosingDetailPane) { if (_isShowDetailPane && !_isClosingDetailPane) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_pageStates[index]!.itemHeight != null) { if (_pageStates[index]!.itemHeight != null) {
@ -509,8 +517,9 @@ class _ViewerState extends State<Viewer>
return; return;
} }
final file = widget.streamFiles[_viewerController.currentPage]; final fd = widget.streamFiles[_viewerController.currentPage];
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first;
setState(() { setState(() {
_pageStates[index]!.favoriteOverride = true; _pageStates[index]!.favoriteOverride = true;
}); });
@ -542,8 +551,9 @@ class _ViewerState extends State<Viewer>
return; return;
} }
final file = widget.streamFiles[_viewerController.currentPage]; final fd = widget.streamFiles[_viewerController.currentPage];
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first;
setState(() { setState(() {
_pageStates[index]!.favoriteOverride = false; _pageStates[index]!.favoriteOverride = false;
}); });
@ -578,8 +588,10 @@ class _ViewerState extends State<Viewer>
} }
void _onSharePressed(BuildContext context) { void _onSharePressed(BuildContext context) {
final c = KiwiContainer().resolve<DiContainer>();
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
ShareHandler( ShareHandler(
c,
context: context, context: context,
).shareFiles(widget.account, [file]); ).shareFiles(widget.account, [file]);
} }
@ -591,7 +603,7 @@ class _ViewerState extends State<Viewer>
return; return;
} }
_log.info("[_onEditPressed] Edit file: ${file.path}"); _log.info("[_onEditPressed] Edit file: ${file.fdPath}");
Navigator.of(context).pushNamed(ImageEditor.routeName, Navigator.of(context).pushNamed(ImageEditor.routeName,
arguments: ImageEditorArguments(widget.account, file)); arguments: ImageEditorArguments(widget.account, file));
} }
@ -604,24 +616,26 @@ class _ViewerState extends State<Viewer>
} }
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
_log.info("[_onEnhancePressed] Enhance file: ${file.path}"); _log.info("[_onEnhancePressed] Enhance file: ${file.fdPath}");
Navigator.of(context).pushNamed(ImageEnhancer.routeName, Navigator.of(context).pushNamed(ImageEnhancer.routeName,
arguments: ImageEnhancerArguments( arguments: ImageEnhancerArguments(
widget.account, file, c.pref.isSaveEditResultToServerOr())); widget.account, file, c.pref.isSaveEditResultToServerOr()));
} }
void _onDownloadPressed() { void _onDownloadPressed() {
final c = KiwiContainer().resolve<DiContainer>();
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDownloadPressed] Downloading file: ${file.path}"); _log.info("[_onDownloadPressed] Downloading file: ${file.fdPath}");
DownloadHandler().downloadFiles(widget.account, [file]); DownloadHandler(c).downloadFiles(widget.account, [file]);
} }
Future<void> _onDeletePressed(BuildContext context) async { Future<void> _onDeletePressed(BuildContext context) async {
final c = KiwiContainer().resolve<DiContainer>();
final file = widget.streamFiles[_viewerController.currentPage]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDeletePressed] Removing file: ${file.path}"); _log.info("[_onDeletePressed] Removing file: ${file.fdPath}");
final count = await RemoveSelectionHandler()( final count = await RemoveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: [file], selection: [file],
isRemoveOpened: true, isRemoveOpened: true,
isMoveToTrash: true, isMoveToTrash: true,
); );
@ -754,6 +768,10 @@ class _ViewerState extends State<Viewer>
class _PageState { class _PageState {
_PageState(this.scrollController); _PageState(this.scrollController);
void setScrollController(ScrollController c) {
scrollController = c;
}
ScrollController scrollController; ScrollController scrollController;
double? itemHeight; double? itemHeight;
bool hasLoaded = false; bool hasLoaded = false;

View file

@ -16,6 +16,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/exif_extension.dart'; import 'package:nc_photos/entity/exif_extension.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/location_util.dart' as location_util; import 'package:nc_photos/location_util.dart' as location_util;
import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/notified_action.dart';
@ -24,6 +25,7 @@ import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/list_file_tag.dart'; import 'package:nc_photos/use_case/list_file_tag.dart';
import 'package:nc_photos/use_case/remove_from_album.dart'; import 'package:nc_photos/use_case/remove_from_album.dart';
import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_album.dart';
@ -42,7 +44,7 @@ class ViewerDetailPane extends StatefulWidget {
const ViewerDetailPane({ const ViewerDetailPane({
Key? key, Key? key,
required this.account, required this.account,
required this.file, required this.fd,
this.album, this.album,
this.onSlideshowPressed, this.onSlideshowPressed,
}) : super(key: key); }) : super(key: key);
@ -51,7 +53,7 @@ class ViewerDetailPane extends StatefulWidget {
createState() => _ViewerDetailPaneState(); createState() => _ViewerDetailPaneState();
final Account account; final Account account;
final File file; final FileDescriptor fd;
/// The album this file belongs to, or null /// The album this file belongs to, or null
final Album? album; final Album? album;
@ -63,6 +65,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
_ViewerDetailPaneState() { _ViewerDetailPaneState() {
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
assert(require(c)); assert(require(c));
assert(InflateFileDescriptor.require(c));
_c = c; _c = c;
} }
@ -72,27 +75,46 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
@override @override
initState() { initState() {
_log.info("[initState] File: ${widget.fd.fdPath}");
super.initState(); super.initState();
_dateTime = widget.fd.fdDateTime.toLocal();
_initFile();
}
_dateTime = widget.file.bestDateTime.toLocal(); Future<void> _initFile() async {
if (widget.file.metadata == null) { _file =
(await InflateFileDescriptor(_c)(widget.account, [widget.fd])).first;
_log.fine("[_initFile] File inflated");
// update file
if (mounted) {
setState(() {});
} else {
return;
}
if (_file!.metadata == null) {
_log.info("[initState] Metadata missing in File"); _log.info("[initState] Metadata missing in File");
} else { } else {
_log.info("[initState] Metadata exists in File"); _log.info("[initState] Metadata exists in File");
if (widget.file.metadata!.exif != null) { if (_file!.metadata!.exif != null) {
_initMetadata(); _initMetadata();
} }
} }
_initTags(); await _initTags();
// update tages
if (mounted) {
setState(() {});
} else {
return;
}
// postpone loading map to improve responsiveness // postpone loading map to improve responsiveness
Future.delayed(const Duration(milliseconds: 750)).then((_) { unawaited(Future.delayed(const Duration(milliseconds: 750)).then((_) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_shouldBlockGpsMap = false; _shouldBlockGpsMap = false;
}); });
} }
}); }));
} }
@override @override
@ -104,43 +126,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
Localizations.localeOf(context).languageCode) Localizations.localeOf(context).languageCode)
.format(_dateTime); .format(_dateTime);
String sizeSubStr = "";
const space = " ";
if (widget.file.metadata?.imageWidth != null &&
widget.file.metadata?.imageHeight != null) {
final pixelCount = widget.file.metadata!.imageWidth! *
widget.file.metadata!.imageHeight!;
if (pixelCount >= 500000) {
final mpCount = pixelCount / 1000000.0;
sizeSubStr += L10n.global().megapixelCount(mpCount.toStringAsFixed(1));
sizeSubStr += space;
}
sizeSubStr += _byteSizeToString(widget.file.contentLength ?? 0);
}
String cameraSubStr = "";
if (_fNumber != null) {
cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
}
if (_exposureTime != null) {
cameraSubStr += L10n.global().secondCountSymbol(_exposureTime!);
cameraSubStr += space;
}
if (_focalLength != null) {
cameraSubStr += L10n.global()
.millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
cameraSubStr += space;
}
if (_isoSpeedRatings != null) {
cameraSubStr += "ISO$_isoSpeedRatings$space";
}
cameraSubStr = cameraSubStr.trim();
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_file != null) ...[
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -164,7 +155,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
label: L10n.global().addToAlbumTooltip, label: L10n.global().addToAlbumTooltip,
onPressed: () => _onAddToAlbumPressed(context), onPressed: () => _onAddToAlbumPressed(context),
), ),
if (widget.file.isArchived == true) if (widget.fd.fdIsArchived == true)
_DetailPaneButton( _DetailPaneButton(
icon: Icons.unarchive_outlined, icon: Icons.unarchive_outlined,
label: L10n.global().unarchiveTooltip, label: L10n.global().unarchiveTooltip,
@ -188,6 +179,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
padding: EdgeInsets.symmetric(horizontal: 32), padding: EdgeInsets.symmetric(horizontal: 32),
child: Divider(), child: Divider(),
), ),
],
ListTile( ListTile(
leading: ListTileCenterLeading( leading: ListTileCenterLeading(
child: Icon( child: Icon(
@ -195,10 +187,11 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
), ),
title: Text(path_lib.basenameWithoutExtension(widget.file.path)), title: Text(path_lib.basenameWithoutExtension(widget.fd.fdPath)),
subtitle: Text(widget.file.strippedPath), subtitle: Text(widget.fd.strippedPath),
), ),
if (!widget.file.isOwned(widget.account.userId)) if (_file != null) ...[
if (!_file!.isOwned(widget.account.userId))
ListTile( ListTile(
leading: ListTileCenterLeading( leading: ListTileCenterLeading(
child: Icon( child: Icon(
@ -206,8 +199,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
), ),
title: Text(widget.file.ownerDisplayName ?? title:
widget.file.ownerId!.toString()), Text(_file!.ownerDisplayName ?? _file!.ownerId!.toString()),
subtitle: Text(L10n.global().fileSharedByDescription), subtitle: Text(L10n.global().fileSharedByDescription),
), ),
if (_tags.isNotEmpty) if (_tags.isNotEmpty)
@ -237,8 +230,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
_tags[index], _tags[index],
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: color: AppTheme.getPrimaryTextColorInverse(
AppTheme.getPrimaryTextColorInverse(context), context),
), ),
), ),
), ),
@ -250,20 +243,24 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
), ),
), ),
), ),
],
ListTile( ListTile(
leading: Icon( leading: Icon(
Icons.calendar_today_outlined, Icons.calendar_today_outlined,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
title: Text("$dateStr $timeStr"), title: Text("$dateStr $timeStr"),
trailing: Icon( trailing: _file == null
? null
: Icon(
Icons.edit_outlined, Icons.edit_outlined,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
onTap: () => _onDateTimeTap(context), onTap: _file == null ? null : () => _onDateTimeTap(context),
), ),
if (widget.file.metadata?.imageWidth != null && if (_file != null) ...[
widget.file.metadata?.imageHeight != null) if (_file!.metadata?.imageWidth != null &&
_file!.metadata?.imageHeight != null)
ListTile( ListTile(
leading: ListTileCenterLeading( leading: ListTileCenterLeading(
child: Icon( child: Icon(
@ -272,8 +269,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
), ),
), ),
title: Text( title: Text(
"${widget.file.metadata!.imageWidth} x ${widget.file.metadata!.imageHeight}"), "${_file!.metadata!.imageWidth} x ${_file!.metadata!.imageHeight}"),
subtitle: Text(sizeSubStr), subtitle: Text(_buildSizeSubtitle()),
) )
else else
ListTile( ListTile(
@ -281,7 +278,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
Icons.aspect_ratio, Icons.aspect_ratio,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
title: Text(_byteSizeToString(widget.file.contentLength ?? 0)), title: Text(_byteSizeToString(_file!.contentLength ?? 0)),
), ),
if (_model != null) if (_model != null)
ListTile( ListTile(
@ -292,7 +289,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
), ),
), ),
title: Text(_model!), title: Text(_model!),
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null, subtitle: _buildCameraSubtitle()
.run((s) => s.isNotEmpty ? Text(s) : null),
), ),
if (_location?.name != null) if (_location?.name != null)
ListTile( ListTile(
@ -330,13 +328,15 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
), ),
), ),
], ],
],
), ),
); );
} }
/// Convert EXIF data to readable format /// Convert EXIF data to readable format
void _initMetadata() { void _initMetadata() {
final exif = widget.file.metadata!.exif!; assert(_file != null);
final exif = _file!.metadata!.exif!;
_log.info("[_initMetadata] $exif"); _log.info("[_initMetadata] $exif");
if (exif.make != null && exif.model != null) { if (exif.make != null && exif.model != null) {
@ -363,20 +363,60 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
if (lat != null && lng != null) { if (lat != null && lng != null) {
_log.fine("GPS: ($lat, $lng)"); _log.fine("GPS: ($lat, $lng)");
_gps = Tuple2(lat, lng); _gps = Tuple2(lat, lng);
_location = widget.file.location; _location = _file!.location;
} }
} }
Future<void> _initTags() async { Future<void> _initTags() async {
assert(_file != null);
final c = KiwiContainer().resolve<DiContainer>(); final c = KiwiContainer().resolve<DiContainer>();
try { try {
final tags = await ListFileTag(c)(widget.account, widget.file); final tags = await ListFileTag(c)(widget.account, _file!);
_tags.addAll(tags.map((t) => t.displayName)); _tags.addAll(tags.map((t) => t.displayName));
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout("[_initTags] Failed while ListFileTag", e, stackTrace); _log.shout("[_initTags] Failed while ListFileTag", e, stackTrace);
} }
} }
String _buildSizeSubtitle() {
String sizeSubStr = "";
const space = " ";
if (_file!.metadata?.imageWidth != null &&
_file!.metadata?.imageHeight != null) {
final pixelCount =
_file!.metadata!.imageWidth! * _file!.metadata!.imageHeight!;
if (pixelCount >= 500000) {
final mpCount = pixelCount / 1000000.0;
sizeSubStr += L10n.global().megapixelCount(mpCount.toStringAsFixed(1));
sizeSubStr += space;
}
sizeSubStr += _byteSizeToString(_file!.contentLength ?? 0);
}
return sizeSubStr;
}
String _buildCameraSubtitle() {
String cameraSubStr = "";
const space = " ";
if (_fNumber != null) {
cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
}
if (_exposureTime != null) {
cameraSubStr += L10n.global().secondCountSymbol(_exposureTime!);
cameraSubStr += space;
}
if (_focalLength != null) {
cameraSubStr += L10n.global()
.millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
cameraSubStr += space;
}
if (_isoSpeedRatings != null) {
cameraSubStr += "ISO$_isoSpeedRatings$space";
}
cameraSubStr = cameraSubStr.trim();
return cameraSubStr;
}
Future<void> _onRemoveFromAlbumPressed(BuildContext context) async { Future<void> _onRemoveFromAlbumPressed(BuildContext context) async {
assert(widget.album!.provider is AlbumStaticProvider); assert(widget.album!.provider is AlbumStaticProvider);
try { try {
@ -385,7 +425,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
final thisItem = AlbumStaticProvider.of(widget.album!) final thisItem = AlbumStaticProvider.of(widget.album!)
.items .items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.firstWhere((element) => element.file.path == widget.file.path); .firstWhere((element) => element.file.path == widget.fd.fdPath);
await RemoveFromAlbum(KiwiContainer().resolve<DiContainer>())( await RemoveFromAlbum(KiwiContainer().resolve<DiContainer>())(
widget.account, widget.album!, [thisItem]); widget.account, widget.album!, [thisItem]);
if (mounted) { if (mounted) {
@ -403,9 +443,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
Future<void> _onSetAlbumCoverPressed(BuildContext context) async { Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
assert(_file != null);
assert(widget.album != null); assert(widget.album != null);
_log.info( _log.info(
"[_onSetAlbumCoverPressed] Set '${widget.file.path}' as album cover for '${widget.album!.name}'"); "[_onSetAlbumCoverPressed] Set '${widget.fd.fdPath}' as album cover for '${widget.album!.name}'");
try { try {
await NotifiedAction( await NotifiedAction(
() async { () async {
@ -413,7 +454,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
widget.account, widget.account,
widget.album!.copyWith( widget.album!.copyWith(
coverProvider: AlbumManualCoverProvider( coverProvider: AlbumManualCoverProvider(
coverFile: widget.file, coverFile: _file!,
), ),
)); ));
}, },
@ -428,20 +469,23 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
Future<void> _onAddToAlbumPressed(BuildContext context) { Future<void> _onAddToAlbumPressed(BuildContext context) {
return AddSelectionToAlbumHandler()( assert(_file != null);
final c = KiwiContainer().resolve<DiContainer>();
return AddSelectionToAlbumHandler(c)(
context: context, context: context,
account: widget.account, account: widget.account,
selectedFiles: [widget.file], selection: [_file!],
clearSelection: () {}, clearSelection: () {},
); );
} }
Future<void> _onArchivePressed(BuildContext context) async { Future<void> _onArchivePressed(BuildContext context) async {
_log.info("[_onArchivePressed] Archive file: ${widget.file.path}"); assert(_file != null);
final count = _log.info("[_onArchivePressed] Archive file: ${widget.fd.fdPath}");
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())( final c = KiwiContainer().resolve<DiContainer>();
final count = await ArchiveSelectionHandler(c)(
account: widget.account, account: widget.account,
selectedFiles: [widget.file], selection: [_file!],
); );
if (count == 1) { if (count == 1) {
if (mounted) { if (mounted) {
@ -451,12 +495,13 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
Future<void> _onUnarchivePressed(BuildContext context) async { Future<void> _onUnarchivePressed(BuildContext context) async {
_log.info("[_onUnarchivePressed] Unarchive file: ${widget.file.path}"); assert(_file != null);
_log.info("[_onUnarchivePressed] Unarchive file: ${widget.fd.fdPath}");
try { try {
await NotifiedAction( await NotifiedAction(
() async { () async {
await UpdateProperty(_c.fileRepo) await UpdateProperty(_c.fileRepo)
.updateIsArchived(widget.account, widget.file, false); .updateIsArchived(widget.account, _file!, false);
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@ -467,7 +512,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
)(); )();
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout( _log.shout(
"[_onUnarchivePressed] Failed while archiving file: ${logFilename(widget.file.path)}", "[_onUnarchivePressed] Failed while archiving file: ${logFilename(widget.fd.fdPath)}",
e, e,
stackTrace); stackTrace);
} }
@ -484,6 +529,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
void _onDateTimeTap(BuildContext context) { void _onDateTimeTap(BuildContext context) {
assert(_file != null);
showDialog( showDialog(
context: context, context: context,
builder: (context) => PhotoDateTimeEditDialog(initialDateTime: _dateTime), builder: (context) => PhotoDateTimeEditDialog(initialDateTime: _dateTime),
@ -493,7 +539,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
try { try {
await UpdateProperty(_c.fileRepo) await UpdateProperty(_c.fileRepo)
.updateOverrideDateTime(widget.account, widget.file, value); .updateOverrideDateTime(widget.account, _file!, value);
if (mounted) { if (mounted) {
setState(() { setState(() {
_dateTime = value; _dateTime = value;
@ -501,7 +547,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
_log.shout( _log.shout(
"[_onDateTimeTap] Failed while updateOverrideDateTime: ${logFilename(widget.file.path)}", "[_onDateTimeTap] Failed while updateOverrideDateTime: ${logFilename(widget.fd.fdPath)}",
e, e,
stacktrace); stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
@ -527,7 +573,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
.items .items
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.firstWhere( .firstWhere(
(element) => element.file.compareServerIdentity(widget.file)); (element) => element.file.compareServerIdentity(widget.fd));
if (thisItem.addedBy == widget.account.userId) { if (thisItem.addedBy == widget.account.userId) {
return true; return true;
} }
@ -537,6 +583,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
late final DiContainer _c; late final DiContainer _c;
File? _file;
late DateTime _dateTime; late DateTime _dateTime;
// EXIF data // EXIF data
String? _model; String? _model;

View file

@ -1,6 +1,7 @@
import 'package:nc_photos/ci_string.dart'; import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/entity/exif.dart'; import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/or_null.dart'; import 'package:nc_photos/or_null.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/favorite.dart'; import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart'; import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart'; import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart'; import 'package:nc_photos/entity/sharee.dart';
import 'package:nc_photos/entity/sqlite_table.dart' as sql; import 'package:nc_photos/entity/sqlite_table.dart' as sql;
@ -377,6 +378,15 @@ File buildJpegFile({
ownerDisplayName: ownerDisplayName ?? ownerId.toString(), ownerDisplayName: ownerDisplayName ?? ownerId.toString(),
); );
FileDescriptor fileToFileDescriptor(File f) => FileDescriptor(
fdPath: f.path,
fdId: f.fileId!,
fdMime: f.contentType,
fdIsArchived: f.isArchived ?? false,
fdIsFavorite: f.isFavorite ?? false,
fdDateTime: f.bestDateTime,
);
Share buildShare({ Share buildShare({
required String id, required String id,
DateTime? stime, DateTime? stime,

View file

@ -0,0 +1,113 @@
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/list_extension.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:test/test.dart';
import '../test_util.dart' as util;
void main() {
group("InflateFileDescriptor", () {
test("one", _one);
test("multiple", _multiple);
test("missing", _missing);
});
}
/// Inflate one FileDescriptor
///
/// Expect: one file
Future<void> _one() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()
..addDir("admin")
..addJpeg("admin/test1.jpg")
..addJpeg("admin/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await util.insertFiles(c.sqliteDb, account, files);
});
expect(
await InflateFileDescriptor(c)(
account,
[util.fileToFileDescriptor(files[1])],
),
[files[1]],
);
}
/// Inflate 3 FileDescriptors
///
/// Expect: 3 files
Future<void> _multiple() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()
..addDir("admin")
..addJpeg("admin/test1.jpg")
..addJpeg("admin/test2.jpg")
..addJpeg("admin/test3.jpg")
..addJpeg("admin/test4.jpg")
..addJpeg("admin/test5.jpg")
..addJpeg("admin/test6.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await util.insertFiles(c.sqliteDb, account, files);
});
expect(
await InflateFileDescriptor(c)(
account,
files.slice(1, 7, 2).map(util.fileToFileDescriptor).toList(),
),
[files[1], files[3], files[5]],
);
}
/// Inflate a FileDescriptor that doesn't exists in the DB
///
/// Expect: throw StateError
Future<void> _missing() async {
final account = util.buildAccount();
final files = (util.FilesBuilder()
..addDir("admin")
..addJpeg("admin/test1.jpg")
..addJpeg("admin/test2.jpg"))
.build();
final c = DiContainer(
sqliteDb: util.buildTestDb(),
);
addTearDown(() => c.sqliteDb.close());
await c.sqliteDb.transaction(() async {
await c.sqliteDb.insertAccountOf(account);
await util.insertFiles(c.sqliteDb, account, files);
});
expect(
() async => await InflateFileDescriptor(c)(
account,
[
FileDescriptor(
fdPath: "remote.php/dav/files/admin/test3.jpg",
fdId: 4,
fdMime: null,
fdIsArchived: false,
fdIsFavorite: false,
fdDateTime: DateTime.now(),
),
],
),
throwsA(const TypeMatcher<StateError>()),
);
}

View file

@ -45,7 +45,7 @@ Future<void> _root() async {
(await ScanDirOffline(c)( (await ScanDirOffline(c)(
account, File(path: file_util.unstripPath(account, ".")))) account, File(path: file_util.unstripPath(account, "."))))
.toSet(), .toSet(),
files.toSet(), files.map(util.fileToFileDescriptor).toSet(),
); );
} }
@ -73,7 +73,7 @@ Future<void> _subDir() async {
(await ScanDirOffline(c)( (await ScanDirOffline(c)(
account, File(path: file_util.unstripPath(account, "test")))) account, File(path: file_util.unstripPath(account, "test"))))
.toSet(), .toSet(),
{files[1]}, [files[1]].map(util.fileToFileDescriptor).toSet(),
); );
} }
@ -102,7 +102,7 @@ Future<void> _unsupportedFile() async {
(await ScanDirOffline(c)( (await ScanDirOffline(c)(
account, File(path: file_util.unstripPath(account, ".")))) account, File(path: file_util.unstripPath(account, "."))))
.toSet(), .toSet(),
{files[0]}, [files[0]].map(util.fileToFileDescriptor).toSet(),
); );
} }
@ -140,12 +140,12 @@ Future<void> _multiAccountRoot() async {
(await ScanDirOffline(c)( (await ScanDirOffline(c)(
account, File(path: file_util.unstripPath(account, ".")))) account, File(path: file_util.unstripPath(account, "."))))
.toSet(), .toSet(),
files.toSet(), files.map(util.fileToFileDescriptor).toSet(),
); );
expect( expect(
(await ScanDirOffline(c)( (await ScanDirOffline(c)(
user1Account, File(path: file_util.unstripPath(user1Account, ".")))) user1Account, File(path: file_util.unstripPath(user1Account, "."))))
.toSet(), .toSet(),
user1Files.toSet(), user1Files.map(util.fileToFileDescriptor).toSet(),
); );
} }

View file

@ -98,7 +98,7 @@ void _prevYear() {
name: "2020", name: "2020",
provider: provider:
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day), AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -141,7 +141,7 @@ void _prevYear2DaysBefore() {
name: "2020", name: "2020",
provider: provider:
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day), AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -184,7 +184,7 @@ void _prevYear2DaysAfter() {
name: "2020", name: "2020",
provider: provider:
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day), AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -227,7 +227,7 @@ void _onFeb29AddFeb27() {
name: "2019", name: "2019",
provider: provider:
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day), AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -270,7 +270,7 @@ void _onFeb29AddMar3() {
name: "2019", name: "2019",
provider: provider:
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day), AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -313,7 +313,7 @@ void _onFeb29AddMar2LeapYear() {
name: "2016", name: "2016",
provider: provider:
AlbumMemoryProvider(year: 2016, month: today.month, day: today.day), AlbumMemoryProvider(year: 2016, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -356,7 +356,7 @@ void _onJan1AddDec31PrevYear() {
name: "2019", name: "2019",
provider: provider:
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day), AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -385,7 +385,7 @@ void _onDec31AddJan1() {
name: "2019", name: "2019",
provider: provider:
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day), AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),
@ -414,7 +414,7 @@ void _onMay15AddMay15Range0() {
name: "2021", name: "2021",
provider: provider:
AlbumMemoryProvider(year: 2021, month: today.month, day: today.day), AlbumMemoryProvider(year: 2021, month: today.month, day: today.day),
coverProvider: AlbumManualCoverProvider(coverFile: file), coverProvider: AlbumMemoryCoverProvider(coverFile: file),
sortProvider: const AlbumTimeSortProvider(isAscending: false), sortProvider: const AlbumTimeSortProvider(isAscending: false),
lastUpdated: DateTime(2021), lastUpdated: DateTime(2021),
), ),