Merge branch 'query-partial-file' into dev

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

View file

@ -2,7 +2,7 @@
import 'package:logging/logging.dart';
import 'package: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) =>

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/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';

View file

@ -8,17 +8,17 @@ import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/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) {

View 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 {

View file

@ -5,6 +5,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/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";
}

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/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;

View file

@ -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';

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

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

View file

@ -2,32 +2,34 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/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",

View file

@ -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';

View file

@ -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';

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/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;

View file

@ -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");
}

View file

@ -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})";

View file

@ -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";

View file

@ -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';

View file

@ -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;

View file

@ -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;

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

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

View file

@ -2,6 +2,7 @@ import 'package:drift/drift.dart' as sql;
import 'package:nc_photos/account.dart';
import 'package:nc_photos/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;

View file

@ -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';

View file

@ -9,6 +9,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/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';

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/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';

View file

@ -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(

View file

@ -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';

View file

@ -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);

View file

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

View file

@ -1,7 +1,7 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/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';

View file

@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/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';

View file

@ -2,7 +2,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/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 {

View file

@ -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();
});

View file

@ -4,6 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/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';

View file

@ -14,6 +14,7 @@ import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/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';

View file

@ -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';

View file

@ -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>();

View file

@ -8,7 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/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';

View file

@ -1,4 +1,4 @@
import 'package:collection/collection.dart' show compareNatural;
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package: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;
}
}

View file

@ -8,6 +8,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/ls_dir.dart';
import 'package:nc_photos/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;

View file

@ -15,6 +15,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/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();
});

View file

@ -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(() {

View file

@ -7,16 +7,23 @@ import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/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");
}

View file

@ -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 {

View file

@ -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");
}

View file

@ -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 =

View file

@ -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");
}

View file

@ -9,7 +9,7 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/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,

View file

@ -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(),

View file

@ -6,9 +6,8 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_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(

View file

@ -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) {

View file

@ -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>();

View file

@ -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;

View file

@ -7,15 +7,15 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/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;
}

View file

@ -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>();

View file

@ -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';

View file

@ -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;

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/cache_manager_util.dart';
import 'package:nc_photos/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;

View file

@ -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();
}
}

View file

@ -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();
});

View file

@ -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>();

View file

@ -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;

View file

@ -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,
);

View file

@ -4,7 +4,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_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;

View file

@ -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;

View file

@ -16,6 +16,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/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;

View file

@ -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';

View file

@ -10,6 +10,7 @@ import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/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';

View file

@ -12,6 +12,7 @@ import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/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,

View file

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

View file

@ -45,7 +45,7 @@ Future<void> _root() async {
(await ScanDirOffline(c)(
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(),
);
}

View file

@ -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),
),