mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Merge branch 'query-partial-file' into dev
This commit is contained in:
commit
b84ff7fbf9
78 changed files with 1288 additions and 631 deletions
|
@ -2,7 +2,7 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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/exception.dart';
|
||||
|
||||
|
@ -12,7 +12,7 @@ const reservedFilenameChars = "<>:\"/\\|?*";
|
|||
/// Return the preview image URL for [file]. See [getFilePreviewUrlRelative]
|
||||
String getFilePreviewUrl(
|
||||
Account account,
|
||||
File file, {
|
||||
FileDescriptor file, {
|
||||
required int width,
|
||||
required int height,
|
||||
String? mode,
|
||||
|
@ -27,7 +27,7 @@ String getFilePreviewUrl(
|
|||
/// cropped
|
||||
String getFilePreviewUrlRelative(
|
||||
Account account,
|
||||
File file, {
|
||||
FileDescriptor file, {
|
||||
required int width,
|
||||
required int height,
|
||||
String? mode,
|
||||
|
@ -36,14 +36,9 @@ String getFilePreviewUrlRelative(
|
|||
String url;
|
||||
if (file_util.isTrash(account, file)) {
|
||||
// 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 {
|
||||
if (file.fileId != null) {
|
||||
url = "index.php/core/preview?fileId=${file.fileId}";
|
||||
} else {
|
||||
final filePath = Uri.encodeQueryComponent(file.strippedPath);
|
||||
url = "index.php/core/preview.png?file=$filePath";
|
||||
}
|
||||
url = "index.php/core/preview?fileId=${file.fdId}";
|
||||
}
|
||||
|
||||
url = "$url&x=$width&y=$height";
|
||||
|
@ -76,12 +71,12 @@ String getFilePreviewUrlByFileId(
|
|||
return url;
|
||||
}
|
||||
|
||||
String getFileUrl(Account account, File file) {
|
||||
String getFileUrl(Account account, FileDescriptor file) {
|
||||
return "${account.url}/${getFileUrlRelative(file)}";
|
||||
}
|
||||
|
||||
String getFileUrlRelative(File file) {
|
||||
return file.path;
|
||||
String getFileUrlRelative(FileDescriptor file) {
|
||||
return file.fdPath;
|
||||
}
|
||||
|
||||
String getWebdavRootUrlRelative(Account account) =>
|
||||
|
|
|
@ -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/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/share.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
|
|
|
@ -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/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/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/event/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/pref.dart';
|
||||
import 'package:nc_photos/throttler.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_offline.dart';
|
||||
import 'package:nc_photos/use_case/sync_dir.dart';
|
||||
|
||||
abstract class ScanAccountDirBlocEvent {
|
||||
const ScanAccountDirBlocEvent();
|
||||
|
@ -63,7 +63,7 @@ abstract class ScanAccountDirBlocState {
|
|||
"}";
|
||||
}
|
||||
|
||||
final List<File> files;
|
||||
final List<FileDescriptor> files;
|
||||
}
|
||||
|
||||
class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
|
||||
|
@ -71,15 +71,15 @@ class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
|
|||
}
|
||||
|
||||
class ScanAccountDirBlocLoading extends ScanAccountDirBlocState {
|
||||
const ScanAccountDirBlocLoading(List<File> files) : super(files);
|
||||
const ScanAccountDirBlocLoading(List<FileDescriptor> files) : super(files);
|
||||
}
|
||||
|
||||
class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState {
|
||||
const ScanAccountDirBlocSuccess(List<File> files) : super(files);
|
||||
const ScanAccountDirBlocSuccess(List<FileDescriptor> files) : super(files);
|
||||
}
|
||||
|
||||
class ScanAccountDirBlocFailure extends ScanAccountDirBlocState {
|
||||
const ScanAccountDirBlocFailure(List<File> files, this.exception)
|
||||
const ScanAccountDirBlocFailure(List<FileDescriptor> files, this.exception)
|
||||
: super(files);
|
||||
|
||||
@override
|
||||
|
@ -96,7 +96,8 @@ class ScanAccountDirBlocFailure extends ScanAccountDirBlocState {
|
|||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
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
|
||||
|
@ -206,7 +207,45 @@ class ScanAccountDirBloc
|
|||
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,
|
||||
|
@ -360,13 +399,20 @@ class ScanAccountDirBloc
|
|||
);
|
||||
}
|
||||
|
||||
Future<List<File>> _queryOffline(ScanAccountDirBlocQueryBase ev) async {
|
||||
final files = <File>[];
|
||||
Future<List<FileDescriptor>> _queryOffline(
|
||||
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) {
|
||||
try {
|
||||
final dir = File(path: file_util.unstripPath(account, r));
|
||||
files.addAll(await ScanDirOffline(_c)(account, dir,
|
||||
isOnlySupportedFormat: false));
|
||||
isOnlySupportedFormat: true));
|
||||
isShareDirIncluded |= file_util.isOrUnderDir(shareDir, dir);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_queryOffline] Failed while ScanDirOffline: ${logFilename(r)}",
|
||||
|
@ -374,81 +420,12 @@ class ScanAccountDirBloc
|
|||
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) {
|
||||
_log.info("[_queryWithFileRepo] Explicitly scanning share folder");
|
||||
try {
|
||||
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);
|
||||
}
|
||||
_log.info("[_queryOffline] Explicitly scanning share folder");
|
||||
files.addAll(await Ls(_c.fileRepoLocal)(account, shareDir));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
bool _isFileOfInterest(File file) {
|
||||
|
|
|
@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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_descriptor.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
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/snack_bar_manager.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:tuple/tuple.dart';
|
||||
|
||||
class DownloadHandler {
|
||||
DownloadHandler(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(InflateFileDescriptor.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
Future<void> downloadFiles(
|
||||
Account account,
|
||||
List<File> files, {
|
||||
List<FileDescriptor> fds, {
|
||||
String? parentDir,
|
||||
}) {
|
||||
}) async {
|
||||
final files = await InflateFileDescriptor(_c)(account, fds);
|
||||
final _DownloadHandlerBase handler;
|
||||
if (platform_k.isAndroid) {
|
||||
handler = _DownlaodHandlerAndroid();
|
||||
|
@ -37,6 +47,8 @@ class DownloadHandler {
|
|||
parentDir: parentDir,
|
||||
);
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
}
|
||||
|
||||
abstract class _DownloadHandlerBase {
|
||||
|
|
|
@ -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/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/type.dart';
|
||||
|
||||
|
@ -20,6 +21,9 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
|||
case AlbumManualCoverProvider._type:
|
||||
return AlbumManualCoverProvider.fromJson(
|
||||
content.cast<String, dynamic>());
|
||||
case AlbumMemoryCoverProvider._type:
|
||||
return AlbumMemoryCoverProvider.fromJson(
|
||||
content.cast<String, dynamic>());
|
||||
default:
|
||||
_log.shout("[fromJson] Unknown type: $type");
|
||||
throw ArgumentError.value(type, "type");
|
||||
|
@ -46,7 +50,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
|||
@override
|
||||
toString();
|
||||
|
||||
File? getCover(Album album);
|
||||
FileDescriptor? getCover(Album album);
|
||||
|
||||
JsonObj _toContentJson();
|
||||
|
||||
|
@ -151,3 +155,39 @@ class AlbumManualCoverProvider extends AlbumCoverProvider {
|
|||
|
||||
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";
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
|
|||
import 'package:nc_photos/entity/album/upgrader.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.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/album/item.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/type.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
|
|
@ -5,21 +5,14 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/ci_string.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/or_null.dart';
|
||||
import 'package:nc_photos/string_extension.dart';
|
||||
import 'package:nc_photos/type.dart';
|
||||
import 'package:path/path.dart' as path_lib;
|
||||
|
||||
int compareFileDateTimeDescending(File x, File y) {
|
||||
final tmp = y.bestDateTime.compareTo(x.bestDateTime);
|
||||
if (tmp != 0) {
|
||||
return tmp;
|
||||
} else {
|
||||
// compare file name if files are modified at the same time
|
||||
return x.path.compareTo(y.path);
|
||||
}
|
||||
}
|
||||
int compareFileDateTimeDescending(File x, File y) =>
|
||||
compareFileDescriptorDateTimeDescending(x, y);
|
||||
|
||||
class ImageLocation with EquatableMixin {
|
||||
const ImageLocation({
|
||||
|
@ -293,7 +286,7 @@ class MetadataUpgraderV2 implements MetadataUpgrader {
|
|||
static final _log = Logger("entity.file.MetadataUpgraderV2");
|
||||
}
|
||||
|
||||
class File with EquatableMixin {
|
||||
class File with EquatableMixin implements FileDescriptor {
|
||||
File({
|
||||
required String path,
|
||||
this.contentLength,
|
||||
|
@ -435,6 +428,7 @@ class File with EquatableMixin {
|
|||
return product + "}";
|
||||
}
|
||||
|
||||
@override
|
||||
JsonObj toJson() {
|
||||
return {
|
||||
"path": path,
|
||||
|
@ -533,6 +527,24 @@ class File with EquatableMixin {
|
|||
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 int? contentLength;
|
||||
final String? contentType;
|
||||
|
@ -563,54 +575,6 @@ extension FileExtension on File {
|
|||
DateTime.now().toUtc();
|
||||
|
||||
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 {
|
||||
|
|
|
@ -9,6 +9,7 @@ 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/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/sqlite_table.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
|
||||
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 {
|
||||
final remote = await _remoteSrc.list(account, dir);
|
||||
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
|
||||
try {
|
||||
_log.info("[list] Update outdated local etag: ${dir.path}");
|
||||
await _c.touchManager
|
||||
.setLocalEtag(account, dir, cacheLoader.remoteTouchEtag);
|
||||
await _c.touchManager.setLocalEtag(account, dir, remoteTouchEtag);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[list] Failed while setLocalToken", e, stacktrace);
|
||||
// ignore error
|
||||
|
@ -593,16 +604,22 @@ class FileCachedDataSource implements FileDataSource {
|
|||
} on ApiException catch (e) {
|
||||
if (e.response.statusCode == 404) {
|
||||
_log.info("[list] File removed: $dir");
|
||||
if (cache != null) {
|
||||
try {
|
||||
await _sqliteDbSrc.remove(account, dir);
|
||||
} catch (e) {
|
||||
_log.warning(
|
||||
"[list] Failed while remove from db, file not cached?", e);
|
||||
}
|
||||
return [];
|
||||
} else if (e.response.statusCode == 403) {
|
||||
_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
|
||||
// listing of its parent
|
||||
await _sqliteDbSrc.emptyDir(account, dir);
|
||||
} catch (e) {
|
||||
_log.warning(
|
||||
"[list] Failed while emptying from db, file not cached?", e);
|
||||
}
|
||||
return [];
|
||||
} else {
|
||||
|
|
|
@ -6,6 +6,7 @@ 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.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
|
|
110
app/lib/entity/file_descriptor.dart
Normal file
110
app/lib/entity/file_descriptor.dart
Normal 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;
|
||||
}
|
|
@ -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/ci_string.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/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/string_extension.dart';
|
||||
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) =>
|
||||
isSupportedMime(mime) && mime.startsWith("image/") == true;
|
||||
supportedImageFormatMimes.contains(mime);
|
||||
|
||||
bool isSupportedImageFormat(File file) =>
|
||||
isSupportedImageMime(file.contentType ?? "");
|
||||
bool isSupportedImageFormat(FileDescriptor file) =>
|
||||
isSupportedImageMime(file.fdMime ?? "");
|
||||
|
||||
bool isSupportedVideoFormat(File file) =>
|
||||
isSupportedFormat(file) && file.contentType?.startsWith("video/") == true;
|
||||
bool isSupportedVideoFormat(FileDescriptor file) =>
|
||||
isSupportedFormat(file) && file.fdMime?.startsWith("video/") == true;
|
||||
|
||||
bool isMetadataSupportedMime(String mime) =>
|
||||
_metadataSupportedFormatMimes.contains(mime);
|
||||
|
||||
bool isMetadataSupportedFormat(File file) =>
|
||||
isMetadataSupportedMime(file.contentType ?? "");
|
||||
bool isMetadataSupportedFormat(FileDescriptor file) =>
|
||||
isMetadataSupportedMime(file.fdMime ?? "");
|
||||
|
||||
bool isTrash(Account account, File file) =>
|
||||
file.path.startsWith(api_util.getTrashbinPath(account));
|
||||
bool isTrash(Account account, FileDescriptor file) =>
|
||||
file.fdPath.startsWith(api_util.getTrashbinPath(account));
|
||||
|
||||
bool isAlbumFile(Account account, File file) =>
|
||||
file.path.startsWith(remote_storage_util.getRemoteAlbumsDir(account));
|
||||
|
@ -94,7 +96,7 @@ bool isMissingMetadata(File file) =>
|
|||
isSupportedImageFormat(file) &&
|
||||
(file.metadata == null || file.location == null);
|
||||
|
||||
final _supportedFormatMimes = [
|
||||
final supportedFormatMimes = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
|
@ -105,6 +107,9 @@ final _supportedFormatMimes = [
|
|||
if (platform_k.isAndroid || platform_k.isWeb) "video/webm",
|
||||
];
|
||||
|
||||
final supportedImageFormatMimes =
|
||||
supportedFormatMimes.where((f) => f.startsWith("image/")).toList();
|
||||
|
||||
const _metadataSupportedFormatMimes = [
|
||||
"image/jpeg",
|
||||
"image/heic",
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/ci_string.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/search.dart';
|
||||
import 'package:nc_photos/entity/search_util.dart' as search_util;
|
||||
import 'package:nc_photos/entity/sqlite_table_converter.dart';
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/ci_string.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/exception.dart';
|
||||
import 'package:nc_photos/type.dart';
|
||||
|
|
|
@ -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/exif.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/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart' as app;
|
||||
import 'package:nc_photos/ci_string.dart';
|
||||
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_converter.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 {
|
||||
await delete(servers).go();
|
||||
// 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;");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
|
||||
class CustomizableMaterialPageRoute extends MaterialPageRoute {
|
||||
CustomizableMaterialPageRoute({
|
||||
|
@ -18,4 +18,4 @@ class CustomizableMaterialPageRoute extends MaterialPageRoute {
|
|||
final Duration reverseTransitionDuration;
|
||||
}
|
||||
|
||||
String getImageHeroTag(File file) => "imageHero(${file.path})";
|
||||
String getImageHeroTag(FileDescriptor file) => "imageHero(${file.fdPath})";
|
||||
|
|
|
@ -14,4 +14,6 @@ String getRemoteLinkSharesDir(Account account) =>
|
|||
"${getRemoteStorageDir(account)}/link_shares";
|
||||
|
||||
String getRemoteStorageDir(Account account) =>
|
||||
"${api_util.getWebdavRootUrlRelative(account)}/.com.nkming.nc_photos";
|
||||
"${api_util.getWebdavRootUrlRelative(account)}/$remoteStorageDirRelativePath";
|
||||
|
||||
const remoteStorageDirRelativePath = ".com.nkming.nc_photos";
|
||||
|
|
|
@ -13,6 +13,7 @@ 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/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/event/event.dart';
|
||||
import 'package:nc_photos/event/native_event.dart';
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:nc_photos/app_localizations.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_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/local_file.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/download_file.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/widget/processing_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
|
||||
class ShareHandler {
|
||||
ShareHandler({
|
||||
ShareHandler(
|
||||
this._c, {
|
||||
required this.context,
|
||||
this.clearSelection,
|
||||
});
|
||||
}) : assert(require(_c)),
|
||||
assert(InflateFileDescriptor.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
Future<void> shareLocalFiles(List<LocalFile> files) async {
|
||||
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 {
|
||||
final method = await _askShareMethod(files);
|
||||
if (method == null) {
|
||||
|
@ -336,6 +343,7 @@ class ShareHandler {
|
|||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
final BuildContext context;
|
||||
final VoidCallback? clearSelection;
|
||||
var isSelectionCleared = false;
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
|||
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/exception.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
|
|
|
@ -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/provider.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/override_comparator.dart';
|
||||
import 'package:nc_photos/use_case/create_share.dart';
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/create_dir.dart';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:nc_photos/account.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/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/platform/download.dart';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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/use_case/ls_single_file.dart';
|
||||
import 'package:nc_photos/use_case/move.dart';
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:nc_photos/use_case/list_potential_shared_album.dart';
|
||||
|
|
32
app/lib/use_case/inflate_file_descriptor.dart
Normal file
32
app/lib/use_case/inflate_file_descriptor.dart
Normal 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;
|
||||
}
|
|
@ -2,6 +2,7 @@ import 'package:drift/drift.dart' as sql;
|
|||
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/entity/sqlite_table_converter.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/location_util.dart' as location_util;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:logging/logging.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/pref.dart';
|
||||
import 'package:nc_photos/use_case/ls.dart';
|
||||
|
|
|
@ -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/provider.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/iterable_extension.dart';
|
||||
import 'package:nc_photos/stream_extension.dart';
|
||||
|
|
|
@ -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/provider.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/use_case/preprocess_album.dart';
|
||||
import 'package:nc_photos/use_case/unshare_file_from_album.dart';
|
||||
|
|
|
@ -3,15 +3,15 @@ import 'dart:convert';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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/type.dart';
|
||||
|
||||
class RequestPublicLink {
|
||||
/// 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 =
|
||||
await Api(account).ocs().dav().direct().post(fileId: file.fileId!);
|
||||
await Api(account).ocs().dav().direct().post(fileId: file.fdId);
|
||||
if (!response.isGood) {
|
||||
_log.severe("[call] Failed requesting server: $response");
|
||||
throw ApiException(
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:kiwi/kiwi.dart';
|
|||
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/event/event.dart';
|
||||
import 'package:nc_photos/use_case/move.dart';
|
||||
|
||||
|
|
|
@ -2,16 +2,18 @@ import 'package:drift/drift.dart' as sql;
|
|||
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/entity/sqlite_table_converter.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;
|
||||
|
||||
class ScanDirOffline {
|
||||
ScanDirOffline(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb);
|
||||
|
||||
Future<List<File>> call(
|
||||
Future<List<FileDescriptor>> call(
|
||||
Account account,
|
||||
File root, {
|
||||
bool isOnlySupportedFormat = true,
|
||||
|
@ -23,36 +25,57 @@ class ScanDirOffline {
|
|||
}, (db, Map args) async {
|
||||
final Account account = args["account"];
|
||||
final File root = args["root"];
|
||||
final strippedPath = root.strippedPathWithEmpty;
|
||||
final bool isOnlySupportedFormat = args["isOnlySupportedFormat"];
|
||||
final dbFiles = await db.useInIsolate((db) async {
|
||||
final query = db.queryFiles().run((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);
|
||||
root.strippedPathWithEmpty.run((p) {
|
||||
if (p.isNotEmpty) {
|
||||
q.byOrRelativePathPattern("$p/%");
|
||||
}
|
||||
});
|
||||
if (isOnlySupportedFormat) {
|
||||
q
|
||||
..byMimePattern("image/%")
|
||||
..byMimePattern("video/%");
|
||||
if (strippedPath.isNotEmpty) {
|
||||
q.byOrRelativePathPattern("$strippedPath/%");
|
||||
}
|
||||
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
|
||||
.map((r) => sql.CompleteFile(
|
||||
r.readTable(db.files),
|
||||
r.readTable(db.accountFiles),
|
||||
r.readTableOrNull(db.images),
|
||||
r.readTableOrNull(db.imageLocations),
|
||||
r.readTableOrNull(db.trashes),
|
||||
))
|
||||
.map((r) => <String, dynamic>{
|
||||
"relativePath": r.read(db.accountFiles.relativePath)!,
|
||||
"fileId": r.read(db.files.fileId)!,
|
||||
"contentType": r.read(db.files.contentType)!,
|
||||
"isArchived": r.read(db.accountFiles.isArchived),
|
||||
"isFavorite": r.read(db.accountFiles.isFavorite),
|
||||
"bestDateTime": r.read(db.accountFiles.bestDateTime)!.toUtc(),
|
||||
})
|
||||
.get();
|
||||
});
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
@ -83,13 +106,11 @@ class ScanDirOfflineMini {
|
|||
}
|
||||
q.byOrRelativePathPattern("$path/%");
|
||||
}
|
||||
if (isOnlySupportedFormat) {
|
||||
q
|
||||
..byMimePattern("image/%")
|
||||
..byMimePattern("video/%");
|
||||
}
|
||||
return q.build();
|
||||
});
|
||||
if (isOnlySupportedFormat) {
|
||||
query.where(db.whereFileIsSupportedMime());
|
||||
}
|
||||
query
|
||||
..orderBy([sql.OrderingTerm.desc(db.accountFiles.bestDateTime)])
|
||||
..limit(limit);
|
||||
|
|
109
app/lib/use_case/sync_dir.dart
Normal file
109
app/lib/use_case/sync_dir.dart
Normal 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");
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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/use_case/move.dart';
|
||||
|
||||
|
|
|
@ -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/provider.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/stream_extension.dart';
|
||||
import 'package:nc_photos/use_case/list_album.dart';
|
||||
|
|
|
@ -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/item.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;
|
||||
|
||||
class UpdateAutoAlbumCover {
|
||||
|
|
|
@ -389,7 +389,8 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
|
||||
void _onDownloadPressed() {
|
||||
DownloadHandler().downloadFiles(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
DownloadHandler(c).downloadFiles(
|
||||
widget.account,
|
||||
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
|
||||
parentDir: _album!.name,
|
||||
|
@ -419,6 +420,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -431,6 +433,7 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
return;
|
||||
}
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -441,10 +444,11 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddPressed(BuildContext context) async {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -493,11 +497,12 @@ class _AlbumBrowserState extends State<AlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
|
|
|
@ -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/app_localizations.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/k.dart' as k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
|
|
@ -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/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/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:nc_photos/ci_string.dart';
|
|||
import 'package:nc_photos/di_container.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/share.dart';
|
||||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
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/snack_bar_manager.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/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
|
@ -230,7 +232,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
.unarchiveSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
final selectedFiles = selectedListItems
|
||||
final selection = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
|
@ -238,6 +240,8 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
clearSelectedItems();
|
||||
});
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles =
|
||||
await InflateFileDescriptor(c)(widget.account, selection);
|
||||
final failures = <File>[];
|
||||
for (final f in selectedFiles) {
|
||||
try {
|
||||
|
@ -265,7 +269,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
}
|
||||
}
|
||||
|
||||
void _transformItems(List<File> files) {
|
||||
void _transformItems(List<FileDescriptor> files) {
|
||||
_buildItemQueue.addJob(
|
||||
PhotoListItemBuilderArguments(
|
||||
widget.account,
|
||||
|
@ -293,7 +297,7 @@ class _ArchiveBrowserState extends State<ArchiveBrowser>
|
|||
|
||||
late final _bloc = ScanAccountDirBloc.of(widget.account);
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/entity/album.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/theme.dart';
|
||||
import 'package:nc_photos/widget/album_grid_item.dart';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:collection/collection.dart' show compareNatural;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.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_localizations.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/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
|
@ -32,7 +31,7 @@ class PhotoListItemBuilderArguments {
|
|||
});
|
||||
|
||||
final Account account;
|
||||
final List<File> files;
|
||||
final List<FileDescriptor> files;
|
||||
final bool isArchived;
|
||||
final PhotoListItemSorter? sorter;
|
||||
final PhotoListItemGrouper? grouper;
|
||||
|
@ -50,17 +49,17 @@ class PhotoListItemBuilderResult {
|
|||
this.smartAlbums = const [],
|
||||
});
|
||||
|
||||
final List<File> backingFiles;
|
||||
final List<FileDescriptor> backingFiles;
|
||||
final List<SelectableItem> listItems;
|
||||
final List<Album> smartAlbums;
|
||||
}
|
||||
|
||||
typedef PhotoListItemSorter = int Function(File, File);
|
||||
typedef PhotoListItemSorter = int Function(FileDescriptor, FileDescriptor);
|
||||
|
||||
abstract class PhotoListItemGrouper {
|
||||
const PhotoListItemGrouper();
|
||||
|
||||
SelectableItem? onFile(File file);
|
||||
SelectableItem? onFile(FileDescriptor file);
|
||||
}
|
||||
|
||||
class PhotoListFileDateGrouper implements PhotoListItemGrouper {
|
||||
|
@ -69,7 +68,7 @@ class PhotoListFileDateGrouper implements PhotoListItemGrouper {
|
|||
}) : helper = DateGroupHelper(isMonthOnly: isMonthOnly);
|
||||
|
||||
@override
|
||||
onFile(File file) => helper
|
||||
onFile(FileDescriptor file) => helper
|
||||
.onFile(file)
|
||||
?.run((date) => PhotoListDateItem(date: date, isMonthOnly: isMonthOnly));
|
||||
|
||||
|
@ -83,10 +82,10 @@ class PhotoListItemSmartAlbumConfig {
|
|||
final int memoriesDayRange;
|
||||
}
|
||||
|
||||
int photoListFileDateTimeSorter(File a, File b) =>
|
||||
compareFileDateTimeDescending(a, b);
|
||||
int photoListFileDateTimeSorter(FileDescriptor a, FileDescriptor b) =>
|
||||
compareFileDescriptorDateTimeDescending(a, b);
|
||||
|
||||
int photoListFilenameSorter(File a, File b) =>
|
||||
int photoListFilenameSorter(FileDescriptor a, FileDescriptor b) =>
|
||||
compareNatural(b.filename, a.filename);
|
||||
|
||||
PhotoListItemBuilderResult buildPhotoListItem(
|
||||
|
@ -112,7 +111,7 @@ class _PhotoListItemBuilder {
|
|||
required this.locale,
|
||||
});
|
||||
|
||||
PhotoListItemBuilderResult call(Account account, List<File> files) {
|
||||
PhotoListItemBuilderResult call(Account account, List<FileDescriptor> files) {
|
||||
final s = Stopwatch()..start();
|
||||
try {
|
||||
return _fromSortedItems(account, _sortItems(files));
|
||||
|
@ -121,17 +120,17 @@ class _PhotoListItemBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
List<File> _sortItems(List<File> files) {
|
||||
final filtered = files.where((f) => (f.isArchived ?? false) == isArchived);
|
||||
List<FileDescriptor> _sortItems(List<FileDescriptor> files) {
|
||||
final filtered = files.where((f) => f.fdIsArchived == isArchived);
|
||||
if (sorter == null) {
|
||||
return filtered.toList();
|
||||
} else {
|
||||
return filtered.stableSorted(sorter);
|
||||
return filtered.sorted(sorter!);
|
||||
}
|
||||
}
|
||||
|
||||
PhotoListItemBuilderResult _fromSortedItems(
|
||||
Account account, List<File> files) {
|
||||
Account account, List<FileDescriptor> files) {
|
||||
final today = DateTime.now();
|
||||
final memoryAlbumHelper = smartAlbumConfig != null
|
||||
? 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,
|
||||
width: k.photoThumbSize, height: k.photoThumbSize);
|
||||
if (file_util.isSupportedImageFormat(file)) {
|
||||
|
@ -176,8 +175,7 @@ class _PhotoListItemBuilder {
|
|||
shouldShowFavoriteBadge: shouldShowFavoriteBadge,
|
||||
);
|
||||
} else {
|
||||
_log.shout(
|
||||
"[_buildListItem] Unsupported file format: ${file.contentType}");
|
||||
_log.shout("[_buildListItem] Unsupported file format: ${file.fdMime}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/bloc/ls_dir.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/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
|
|
@ -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/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/event/event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
@ -388,7 +389,8 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onDownloadPressed() {
|
||||
DownloadHandler().downloadFiles(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
DownloadHandler(c).downloadFiles(
|
||||
widget.account,
|
||||
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
|
||||
parentDir: _album!.name,
|
||||
|
@ -411,11 +413,13 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -477,11 +481,12 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/app_init.dart' as app_init;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/bloc/scan_local_dir.dart';
|
||||
import 'package:nc_photos/compute_queue.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
@ -244,11 +246,13 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionSharePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListLocalFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
await ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
|
|
@ -7,16 +7,23 @@ import 'package:nc_photos/di_container.dart';
|
|||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/album/item.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/use_case/add_to_album.dart';
|
||||
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||||
import 'package:nc_photos/widget/album_picker.dart';
|
||||
|
||||
class AddSelectionToAlbumHandler {
|
||||
AddSelectionToAlbumHandler(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(InflateFileDescriptor.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
Future<void> call({
|
||||
required BuildContext context,
|
||||
required Account account,
|
||||
required List<File> selectedFiles,
|
||||
required List<FileDescriptor> selection,
|
||||
required VoidCallback clearSelection,
|
||||
}) async {
|
||||
try {
|
||||
|
@ -32,6 +39,8 @@ class AddSelectionToAlbumHandler {
|
|||
await NotifiedAction(
|
||||
() async {
|
||||
assert(value.provider is AlbumStaticProvider);
|
||||
final selectedFiles =
|
||||
await InflateFileDescriptor(_c)(account, selection);
|
||||
final selected = selectedFiles
|
||||
.map((f) => AlbumFileItem(
|
||||
addedBy: account.userId,
|
||||
|
@ -52,6 +61,8 @@ class AddSelectionToAlbumHandler {
|
|||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger(
|
||||
"widget.handler.add_selection_to_album_handler.AddSelectionToAlbumHandler");
|
||||
}
|
||||
|
|
|
@ -4,19 +4,24 @@ import 'package:nc_photos/app_localizations.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_descriptor.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';
|
||||
|
||||
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);
|
||||
|
||||
/// Archive [selectedFiles] and return the archived count
|
||||
Future<int> call({
|
||||
required Account account,
|
||||
required List<File> selectedFiles,
|
||||
}) {
|
||||
required List<FileDescriptor> selection,
|
||||
}) async {
|
||||
final selectedFiles = await InflateFileDescriptor(_c)(account, selection);
|
||||
return NotifiedListAction<File>(
|
||||
list: selectedFiles,
|
||||
action: (file) async {
|
||||
|
|
|
@ -5,22 +5,30 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/app_localizations.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_descriptor.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/navigation_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/widget/trashbin_browser.dart';
|
||||
|
||||
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
|
||||
Future<int> call({
|
||||
required Account account,
|
||||
required List<File> selectedFiles,
|
||||
required List<FileDescriptor> selection,
|
||||
bool shouldCleanupAlbum = true,
|
||||
bool isRemoveOpened = false,
|
||||
bool isMoveToTrash = false,
|
||||
}) async {
|
||||
final selectedFiles = await InflateFileDescriptor(_c)(account, selection);
|
||||
final String processingText, successText;
|
||||
final String Function(int) failureText;
|
||||
if (isRemoveOpened) {
|
||||
|
@ -81,6 +89,8 @@ class RemoveSelectionHandler {
|
|||
return selectedFiles.length - failureCount;
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.handler.remove_selection_handler.RemoveSelectionHandler");
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/album.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/event/native_event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
@ -359,7 +359,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
?.item
|
||||
.as<PhotoListFileItem>()
|
||||
?.file
|
||||
.bestDateTime;
|
||||
.fdDateTime;
|
||||
if (date != null) {
|
||||
final text = DateFormat(DateFormat.YEAR_ABBR_MONTH,
|
||||
Localizations.localeOf(context).languageCode)
|
||||
|
@ -424,11 +424,13 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -439,10 +441,11 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -457,17 +460,19 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -475,13 +480,14 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -489,9 +495,9 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await RemoveSelectionHandler()(
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
}
|
||||
|
@ -523,23 +529,34 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
_hasFiredMetadataTask.value = false;
|
||||
}
|
||||
|
||||
void _tryStartMetadataTask({
|
||||
Future<void> _tryStartMetadataTask({
|
||||
bool ignoreFired = false,
|
||||
}) {
|
||||
}) async {
|
||||
if (_bloc.state is ScanAccountDirBlocSuccess &&
|
||||
Pref().isEnableExifOr() &&
|
||||
(!_hasFiredMetadataTask.value || ignoreFired)) {
|
||||
try {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
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 (_web != null) {
|
||||
_web!.startMetadataTask(missingMetadataCount);
|
||||
} else {
|
||||
service.startService();
|
||||
unawaited(service.startService());
|
||||
}
|
||||
}
|
||||
|
||||
_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
|
||||
void _transformItems(
|
||||
List<File> files, {
|
||||
List<FileDescriptor> files, {
|
||||
bool isSorted = false,
|
||||
bool isPostSuccess = false,
|
||||
}) {
|
||||
|
@ -700,7 +717,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
late final _bloc = ScanAccountDirBloc.of(widget.account);
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
var _smartAlbums = <Album>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
@ -438,11 +439,13 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -453,10 +456,11 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -471,17 +475,19 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -489,13 +495,14 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -503,9 +510,9 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await RemoveSelectionHandler()(
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
}
|
||||
|
@ -583,7 +590,7 @@ class _HomeSearchState extends State<HomeSearch>
|
|||
late final _thumbSize =
|
||||
photo_list_util.getThumbSize(_thumbZoomLevel).toDouble();
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
|
||||
static final _log = Logger("widget.home_search._HomeSearchState");
|
||||
}
|
||||
|
|
|
@ -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/cache_manager_util.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/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
@ -27,7 +27,7 @@ class ImageEditorArguments {
|
|||
const ImageEditorArguments(this.account, this.file);
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
}
|
||||
|
||||
class ImageEditor extends StatefulWidget {
|
||||
|
@ -54,7 +54,7 @@ class ImageEditor extends StatefulWidget {
|
|||
createState() => _ImageEditorState();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
}
|
||||
|
||||
class _ImageEditorState extends State<ImageEditor> {
|
||||
|
@ -275,7 +275,7 @@ class _ImageEditorState extends State<ImageEditor> {
|
|||
Future<void> _onSavePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await ImageProcessor.filter(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
4096,
|
||||
3072,
|
||||
|
|
|
@ -11,7 +11,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api.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_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/help_utils.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
@ -34,7 +34,7 @@ class ImageEnhancerArguments {
|
|||
const ImageEnhancerArguments(this.account, this.file, this.isSaveToServer);
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
final bool isSaveToServer;
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,8 @@ class ImageEnhancer extends StatefulWidget {
|
|||
builder: (context) => ImageEnhancer.fromArgs(args),
|
||||
);
|
||||
|
||||
static bool isSupportedFormat(File file) =>
|
||||
file_util.isSupportedImageFormat(file) && file.contentType != "image/gif";
|
||||
static bool isSupportedFormat(FileDescriptor file) =>
|
||||
file_util.isSupportedImageFormat(file) && file.fdMime != "image/gif";
|
||||
|
||||
const ImageEnhancer({
|
||||
super.key,
|
||||
|
@ -67,7 +67,7 @@ class ImageEnhancer extends StatefulWidget {
|
|||
createState() => _ImageEnhancerState();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
final bool isSaveToServer;
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
switch (_selectedOption.algorithm) {
|
||||
case _Algorithm.zeroDce:
|
||||
await ImageProcessor.zeroDce(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
_c.pref.getEnhanceMaxWidthOr(),
|
||||
_c.pref.getEnhanceMaxHeightOr(),
|
||||
|
@ -195,7 +195,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
|
||||
case _Algorithm.deepLab3Portrait:
|
||||
await ImageProcessor.deepLab3Portrait(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
_c.pref.getEnhanceMaxWidthOr(),
|
||||
_c.pref.getEnhanceMaxHeightOr(),
|
||||
|
@ -209,7 +209,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
|
||||
case _Algorithm.esrgan:
|
||||
await ImageProcessor.esrgan(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
_c.pref.getEnhanceMaxWidthOr(),
|
||||
_c.pref.getEnhanceMaxHeightOr(),
|
||||
|
@ -222,7 +222,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
|
||||
case _Algorithm.arbitraryStyleTransfer:
|
||||
await ImageProcessor.arbitraryStyleTransfer(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
math.min(
|
||||
_c.pref.getEnhanceMaxWidthOr(), _isAtLeast5GbRam() ? 1600 : 1280),
|
||||
|
@ -239,7 +239,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
|
||||
case _Algorithm.deepLab3ColorPop:
|
||||
await ImageProcessor.deepLab3ColorPop(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
_c.pref.getEnhanceMaxWidthOr(),
|
||||
_c.pref.getEnhanceMaxHeightOr(),
|
||||
|
@ -253,7 +253,7 @@ class _ImageEnhancerState extends State<ImageEnhancer> {
|
|||
|
||||
case _Algorithm.neurOp:
|
||||
await ImageProcessor.neurOp(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
"${widget.account.url}/${widget.file.fdPath}",
|
||||
widget.file.filename,
|
||||
_c.pref.getEnhanceMaxWidthOr(),
|
||||
_c.pref.getEnhanceMaxHeightOr(),
|
||||
|
|
|
@ -6,9 +6,8 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/flutter_util.dart' as flutter_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
||||
import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
|
||||
|
@ -91,7 +90,7 @@ class RemoteImageViewer extends StatefulWidget {
|
|||
@override
|
||||
createState() => _RemoteImageViewerState();
|
||||
|
||||
static void preloadImage(Account account, app.File file) {
|
||||
static void preloadImage(Account account, FileDescriptor file) {
|
||||
LargeImageCacheManager.inst.getFileStream(
|
||||
_getImageUrl(account, file),
|
||||
headers: {
|
||||
|
@ -101,7 +100,7 @@ class RemoteImageViewer extends StatefulWidget {
|
|||
}
|
||||
|
||||
final Account account;
|
||||
final app.File file;
|
||||
final FileDescriptor file;
|
||||
final bool canZoom;
|
||||
final VoidCallback? onLoaded;
|
||||
final ValueChanged<double>? onHeightChanged;
|
||||
|
@ -116,8 +115,6 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
|
|||
onHeightChanged: widget.onHeightChanged,
|
||||
onZoomStarted: widget.onZoomStarted,
|
||||
onZoomEnded: widget.onZoomEnded,
|
||||
child: Hero(
|
||||
tag: flutter_util.getImageHeroTag(widget.file),
|
||||
child: mod.CachedNetworkImage(
|
||||
cacheManager: LargeImageCacheManager.inst,
|
||||
imageUrl: _getImageUrl(widget.account, widget.file),
|
||||
|
@ -136,7 +133,6 @@ class _RemoteImageViewerState extends State<RemoteImageViewer> {
|
|||
return child;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void _onItemLoaded() {
|
||||
|
@ -333,8 +329,8 @@ class _ImageViewerState extends State<_ImageViewer>
|
|||
static final _log = Logger("widget.image_viewer._ImageViewerState");
|
||||
}
|
||||
|
||||
String _getImageUrl(Account account, app.File file) {
|
||||
if (file.contentType == "image/gif") {
|
||||
String _getImageUrl(Account account, FileDescriptor file) {
|
||||
if (file.fdMime == "image/gif") {
|
||||
return api_util.getFileUrl(account, file);
|
||||
} else {
|
||||
return api_util.getFilePreviewUrl(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:kiwi/kiwi.dart';
|
||||
import 'package:logging/logging.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/local_file.dart';
|
||||
import 'package:nc_photos/share_handler.dart';
|
||||
|
@ -135,9 +137,10 @@ class _LocalFileViewerState extends State<LocalFileViewer> {
|
|||
}
|
||||
|
||||
Future<void> _onSharePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
_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) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
@ -351,11 +352,13 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -366,10 +369,11 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -384,17 +388,19 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -402,13 +408,14 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -416,16 +423,15 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await RemoveSelectionHandler()(
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
|
||||
if (_backingFiles.containsIf(ev.file, (a, b) => a.fileId == b.fileId) !=
|
||||
true) {
|
||||
if (_backingFiles.containsIf(ev.file, (a, b) => a.fdId == b.fdId) != true) {
|
||||
return;
|
||||
}
|
||||
_refreshThrottler.trigger(
|
||||
|
@ -474,7 +480,7 @@ class _PersonBrowserState extends State<PersonBrowser>
|
|||
late final DiContainer _c;
|
||||
|
||||
late final ListFaceFileBloc _bloc = ListFaceFileBloc(_c);
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
|
|
@ -7,8 +7,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/flutter_util.dart'as flutter_util;
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
||||
|
@ -33,24 +32,24 @@ abstract class PhotoListFileItem extends SelectableItem {
|
|||
other is PhotoListFileItem && file.compareServerIdentity(other.file);
|
||||
|
||||
@override
|
||||
get hashCode => file.path.hashCode;
|
||||
get hashCode => file.fdPath.hashCode;
|
||||
|
||||
@override
|
||||
toString() => "$runtimeType {"
|
||||
"fileIndex: $fileIndex, "
|
||||
"file: ${file.path}, "
|
||||
"file: ${file.fdPath}, "
|
||||
"shouldShowFavoriteBadge: $shouldShowFavoriteBadge, "
|
||||
"}";
|
||||
|
||||
final int fileIndex;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
final bool shouldShowFavoriteBadge;
|
||||
}
|
||||
|
||||
class PhotoListImageItem extends PhotoListFileItem {
|
||||
const PhotoListImageItem({
|
||||
required int fileIndex,
|
||||
required File file,
|
||||
required FileDescriptor file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
required bool shouldShowFavoriteBadge,
|
||||
|
@ -64,9 +63,8 @@ class PhotoListImageItem extends PhotoListFileItem {
|
|||
buildWidget(BuildContext context) => PhotoListImage(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isGif: file.contentType == "image/gif",
|
||||
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true,
|
||||
heroKey: flutter_util.getImageHeroTag(file),
|
||||
isGif: file.fdMime == "image/gif",
|
||||
isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
|
||||
);
|
||||
|
||||
final Account account;
|
||||
|
@ -76,7 +74,7 @@ class PhotoListImageItem extends PhotoListFileItem {
|
|||
class PhotoListVideoItem extends PhotoListFileItem {
|
||||
const PhotoListVideoItem({
|
||||
required int fileIndex,
|
||||
required File file,
|
||||
required FileDescriptor file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
required bool shouldShowFavoriteBadge,
|
||||
|
@ -90,7 +88,7 @@ class PhotoListVideoItem extends PhotoListFileItem {
|
|||
buildWidget(BuildContext context) => PhotoListVideo(
|
||||
account: account,
|
||||
previewUrl: previewUrl,
|
||||
isFavorite: shouldShowFavoriteBadge && file.isFavorite == true,
|
||||
isFavorite: shouldShowFavoriteBadge && file.fdIsFavorite == true,
|
||||
);
|
||||
|
||||
final Account account;
|
||||
|
|
|
@ -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/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 {
|
||||
DateGroupHelper({
|
||||
required this.isMonthOnly,
|
||||
});
|
||||
|
||||
DateTime? onFile(File file) {
|
||||
final newDate = file.bestDateTime.toLocal();
|
||||
DateTime? onFile(FileDescriptor file) {
|
||||
final newDate = file.fdDateTime.toLocal();
|
||||
if (newDate.year != _currentDate?.year ||
|
||||
newDate.month != _currentDate?.month ||
|
||||
(!isMonthOnly && newDate.day != _currentDate?.day)) {
|
||||
|
@ -40,8 +40,8 @@ class MemoryAlbumHelper {
|
|||
}) : today = (today?.toLocal() ?? DateTime.now()).toMidnight(),
|
||||
dayRange = math.max(dayRange, 0);
|
||||
|
||||
void addFile(File f) {
|
||||
final date = f.bestDateTime.toLocal().toMidnight();
|
||||
void addFile(FileDescriptor f) {
|
||||
final date = f.fdDateTime.toLocal().toMidnight();
|
||||
final diff = today.difference(date).inDays;
|
||||
if (diff < 300) {
|
||||
return;
|
||||
|
@ -49,8 +49,7 @@ class MemoryAlbumHelper {
|
|||
for (final dy in [0, -1, 1]) {
|
||||
if (today.copyWith(year: date.year + dy).difference(date).abs().inDays <=
|
||||
dayRange) {
|
||||
_log.fine(
|
||||
"[addFile] Add file (${f.bestDateTime}) to ${date.year + dy}");
|
||||
_log.fine("[addFile] Add file (${f.fdDateTime}) to ${date.year + dy}");
|
||||
_addFileToYear(f, date.year + dy);
|
||||
break;
|
||||
}
|
||||
|
@ -69,13 +68,13 @@ class MemoryAlbumHelper {
|
|||
provider: AlbumMemoryProvider(
|
||||
year: e.key, month: today.month, day: today.day),
|
||||
coverProvider:
|
||||
AlbumManualCoverProvider(coverFile: e.value.coverFile),
|
||||
AlbumMemoryCoverProvider(coverFile: e.value.coverFile),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _addFileToYear(File f, int year) {
|
||||
void _addFileToYear(FileDescriptor f, int year) {
|
||||
final item = _data[year];
|
||||
final date = today.copyWith(year: year);
|
||||
if (item == null) {
|
||||
|
@ -117,10 +116,10 @@ class _MemoryAlbumHelperItem {
|
|||
_MemoryAlbumHelperItem(this.date, this.coverFile)
|
||||
: coverDiff = getCoverDiff(date, coverFile);
|
||||
|
||||
static Duration getCoverDiff(DateTime date, File f) =>
|
||||
f.bestDateTime.difference(date.copyWith(hour: 12)).abs();
|
||||
static Duration getCoverDiff(DateTime date, FileDescriptor f) =>
|
||||
f.fdDateTime.difference(date.copyWith(hour: 12)).abs();
|
||||
|
||||
final DateTime date;
|
||||
File coverFile;
|
||||
FileDescriptor coverFile;
|
||||
Duration coverDiff;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
|
@ -295,11 +296,13 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -310,10 +313,11 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -328,17 +332,19 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -346,13 +352,14 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -360,9 +367,9 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await RemoveSelectionHandler()(
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
}
|
||||
|
@ -408,7 +415,7 @@ class _PlaceBrowserState extends State<PlaceBrowser>
|
|||
late final DiContainer _c;
|
||||
|
||||
late final ListLocationFileBloc _bloc = ListLocationFileBloc(_c);
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:nc_photos/account.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_descriptor.dart';
|
||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:nc_photos/account.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/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/cache_manager_util.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/share.dart';
|
||||
import 'package:nc_photos/entity/share/data_source.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.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/k.dart' as k;
|
||||
import 'package:nc_photos/theme.dart';
|
||||
|
@ -26,7 +26,7 @@ class SlideshowViewerArguments {
|
|||
);
|
||||
|
||||
final Account account;
|
||||
final List<File> streamFiles;
|
||||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
final SlideshowConfig config;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class SlideshowViewer extends StatefulWidget {
|
|||
createState() => _SlideshowViewerState();
|
||||
|
||||
final Account account;
|
||||
final List<File> streamFiles;
|
||||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
final SlideshowConfig config;
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ class _SlideshowViewerState extends State<SlideshowViewer>
|
|||
} else if (file_util.isSupportedVideoFormat(file)) {
|
||||
return _buildVideoView(context, index);
|
||||
} else {
|
||||
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
||||
_log.shout("[_buildItemView] Unknown file format: ${file.fdMime}");
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,7 +216,8 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onDownloadPressed() {
|
||||
DownloadHandler().downloadFiles(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
DownloadHandler(c).downloadFiles(
|
||||
widget.account,
|
||||
_sortedItems.whereType<AlbumFileItem>().map((e) => e.file).toList(),
|
||||
parentDir: _album!.name,
|
||||
|
@ -236,11 +237,13 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -251,10 +254,11 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddPressed(BuildContext context) async {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -269,11 +273,12 @@ class _SmartAlbumBrowserState extends State<SmartAlbumBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<_FileListItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
|
@ -304,11 +305,13 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
clearSelection: () {
|
||||
setState(() {
|
||||
|
@ -319,10 +322,11 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
}
|
||||
|
||||
Future<void> _onSelectionAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: selectedListItems
|
||||
selection: selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList(),
|
||||
|
@ -337,17 +341,19 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
}
|
||||
|
||||
void _onSelectionDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selected = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
DownloadHandler().downloadFiles(widget.account, selected);
|
||||
DownloadHandler(c).downloadFiles(widget.account, selected);
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSelectionArchivePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -355,13 +361,14 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSelectionDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
|
@ -369,16 +376,15 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
await RemoveSelectionHandler()(
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: selectedFiles,
|
||||
selection: selectedFiles,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFilePropertyUpdated(FilePropertyUpdatedEvent ev) {
|
||||
if (_backingFiles.containsIf(ev.file, (a, b) => a.fileId == b.fileId) !=
|
||||
true) {
|
||||
if (_backingFiles.containsIf(ev.file, (a, b) => a.fdId == b.fdId) != true) {
|
||||
return;
|
||||
}
|
||||
_refreshThrottler.trigger(
|
||||
|
@ -427,7 +433,7 @@ class _TagBrowserState extends State<TagBrowser>
|
|||
late final DiContainer _c;
|
||||
|
||||
late final ListTagFileBloc _bloc = ListTagFileBloc(_c);
|
||||
var _backingFiles = <File>[];
|
||||
var _backingFiles = <FileDescriptor>[];
|
||||
|
||||
final _buildItemQueue =
|
||||
ComputeQueue<PhotoListItemBuilderArguments, PhotoListItemBuilderResult>();
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:nc_photos/compute_queue.dart';
|
|||
import 'package:nc_photos/debug_util.dart';
|
||||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
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/snack_bar_manager.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/widget/builder/photo_list_item_builder.dart';
|
||||
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||
|
@ -294,18 +296,20 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
.restoreSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
final selectedFiles = selectedListItems
|
||||
final selection = selectedListItems
|
||||
.whereType<PhotoListFileItem>()
|
||||
.map((e) => e.file)
|
||||
.toList();
|
||||
setState(() {
|
||||
clearSelectedItems();
|
||||
});
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final selectedFiles =
|
||||
await InflateFileDescriptor(c)(widget.account, selection);
|
||||
final failures = <File>[];
|
||||
for (final f in selectedFiles) {
|
||||
try {
|
||||
await RestoreTrashbin(KiwiContainer().resolve<DiContainer>())(
|
||||
widget.account, f);
|
||||
await RestoreTrashbin(c)(widget.account, f);
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_onSelectionAppBarRestorePressed] Failed while restoring file: ${logFilename(f.path)}",
|
||||
|
@ -363,7 +367,7 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
(result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_backingFiles = result.backingFiles;
|
||||
_backingFiles = result.backingFiles.cast();
|
||||
itemStreamListItems = result.listItems;
|
||||
});
|
||||
}
|
||||
|
@ -382,10 +386,11 @@ class _TrashbinBrowserState extends State<TrashbinBrowser>
|
|||
return _deleteFiles(selectedFiles);
|
||||
}
|
||||
|
||||
Future<void> _deleteFiles(List<File> files) async {
|
||||
await RemoveSelectionHandler()(
|
||||
Future<void> _deleteFiles(List<FileDescriptor> files) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: files,
|
||||
selection: files,
|
||||
shouldCleanupAlbum: false,
|
||||
);
|
||||
}
|
||||
|
@ -415,7 +420,9 @@ enum _SelectionAppBarMenuOption {
|
|||
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) {
|
||||
// ?
|
||||
return 0;
|
||||
|
|
|
@ -311,11 +311,12 @@ class _TrashbinViewerState extends State<TrashbinViewer> {
|
|||
}
|
||||
|
||||
Future<void> _delete(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
_log.info("[_delete] Removing file: ${file.path}");
|
||||
final count = await RemoveSelectionHandler()(
|
||||
final count = await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: [file],
|
||||
selection: [file],
|
||||
shouldCleanupAlbum: false,
|
||||
isRemoveOpened: true,
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
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/k.dart' as k;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
|
@ -33,7 +33,7 @@ class VideoViewer extends StatefulWidget {
|
|||
createState() => _VideoViewerState();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor file;
|
||||
final VoidCallback? onLoaded;
|
||||
final VoidCallback? onLoadFailure;
|
||||
final ValueChanged<double>? onHeightChanged;
|
||||
|
|
|
@ -12,7 +12,7 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/download_handler.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/flutter_util.dart';
|
||||
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/share_handler.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/widget/animated_visibility.dart';
|
||||
import 'package:nc_photos/widget/disposable.dart';
|
||||
|
@ -45,7 +46,7 @@ class ViewerArguments {
|
|||
});
|
||||
|
||||
final Account account;
|
||||
final List<File> streamFiles;
|
||||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
final Album? album;
|
||||
}
|
||||
|
@ -81,7 +82,7 @@ class Viewer extends StatefulWidget {
|
|||
createState() => _ViewerState();
|
||||
|
||||
final Account account;
|
||||
final List<File> streamFiles;
|
||||
final List<FileDescriptor> streamFiles;
|
||||
final int startIndex;
|
||||
|
||||
/// The album these files belongs to, or null
|
||||
|
@ -166,7 +167,8 @@ class _ViewerState extends State<Viewer>
|
|||
foregroundColor: Colors.white.withOpacity(.87),
|
||||
actions: [
|
||||
if (!_isDetailPaneActive && _canOpenDetailPane()) ...[
|
||||
(_pageStates[index]?.favoriteOverride ?? file.isFavorite) ==
|
||||
(_pageStates[index]?.favoriteOverride ??
|
||||
file.fdIsFavorite) ==
|
||||
true
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.star),
|
||||
|
@ -313,7 +315,7 @@ class _ViewerState extends State<Viewer>
|
|||
visible: _isShowDetailPane,
|
||||
child: ViewerDetailPane(
|
||||
account: widget.account,
|
||||
file: widget.streamFiles[index],
|
||||
fd: widget.streamFiles[index],
|
||||
album: widget.album,
|
||||
onSlideshowPressed: _onSlideshowPressed,
|
||||
),
|
||||
|
@ -336,7 +338,7 @@ class _ViewerState extends State<Viewer>
|
|||
} else if (file_util.isSupportedVideoFormat(file)) {
|
||||
return _buildVideoView(context, index);
|
||||
} else {
|
||||
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
||||
_log.shout("[_buildItemView] Unknown file format: ${file.fdMime}");
|
||||
_pageStates[index]!.itemHeight = 0;
|
||||
return Container();
|
||||
}
|
||||
|
@ -483,11 +485,17 @@ class _ViewerState extends State<Viewer>
|
|||
_pageStates[index] = _PageState(ScrollController(
|
||||
initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane
|
||||
? _calcDetailPaneOpenedScrollPosition(index)
|
||||
: 0));
|
||||
: 0,
|
||||
));
|
||||
}
|
||||
|
||||
/// Called when the page is being built after previously moved out of view
|
||||
void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
|
||||
_pageStates[index]!.setScrollController(ScrollController(
|
||||
initialScrollOffset: _isShowDetailPane && !_isClosingDetailPane
|
||||
? _calcDetailPaneOpenedScrollPosition(index)
|
||||
: 0,
|
||||
));
|
||||
if (_isShowDetailPane && !_isClosingDetailPane) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pageStates[index]!.itemHeight != null) {
|
||||
|
@ -509,8 +517,9 @@ class _ViewerState extends State<Viewer>
|
|||
return;
|
||||
}
|
||||
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
final fd = widget.streamFiles[_viewerController.currentPage];
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first;
|
||||
setState(() {
|
||||
_pageStates[index]!.favoriteOverride = true;
|
||||
});
|
||||
|
@ -542,8 +551,9 @@ class _ViewerState extends State<Viewer>
|
|||
return;
|
||||
}
|
||||
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
final fd = widget.streamFiles[_viewerController.currentPage];
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = (await InflateFileDescriptor(c)(widget.account, [fd])).first;
|
||||
setState(() {
|
||||
_pageStates[index]!.favoriteOverride = false;
|
||||
});
|
||||
|
@ -578,8 +588,10 @@ class _ViewerState extends State<Viewer>
|
|||
}
|
||||
|
||||
void _onSharePressed(BuildContext context) {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
ShareHandler(
|
||||
c,
|
||||
context: context,
|
||||
).shareFiles(widget.account, [file]);
|
||||
}
|
||||
|
@ -591,7 +603,7 @@ class _ViewerState extends State<Viewer>
|
|||
return;
|
||||
}
|
||||
|
||||
_log.info("[_onEditPressed] Edit file: ${file.path}");
|
||||
_log.info("[_onEditPressed] Edit file: ${file.fdPath}");
|
||||
Navigator.of(context).pushNamed(ImageEditor.routeName,
|
||||
arguments: ImageEditorArguments(widget.account, file));
|
||||
}
|
||||
|
@ -604,24 +616,26 @@ class _ViewerState extends State<Viewer>
|
|||
}
|
||||
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,
|
||||
arguments: ImageEnhancerArguments(
|
||||
widget.account, file, c.pref.isSaveEditResultToServerOr()));
|
||||
}
|
||||
|
||||
void _onDownloadPressed() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
||||
DownloadHandler().downloadFiles(widget.account, [file]);
|
||||
_log.info("[_onDownloadPressed] Downloading file: ${file.fdPath}");
|
||||
DownloadHandler(c).downloadFiles(widget.account, [file]);
|
||||
}
|
||||
|
||||
Future<void> _onDeletePressed(BuildContext context) async {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
_log.info("[_onDeletePressed] Removing file: ${file.path}");
|
||||
final count = await RemoveSelectionHandler()(
|
||||
_log.info("[_onDeletePressed] Removing file: ${file.fdPath}");
|
||||
final count = await RemoveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: [file],
|
||||
selection: [file],
|
||||
isRemoveOpened: true,
|
||||
isMoveToTrash: true,
|
||||
);
|
||||
|
@ -754,6 +768,10 @@ class _ViewerState extends State<Viewer>
|
|||
class _PageState {
|
||||
_PageState(this.scrollController);
|
||||
|
||||
void setScrollController(ScrollController c) {
|
||||
scrollController = c;
|
||||
}
|
||||
|
||||
ScrollController scrollController;
|
||||
double? itemHeight;
|
||||
bool hasLoaded = false;
|
||||
|
|
|
@ -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/exif_extension.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/location_util.dart' as location_util;
|
||||
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/snack_bar_manager.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/remove_from_album.dart';
|
||||
import 'package:nc_photos/use_case/update_album.dart';
|
||||
|
@ -42,7 +44,7 @@ class ViewerDetailPane extends StatefulWidget {
|
|||
const ViewerDetailPane({
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.file,
|
||||
required this.fd,
|
||||
this.album,
|
||||
this.onSlideshowPressed,
|
||||
}) : super(key: key);
|
||||
|
@ -51,7 +53,7 @@ class ViewerDetailPane extends StatefulWidget {
|
|||
createState() => _ViewerDetailPaneState();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
final FileDescriptor fd;
|
||||
|
||||
/// The album this file belongs to, or null
|
||||
final Album? album;
|
||||
|
@ -63,6 +65,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_ViewerDetailPaneState() {
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
assert(require(c));
|
||||
assert(InflateFileDescriptor.require(c));
|
||||
_c = c;
|
||||
}
|
||||
|
||||
|
@ -72,27 +75,46 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
|
||||
@override
|
||||
initState() {
|
||||
_log.info("[initState] File: ${widget.fd.fdPath}");
|
||||
super.initState();
|
||||
_dateTime = widget.fd.fdDateTime.toLocal();
|
||||
_initFile();
|
||||
}
|
||||
|
||||
_dateTime = widget.file.bestDateTime.toLocal();
|
||||
if (widget.file.metadata == null) {
|
||||
Future<void> _initFile() async {
|
||||
_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");
|
||||
} else {
|
||||
_log.info("[initState] Metadata exists in File");
|
||||
if (widget.file.metadata!.exif != null) {
|
||||
if (_file!.metadata!.exif != null) {
|
||||
_initMetadata();
|
||||
}
|
||||
}
|
||||
_initTags();
|
||||
await _initTags();
|
||||
// update tages
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// postpone loading map to improve responsiveness
|
||||
Future.delayed(const Duration(milliseconds: 750)).then((_) {
|
||||
unawaited(Future.delayed(const Duration(milliseconds: 750)).then((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_shouldBlockGpsMap = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -104,43 +126,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
Localizations.localeOf(context).languageCode)
|
||||
.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(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_file != null) ...[
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
|
@ -164,7 +155,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
label: L10n.global().addToAlbumTooltip,
|
||||
onPressed: () => _onAddToAlbumPressed(context),
|
||||
),
|
||||
if (widget.file.isArchived == true)
|
||||
if (widget.fd.fdIsArchived == true)
|
||||
_DetailPaneButton(
|
||||
icon: Icons.unarchive_outlined,
|
||||
label: L10n.global().unarchiveTooltip,
|
||||
|
@ -188,6 +179,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Divider(),
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
leading: ListTileCenterLeading(
|
||||
child: Icon(
|
||||
|
@ -195,10 +187,11 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
title: Text(path_lib.basenameWithoutExtension(widget.file.path)),
|
||||
subtitle: Text(widget.file.strippedPath),
|
||||
title: Text(path_lib.basenameWithoutExtension(widget.fd.fdPath)),
|
||||
subtitle: Text(widget.fd.strippedPath),
|
||||
),
|
||||
if (!widget.file.isOwned(widget.account.userId))
|
||||
if (_file != null) ...[
|
||||
if (!_file!.isOwned(widget.account.userId))
|
||||
ListTile(
|
||||
leading: ListTileCenterLeading(
|
||||
child: Icon(
|
||||
|
@ -206,8 +199,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
),
|
||||
title: Text(widget.file.ownerDisplayName ??
|
||||
widget.file.ownerId!.toString()),
|
||||
title:
|
||||
Text(_file!.ownerDisplayName ?? _file!.ownerId!.toString()),
|
||||
subtitle: Text(L10n.global().fileSharedByDescription),
|
||||
),
|
||||
if (_tags.isNotEmpty)
|
||||
|
@ -237,8 +230,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_tags[index],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppTheme.getPrimaryTextColorInverse(context),
|
||||
color: AppTheme.getPrimaryTextColorInverse(
|
||||
context),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -250,20 +243,24 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text("$dateStr $timeStr"),
|
||||
trailing: Icon(
|
||||
trailing: _file == null
|
||||
? null
|
||||
: Icon(
|
||||
Icons.edit_outlined,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
onTap: () => _onDateTimeTap(context),
|
||||
onTap: _file == null ? null : () => _onDateTimeTap(context),
|
||||
),
|
||||
if (widget.file.metadata?.imageWidth != null &&
|
||||
widget.file.metadata?.imageHeight != null)
|
||||
if (_file != null) ...[
|
||||
if (_file!.metadata?.imageWidth != null &&
|
||||
_file!.metadata?.imageHeight != null)
|
||||
ListTile(
|
||||
leading: ListTileCenterLeading(
|
||||
child: Icon(
|
||||
|
@ -272,8 +269,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
),
|
||||
),
|
||||
title: Text(
|
||||
"${widget.file.metadata!.imageWidth} x ${widget.file.metadata!.imageHeight}"),
|
||||
subtitle: Text(sizeSubStr),
|
||||
"${_file!.metadata!.imageWidth} x ${_file!.metadata!.imageHeight}"),
|
||||
subtitle: Text(_buildSizeSubtitle()),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
|
@ -281,7 +278,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
Icons.aspect_ratio,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text(_byteSizeToString(widget.file.contentLength ?? 0)),
|
||||
title: Text(_byteSizeToString(_file!.contentLength ?? 0)),
|
||||
),
|
||||
if (_model != null)
|
||||
ListTile(
|
||||
|
@ -292,7 +289,8 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
),
|
||||
),
|
||||
title: Text(_model!),
|
||||
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
||||
subtitle: _buildCameraSubtitle()
|
||||
.run((s) => s.isNotEmpty ? Text(s) : null),
|
||||
),
|
||||
if (_location?.name != null)
|
||||
ListTile(
|
||||
|
@ -330,13 +328,15 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert EXIF data to readable format
|
||||
void _initMetadata() {
|
||||
final exif = widget.file.metadata!.exif!;
|
||||
assert(_file != null);
|
||||
final exif = _file!.metadata!.exif!;
|
||||
_log.info("[_initMetadata] $exif");
|
||||
|
||||
if (exif.make != null && exif.model != null) {
|
||||
|
@ -363,20 +363,60 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
if (lat != null && lng != null) {
|
||||
_log.fine("GPS: ($lat, $lng)");
|
||||
_gps = Tuple2(lat, lng);
|
||||
_location = widget.file.location;
|
||||
_location = _file!.location;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initTags() async {
|
||||
assert(_file != null);
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
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));
|
||||
} catch (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 {
|
||||
assert(widget.album!.provider is AlbumStaticProvider);
|
||||
try {
|
||||
|
@ -385,7 +425,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
final thisItem = AlbumStaticProvider.of(widget.album!)
|
||||
.items
|
||||
.whereType<AlbumFileItem>()
|
||||
.firstWhere((element) => element.file.path == widget.file.path);
|
||||
.firstWhere((element) => element.file.path == widget.fd.fdPath);
|
||||
await RemoveFromAlbum(KiwiContainer().resolve<DiContainer>())(
|
||||
widget.account, widget.album!, [thisItem]);
|
||||
if (mounted) {
|
||||
|
@ -403,9 +443,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
|
||||
Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
|
||||
assert(_file != null);
|
||||
assert(widget.album != null);
|
||||
_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 {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
|
@ -413,7 +454,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
widget.account,
|
||||
widget.album!.copyWith(
|
||||
coverProvider: AlbumManualCoverProvider(
|
||||
coverFile: widget.file,
|
||||
coverFile: _file!,
|
||||
),
|
||||
));
|
||||
},
|
||||
|
@ -428,20 +469,23 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
|
||||
Future<void> _onAddToAlbumPressed(BuildContext context) {
|
||||
return AddSelectionToAlbumHandler()(
|
||||
assert(_file != null);
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
return AddSelectionToAlbumHandler(c)(
|
||||
context: context,
|
||||
account: widget.account,
|
||||
selectedFiles: [widget.file],
|
||||
selection: [_file!],
|
||||
clearSelection: () {},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onArchivePressed(BuildContext context) async {
|
||||
_log.info("[_onArchivePressed] Archive file: ${widget.file.path}");
|
||||
final count =
|
||||
await ArchiveSelectionHandler(KiwiContainer().resolve<DiContainer>())(
|
||||
assert(_file != null);
|
||||
_log.info("[_onArchivePressed] Archive file: ${widget.fd.fdPath}");
|
||||
final c = KiwiContainer().resolve<DiContainer>();
|
||||
final count = await ArchiveSelectionHandler(c)(
|
||||
account: widget.account,
|
||||
selectedFiles: [widget.file],
|
||||
selection: [_file!],
|
||||
);
|
||||
if (count == 1) {
|
||||
if (mounted) {
|
||||
|
@ -451,12 +495,13 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
|
||||
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 {
|
||||
await NotifiedAction(
|
||||
() async {
|
||||
await UpdateProperty(_c.fileRepo)
|
||||
.updateIsArchived(widget.account, widget.file, false);
|
||||
.updateIsArchived(widget.account, _file!, false);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
@ -467,7 +512,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
)();
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[_onUnarchivePressed] Failed while archiving file: ${logFilename(widget.file.path)}",
|
||||
"[_onUnarchivePressed] Failed while archiving file: ${logFilename(widget.fd.fdPath)}",
|
||||
e,
|
||||
stackTrace);
|
||||
}
|
||||
|
@ -484,6 +529,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
|
||||
void _onDateTimeTap(BuildContext context) {
|
||||
assert(_file != null);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => PhotoDateTimeEditDialog(initialDateTime: _dateTime),
|
||||
|
@ -493,7 +539,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
try {
|
||||
await UpdateProperty(_c.fileRepo)
|
||||
.updateOverrideDateTime(widget.account, widget.file, value);
|
||||
.updateOverrideDateTime(widget.account, _file!, value);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dateTime = value;
|
||||
|
@ -501,7 +547,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_onDateTimeTap] Failed while updateOverrideDateTime: ${logFilename(widget.file.path)}",
|
||||
"[_onDateTimeTap] Failed while updateOverrideDateTime: ${logFilename(widget.fd.fdPath)}",
|
||||
e,
|
||||
stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
|
@ -527,7 +573,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
.items
|
||||
.whereType<AlbumFileItem>()
|
||||
.firstWhere(
|
||||
(element) => element.file.compareServerIdentity(widget.file));
|
||||
(element) => element.file.compareServerIdentity(widget.fd));
|
||||
if (thisItem.addedBy == widget.account.userId) {
|
||||
return true;
|
||||
}
|
||||
|
@ -537,6 +583,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
|
||||
late final DiContainer _c;
|
||||
|
||||
File? _file;
|
||||
late DateTime _dateTime;
|
||||
// EXIF data
|
||||
String? _model;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:nc_photos/ci_string.dart';
|
||||
import 'package:nc_photos/entity/exif.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:test/test.dart';
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
|
|||
import 'package:nc_photos/entity/favorite.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/person.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
|
|
|
@ -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/sort_provider.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/sharee.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
|
@ -377,6 +378,15 @@ File buildJpegFile({
|
|||
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({
|
||||
required String id,
|
||||
DateTime? stime,
|
||||
|
|
113
app/test/use_case/inflate_file_descriptor_test.dart
Normal file
113
app/test/use_case/inflate_file_descriptor_test.dart
Normal 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>()),
|
||||
);
|
||||
}
|
|
@ -45,7 +45,7 @@ Future<void> _root() async {
|
|||
(await ScanDirOffline(c)(
|
||||
account, File(path: file_util.unstripPath(account, "."))))
|
||||
.toSet(),
|
||||
files.toSet(),
|
||||
files.map(util.fileToFileDescriptor).toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ Future<void> _subDir() async {
|
|||
(await ScanDirOffline(c)(
|
||||
account, File(path: file_util.unstripPath(account, "test"))))
|
||||
.toSet(),
|
||||
{files[1]},
|
||||
[files[1]].map(util.fileToFileDescriptor).toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ Future<void> _unsupportedFile() async {
|
|||
(await ScanDirOffline(c)(
|
||||
account, File(path: file_util.unstripPath(account, "."))))
|
||||
.toSet(),
|
||||
{files[0]},
|
||||
[files[0]].map(util.fileToFileDescriptor).toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -140,12 +140,12 @@ Future<void> _multiAccountRoot() async {
|
|||
(await ScanDirOffline(c)(
|
||||
account, File(path: file_util.unstripPath(account, "."))))
|
||||
.toSet(),
|
||||
files.toSet(),
|
||||
files.map(util.fileToFileDescriptor).toSet(),
|
||||
);
|
||||
expect(
|
||||
(await ScanDirOffline(c)(
|
||||
user1Account, File(path: file_util.unstripPath(user1Account, "."))))
|
||||
.toSet(),
|
||||
user1Files.toSet(),
|
||||
user1Files.map(util.fileToFileDescriptor).toSet(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ void _prevYear() {
|
|||
name: "2020",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -141,7 +141,7 @@ void _prevYear2DaysBefore() {
|
|||
name: "2020",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -184,7 +184,7 @@ void _prevYear2DaysAfter() {
|
|||
name: "2020",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2020, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -227,7 +227,7 @@ void _onFeb29AddFeb27() {
|
|||
name: "2019",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -270,7 +270,7 @@ void _onFeb29AddMar3() {
|
|||
name: "2019",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -313,7 +313,7 @@ void _onFeb29AddMar2LeapYear() {
|
|||
name: "2016",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2016, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -356,7 +356,7 @@ void _onJan1AddDec31PrevYear() {
|
|||
name: "2019",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -385,7 +385,7 @@ void _onDec31AddJan1() {
|
|||
name: "2019",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2019, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
@ -414,7 +414,7 @@ void _onMay15AddMay15Range0() {
|
|||
name: "2021",
|
||||
provider:
|
||||
AlbumMemoryProvider(year: 2021, month: today.month, day: today.day),
|
||||
coverProvider: AlbumManualCoverProvider(coverFile: file),
|
||||
coverProvider: AlbumMemoryCoverProvider(coverFile: file),
|
||||
sortProvider: const AlbumTimeSortProvider(isAscending: false),
|
||||
lastUpdated: DateTime(2021),
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue