From 7563fa2ad6398930c441853c820b7124976d2d04 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 14 Jul 2022 04:23:41 +0800 Subject: [PATCH] Show first 100 photos on startup This is to create an illusion that the app loads quickly when it's not ;) --- app/lib/bloc/scan_account_dir.dart | 21 ++++++++ app/lib/entity/file/data_source.dart | 15 +++--- app/lib/entity/sqlite_table.dart | 4 ++ app/lib/entity/sqlite_table.g.dart | 61 ++++++++++++++++++---- app/lib/entity/sqlite_table_converter.dart | 1 + app/lib/entity/sqlite_table_extension.dart | 14 +++-- app/lib/use_case/scan_dir_offline.dart | 51 ++++++++++++++++++ 7 files changed, 145 insertions(+), 22 deletions(-) diff --git a/app/lib/bloc/scan_account_dir.dart b/app/lib/bloc/scan_account_dir.dart index 23895837..eb22d991 100644 --- a/app/lib/bloc/scan_account_dir.dart +++ b/app/lib/bloc/scan_account_dir.dart @@ -186,6 +186,17 @@ class ScanAccountDirBloc final hasContent = state.files.isNotEmpty; final stopwatch = Stopwatch()..start(); + if (!hasContent) { + try { + emit(ScanAccountDirBlocLoading(await _queryOfflineMini(ev))); + } catch (e, stackTrace) { + _log.shout( + "[_onEventQuery] Failed while _queryOfflineMini", e, stackTrace); + } + _log.info( + "[_onEventQuery] Elapsed time (_queryOfflineMini): ${stopwatch.elapsedMilliseconds}ms"); + stopwatch.reset(); + } final cacheFiles = await _queryOffline(ev); _log.info( "[_onEventQuery] Elapsed time (_queryOffline): ${stopwatch.elapsedMilliseconds}ms"); @@ -322,6 +333,16 @@ class ScanAccountDirBloc ); } + /// Query a small amount of files to give an illusion of quick startup + Future> _queryOfflineMini(ScanAccountDirBlocQueryBase ev) async { + return await ScanDirOfflineMini(_c)( + account, + account.roots.map((r) => File(path: file_util.unstripPath(account, r))), + 100, + isOnlySupportedFormat: true, + ); + } + Future> _queryOffline(ScanAccountDirBlocQueryBase ev) async { final files = []; for (final r in account.roots) { diff --git a/app/lib/entity/file/data_source.dart b/app/lib/entity/file/data_source.dart index 74cb506d..9f9e1d23 100644 --- a/app/lib/entity/file/data_source.dart +++ b/app/lib/entity/file/data_source.dart @@ -308,15 +308,12 @@ class FileSqliteDbDataSource implements FileDataSource { Account account, int fromEpochMs, int toEpochMs) async { _log.info("[listByDate] [$fromEpochMs, $toEpochMs]"); final dbFiles = await _c.sqliteDb.use((db) async { - final dateTime = sql.coalesce([ - db.accountFiles.overrideDateTime, - db.images.dateTimeOriginal, - db.files.lastModified, - ]).secondsSinceEpoch; - final queryBuilder = db.queryFiles() - ..setQueryMode(sql.FilesQueryMode.completeFile) - ..setAppAccount(account); - final query = queryBuilder.build(); + final query = db.queryFiles().run((q) { + q.setQueryMode(sql.FilesQueryMode.completeFile); + q.setAppAccount(account); + return q.build(); + }); + final dateTime = db.accountFiles.bestDateTime.secondsSinceEpoch; query ..where(dateTime.isBetweenValues( fromEpochMs ~/ 1000, (toEpochMs ~/ 1000) - 1)) diff --git a/app/lib/entity/sqlite_table.dart b/app/lib/entity/sqlite_table.dart index 30d4eb3b..239699ab 100644 --- a/app/lib/entity/sqlite_table.dart +++ b/app/lib/entity/sqlite_table.dart @@ -58,6 +58,8 @@ class AccountFiles extends Table { BoolColumn get isArchived => boolean().nullable()(); DateTimeColumn get overrideDateTime => dateTime().map(const _DateTimeConverter()).nullable()(); + DateTimeColumn get bestDateTime => + dateTime().map(const _DateTimeConverter())(); @override get uniqueKeys => [ @@ -186,6 +188,8 @@ class SqliteDb extends _$SqliteDb { "CREATE INDEX account_files_file_index ON account_files(file);")); await m.createIndex(Index("account_files_relative_path_index", "CREATE INDEX account_files_relative_path_index ON account_files(relative_path);")); + await m.createIndex(Index("account_files_best_date_time_index", + "CREATE INDEX account_files_best_date_time_index ON account_files(best_date_time);")); await m.createIndex(Index("dir_files_dir_index", "CREATE INDEX dir_files_dir_index ON dir_files(dir);")); diff --git a/app/lib/entity/sqlite_table.g.dart b/app/lib/entity/sqlite_table.g.dart index fa4d81ea..2e8e3d6d 100644 --- a/app/lib/entity/sqlite_table.g.dart +++ b/app/lib/entity/sqlite_table.g.dart @@ -1000,6 +1000,7 @@ class AccountFile extends DataClass implements Insertable { final bool? isFavorite; final bool? isArchived; final DateTime? overrideDateTime; + final DateTime bestDateTime; AccountFile( {required this.rowId, required this.account, @@ -1007,7 +1008,8 @@ class AccountFile extends DataClass implements Insertable { required this.relativePath, this.isFavorite, this.isArchived, - this.overrideDateTime}); + this.overrideDateTime, + required this.bestDateTime}); factory AccountFile.fromData(Map data, {String? prefix}) { final effectivePrefix = prefix ?? ''; return AccountFile( @@ -1026,6 +1028,9 @@ class AccountFile extends DataClass implements Insertable { overrideDateTime: $AccountFilesTable.$converter0.mapToDart( const DateTimeType().mapFromDatabaseResponse( data['${effectivePrefix}override_date_time'])), + bestDateTime: $AccountFilesTable.$converter1.mapToDart( + const DateTimeType().mapFromDatabaseResponse( + data['${effectivePrefix}best_date_time']))!, ); } @override @@ -1046,6 +1051,11 @@ class AccountFile extends DataClass implements Insertable { map['override_date_time'] = Variable(converter.mapToSql(overrideDateTime)); } + { + final converter = $AccountFilesTable.$converter1; + map['best_date_time'] = + Variable(converter.mapToSql(bestDateTime)!); + } return map; } @@ -1064,6 +1074,7 @@ class AccountFile extends DataClass implements Insertable { overrideDateTime: overrideDateTime == null && nullToAbsent ? const Value.absent() : Value(overrideDateTime), + bestDateTime: Value(bestDateTime), ); } @@ -1079,6 +1090,7 @@ class AccountFile extends DataClass implements Insertable { isArchived: serializer.fromJson(json['isArchived']), overrideDateTime: serializer.fromJson(json['overrideDateTime']), + bestDateTime: serializer.fromJson(json['bestDateTime']), ); } @override @@ -1092,6 +1104,7 @@ class AccountFile extends DataClass implements Insertable { 'isFavorite': serializer.toJson(isFavorite), 'isArchived': serializer.toJson(isArchived), 'overrideDateTime': serializer.toJson(overrideDateTime), + 'bestDateTime': serializer.toJson(bestDateTime), }; } @@ -1102,7 +1115,8 @@ class AccountFile extends DataClass implements Insertable { String? relativePath, Value isFavorite = const Value.absent(), Value isArchived = const Value.absent(), - Value overrideDateTime = const Value.absent()}) => + Value overrideDateTime = const Value.absent(), + DateTime? bestDateTime}) => AccountFile( rowId: rowId ?? this.rowId, account: account ?? this.account, @@ -1113,6 +1127,7 @@ class AccountFile extends DataClass implements Insertable { overrideDateTime: overrideDateTime.present ? overrideDateTime.value : this.overrideDateTime, + bestDateTime: bestDateTime ?? this.bestDateTime, ); @override String toString() { @@ -1123,14 +1138,15 @@ class AccountFile extends DataClass implements Insertable { ..write('relativePath: $relativePath, ') ..write('isFavorite: $isFavorite, ') ..write('isArchived: $isArchived, ') - ..write('overrideDateTime: $overrideDateTime') + ..write('overrideDateTime: $overrideDateTime, ') + ..write('bestDateTime: $bestDateTime') ..write(')')) .toString(); } @override int get hashCode => Object.hash(rowId, account, file, relativePath, - isFavorite, isArchived, overrideDateTime); + isFavorite, isArchived, overrideDateTime, bestDateTime); @override bool operator ==(Object other) => identical(this, other) || @@ -1141,7 +1157,8 @@ class AccountFile extends DataClass implements Insertable { other.relativePath == this.relativePath && other.isFavorite == this.isFavorite && other.isArchived == this.isArchived && - other.overrideDateTime == this.overrideDateTime); + other.overrideDateTime == this.overrideDateTime && + other.bestDateTime == this.bestDateTime); } class AccountFilesCompanion extends UpdateCompanion { @@ -1152,6 +1169,7 @@ class AccountFilesCompanion extends UpdateCompanion { final Value isFavorite; final Value isArchived; final Value overrideDateTime; + final Value bestDateTime; const AccountFilesCompanion({ this.rowId = const Value.absent(), this.account = const Value.absent(), @@ -1160,6 +1178,7 @@ class AccountFilesCompanion extends UpdateCompanion { this.isFavorite = const Value.absent(), this.isArchived = const Value.absent(), this.overrideDateTime = const Value.absent(), + this.bestDateTime = const Value.absent(), }); AccountFilesCompanion.insert({ this.rowId = const Value.absent(), @@ -1169,9 +1188,11 @@ class AccountFilesCompanion extends UpdateCompanion { this.isFavorite = const Value.absent(), this.isArchived = const Value.absent(), this.overrideDateTime = const Value.absent(), + required DateTime bestDateTime, }) : account = Value(account), file = Value(file), - relativePath = Value(relativePath); + relativePath = Value(relativePath), + bestDateTime = Value(bestDateTime); static Insertable custom({ Expression? rowId, Expression? account, @@ -1180,6 +1201,7 @@ class AccountFilesCompanion extends UpdateCompanion { Expression? isFavorite, Expression? isArchived, Expression? overrideDateTime, + Expression? bestDateTime, }) { return RawValuesInsertable({ if (rowId != null) 'row_id': rowId, @@ -1189,6 +1211,7 @@ class AccountFilesCompanion extends UpdateCompanion { if (isFavorite != null) 'is_favorite': isFavorite, if (isArchived != null) 'is_archived': isArchived, if (overrideDateTime != null) 'override_date_time': overrideDateTime, + if (bestDateTime != null) 'best_date_time': bestDateTime, }); } @@ -1199,7 +1222,8 @@ class AccountFilesCompanion extends UpdateCompanion { Value? relativePath, Value? isFavorite, Value? isArchived, - Value? overrideDateTime}) { + Value? overrideDateTime, + Value? bestDateTime}) { return AccountFilesCompanion( rowId: rowId ?? this.rowId, account: account ?? this.account, @@ -1208,6 +1232,7 @@ class AccountFilesCompanion extends UpdateCompanion { isFavorite: isFavorite ?? this.isFavorite, isArchived: isArchived ?? this.isArchived, overrideDateTime: overrideDateTime ?? this.overrideDateTime, + bestDateTime: bestDateTime ?? this.bestDateTime, ); } @@ -1237,6 +1262,11 @@ class AccountFilesCompanion extends UpdateCompanion { map['override_date_time'] = Variable(converter.mapToSql(overrideDateTime.value)); } + if (bestDateTime.present) { + final converter = $AccountFilesTable.$converter1; + map['best_date_time'] = + Variable(converter.mapToSql(bestDateTime.value)!); + } return map; } @@ -1249,7 +1279,8 @@ class AccountFilesCompanion extends UpdateCompanion { ..write('relativePath: $relativePath, ') ..write('isFavorite: $isFavorite, ') ..write('isArchived: $isArchived, ') - ..write('overrideDateTime: $overrideDateTime') + ..write('overrideDateTime: $overrideDateTime, ') + ..write('bestDateTime: $bestDateTime') ..write(')')) .toString(); } @@ -1310,6 +1341,14 @@ class $AccountFilesTable extends AccountFiles 'override_date_time', aliasedName, true, type: const IntType(), requiredDuringInsert: false) .withConverter($AccountFilesTable.$converter0); + final VerificationMeta _bestDateTimeMeta = + const VerificationMeta('bestDateTime'); + @override + late final GeneratedColumnWithTypeConverter + bestDateTime = GeneratedColumn( + 'best_date_time', aliasedName, false, + type: const IntType(), requiredDuringInsert: true) + .withConverter($AccountFilesTable.$converter1); @override List get $columns => [ rowId, @@ -1318,7 +1357,8 @@ class $AccountFilesTable extends AccountFiles relativePath, isFavorite, isArchived, - overrideDateTime + overrideDateTime, + bestDateTime ]; @override String get aliasedName => _alias ?? 'account_files'; @@ -1366,6 +1406,7 @@ class $AccountFilesTable extends AccountFiles data['is_archived']!, _isArchivedMeta)); } context.handle(_overrideDateTimeMeta, const VerificationResult.success()); + context.handle(_bestDateTimeMeta, const VerificationResult.success()); return context; } @@ -1388,6 +1429,8 @@ class $AccountFilesTable extends AccountFiles static TypeConverter $converter0 = const _DateTimeConverter(); + static TypeConverter $converter1 = + const _DateTimeConverter(); } class Image extends DataClass implements Insertable { diff --git a/app/lib/entity/sqlite_table_converter.dart b/app/lib/entity/sqlite_table_converter.dart index f3ec1dd1..7f8ac9eb 100644 --- a/app/lib/entity/sqlite_table_converter.dart +++ b/app/lib/entity/sqlite_table_converter.dart @@ -122,6 +122,7 @@ class SqliteFileConverter { isFavorite: Value(file.isFavorite), isArchived: Value(file.isArchived), overrideDateTime: Value(file.overrideDateTime), + bestDateTime: Value(file.bestDateTime), ); final dbImage = file.metadata?.run((m) => sql.ImagesCompanion.insert( lastUpdated: m.lastUpdated, diff --git a/app/lib/entity/sqlite_table_extension.dart b/app/lib/entity/sqlite_table_extension.dart index dd179ba0..efb7ea08 100644 --- a/app/lib/entity/sqlite_table_extension.dart +++ b/app/lib/entity/sqlite_table_extension.dart @@ -466,7 +466,7 @@ class FilesQueryBuilder { } void byRelativePathPattern(String pattern) { - _byRelativePathPattern = pattern; + (_byRelativePathPatterns ??= []).add(pattern); } void byMimePattern(String pattern) { @@ -536,8 +536,14 @@ class FilesQueryBuilder { if (_byRelativePath != null) { query.where(db.accountFiles.relativePath.equals(_byRelativePath)); } - if (_byRelativePathPattern != null) { - query.where(db.accountFiles.relativePath.like(_byRelativePathPattern!)); + if (_byRelativePathPatterns?.isNotEmpty == true) { + final expression = _byRelativePathPatterns! + .sublist(1) + .fold>( + db.accountFiles.relativePath.like(_byRelativePathPatterns![0]), + (previousValue, element) => + previousValue | db.accountFiles.relativePath.like(element)); + query.where(expression); } if (_byMimePatterns?.isNotEmpty == true) { final expression = _byMimePatterns!.sublist(1).fold>( @@ -576,7 +582,7 @@ class FilesQueryBuilder { int? _byFileId; Iterable? _byFileIds; String? _byRelativePath; - String? _byRelativePathPattern; + List? _byRelativePathPatterns; List? _byMimePatterns; bool? _byFavorite; int? _byDirRowId; diff --git a/app/lib/use_case/scan_dir_offline.dart b/app/lib/use_case/scan_dir_offline.dart index 46c47cf1..e88b24ed 100644 --- a/app/lib/use_case/scan_dir_offline.dart +++ b/app/lib/use_case/scan_dir_offline.dart @@ -1,3 +1,4 @@ +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'; @@ -57,3 +58,53 @@ class ScanDirOffline { final DiContainer _c; } + +class ScanDirOfflineMini { + ScanDirOfflineMini(this._c) : assert(require(_c)); + + static bool require(DiContainer c) => DiContainer.has(c, DiType.sqliteDb); + + Future> call( + Account account, + Iterable roots, + int limit, { + bool isOnlySupportedFormat = true, + }) async { + final dbFiles = await _c.sqliteDb.use((db) async { + final query = db.queryFiles().run((q) { + q + ..setQueryMode(sql.FilesQueryMode.completeFile) + ..setAppAccount(account); + for (final r in roots) { + final path = r.strippedPathWithEmpty; + if (path.isEmpty) { + break; + } + q.byRelativePathPattern("$path/%"); + } + if (isOnlySupportedFormat) { + q + ..byMimePattern("image/%") + ..byMimePattern("video/%"); + } + return q.build(); + }); + query + ..orderBy([sql.OrderingTerm.desc(db.accountFiles.bestDateTime)]) + ..limit(limit); + return await query + .map((r) => sql.CompleteFile( + r.readTable(db.files), + r.readTable(db.accountFiles), + r.readTableOrNull(db.images), + r.readTableOrNull(db.trashes), + )) + .get(); + }); + return dbFiles + .map((f) => SqliteFileConverter.fromSql(account.userId.toString(), f)) + .toList(); + } + + final DiContainer _c; +}