mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-27 17:34:44 +01:00
Sync tags on startup
This commit is contained in:
parent
8b1ed216b0
commit
711516439e
12 changed files with 769 additions and 3 deletions
|
@ -178,6 +178,8 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
c.shareeRepo = ShareeRepo(ShareeRemoteDataSource());
|
||||
c.favoriteRepo = const FavoriteRepo(FavoriteRemoteDataSource());
|
||||
c.tagRepo = const TagRepo(TagRemoteDataSource());
|
||||
c.tagRepoRemote = const TagRepo(TagRemoteDataSource());
|
||||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
c.taggedFileRepo = const TaggedFileRepo(TaggedFileRemoteDataSource());
|
||||
|
||||
if (platform_k.isAndroid) {
|
||||
|
|
|
@ -24,6 +24,8 @@ enum DiType {
|
|||
shareeRepo,
|
||||
favoriteRepo,
|
||||
tagRepo,
|
||||
tagRepoRemote,
|
||||
tagRepoLocal,
|
||||
taggedFileRepo,
|
||||
localFileRepo,
|
||||
pref,
|
||||
|
@ -43,6 +45,8 @@ class DiContainer {
|
|||
ShareeRepo? shareeRepo,
|
||||
FavoriteRepo? favoriteRepo,
|
||||
TagRepo? tagRepo,
|
||||
TagRepo? tagRepoRemote,
|
||||
TagRepo? tagRepoLocal,
|
||||
TaggedFileRepo? taggedFileRepo,
|
||||
LocalFileRepo? localFileRepo,
|
||||
Pref? pref,
|
||||
|
@ -58,6 +62,8 @@ class DiContainer {
|
|||
_shareeRepo = shareeRepo,
|
||||
_favoriteRepo = favoriteRepo,
|
||||
_tagRepo = tagRepo,
|
||||
_tagRepoRemote = tagRepoRemote,
|
||||
_tagRepoLocal = tagRepoLocal,
|
||||
_taggedFileRepo = taggedFileRepo,
|
||||
_localFileRepo = localFileRepo,
|
||||
_pref = pref,
|
||||
|
@ -89,6 +95,10 @@ class DiContainer {
|
|||
return contianer._favoriteRepo != null;
|
||||
case DiType.tagRepo:
|
||||
return contianer._tagRepo != null;
|
||||
case DiType.tagRepoRemote:
|
||||
return contianer._tagRepoRemote != null;
|
||||
case DiType.tagRepoLocal:
|
||||
return contianer._tagRepoLocal != null;
|
||||
case DiType.taggedFileRepo:
|
||||
return contianer._taggedFileRepo != null;
|
||||
case DiType.localFileRepo:
|
||||
|
@ -142,6 +152,8 @@ class DiContainer {
|
|||
ShareeRepo get shareeRepo => _shareeRepo!;
|
||||
FavoriteRepo get favoriteRepo => _favoriteRepo!;
|
||||
TagRepo get tagRepo => _tagRepo!;
|
||||
TagRepo get tagRepoRemote => _tagRepoRemote!;
|
||||
TagRepo get tagRepoLocal => _tagRepoLocal!;
|
||||
TaggedFileRepo get taggedFileRepo => _taggedFileRepo!;
|
||||
LocalFileRepo get localFileRepo => _localFileRepo!;
|
||||
|
||||
|
@ -203,6 +215,16 @@ class DiContainer {
|
|||
_tagRepo = v;
|
||||
}
|
||||
|
||||
set tagRepoRemote(TagRepo v) {
|
||||
assert(_tagRepoRemote == null);
|
||||
_tagRepoRemote = v;
|
||||
}
|
||||
|
||||
set tagRepoLocal(TagRepo v) {
|
||||
assert(_tagRepoLocal == null);
|
||||
_tagRepoLocal = v;
|
||||
}
|
||||
|
||||
set taggedFileRepo(TaggedFileRepo v) {
|
||||
assert(_taggedFileRepo == null);
|
||||
_taggedFileRepo = v;
|
||||
|
@ -237,6 +259,8 @@ class DiContainer {
|
|||
ShareeRepo? _shareeRepo;
|
||||
FavoriteRepo? _favoriteRepo;
|
||||
TagRepo? _tagRepo;
|
||||
TagRepo? _tagRepoRemote;
|
||||
TagRepo? _tagRepoLocal;
|
||||
TaggedFileRepo? _taggedFileRepo;
|
||||
LocalFileRepo? _localFileRepo;
|
||||
|
||||
|
@ -250,4 +274,6 @@ extension DiContainerExtension on DiContainer {
|
|||
DiContainer withRemoteFileRepo() =>
|
||||
copyWith(fileRepo: OrNull(fileRepoRemote));
|
||||
DiContainer withLocalFileRepo() => copyWith(fileRepo: OrNull(fileRepoLocal));
|
||||
DiContainer withRemoteTagRepo() => copyWith(tagRepo: OrNull(tagRepoRemote));
|
||||
DiContainer withLocalTagRepo() => copyWith(tagRepo: OrNull(tagRepoLocal));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
|
||||
|
@ -147,6 +148,21 @@ class AlbumShares extends Table {
|
|||
get primaryKey => {album, userId};
|
||||
}
|
||||
|
||||
class Tags extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
IntColumn get server =>
|
||||
integer().references(Servers, #rowId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get tagId => integer()();
|
||||
TextColumn get displayName => text()();
|
||||
BoolColumn get userVisible => boolean().nullable()();
|
||||
BoolColumn get userAssignable => boolean().nullable()();
|
||||
|
||||
@override
|
||||
get uniqueKeys => [
|
||||
{server, tagId},
|
||||
];
|
||||
}
|
||||
|
||||
// remember to also update the truncate method after adding a new table
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
|
@ -159,6 +175,7 @@ class AlbumShares extends Table {
|
|||
DirFiles,
|
||||
Albums,
|
||||
AlbumShares,
|
||||
Tags,
|
||||
],
|
||||
)
|
||||
class SqliteDb extends _$SqliteDb {
|
||||
|
@ -169,7 +186,7 @@ class SqliteDb extends _$SqliteDb {
|
|||
SqliteDb.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
|
||||
@override
|
||||
get schemaVersion => 1;
|
||||
get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
get migration => MigrationStrategy(
|
||||
|
@ -198,6 +215,22 @@ class SqliteDb extends _$SqliteDb {
|
|||
|
||||
await m.createIndex(Index("album_shares_album_index",
|
||||
"CREATE INDEX album_shares_album_index ON album_shares(album);"));
|
||||
|
||||
await _createIndexV2(m);
|
||||
},
|
||||
onUpgrade: (m, from, to) async {
|
||||
_log.info("[onUpgrade] $from -> $to");
|
||||
try {
|
||||
await transaction(() async {
|
||||
if (from < 2) {
|
||||
await m.createTable(tags);
|
||||
await _createIndexV2(m);
|
||||
}
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[onUpgrade] Failed upgrading sqlite db", e, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
await customStatement("PRAGMA foreign_keys = ON;");
|
||||
|
@ -208,6 +241,13 @@ class SqliteDb extends _$SqliteDb {
|
|||
await customStatement("PRAGMA busy_timeout = 5000;");
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> _createIndexV2(Migrator m) async {
|
||||
await m.createIndex(Index("tags_server_index",
|
||||
"CREATE INDEX tags_server_index ON tags(server);"));
|
||||
}
|
||||
|
||||
static final _log = Logger("entity.sqlite_table.SqliteDb");
|
||||
}
|
||||
|
||||
class _DateTimeConverter extends TypeConverter<DateTime, DateTime> {
|
||||
|
|
|
@ -3070,6 +3070,350 @@ class $AlbumSharesTable extends AlbumShares
|
|||
const _DateTimeConverter();
|
||||
}
|
||||
|
||||
class Tag extends DataClass implements Insertable<Tag> {
|
||||
final int rowId;
|
||||
final int server;
|
||||
final int tagId;
|
||||
final String displayName;
|
||||
final bool? userVisible;
|
||||
final bool? userAssignable;
|
||||
Tag(
|
||||
{required this.rowId,
|
||||
required this.server,
|
||||
required this.tagId,
|
||||
required this.displayName,
|
||||
this.userVisible,
|
||||
this.userAssignable});
|
||||
factory Tag.fromData(Map<String, dynamic> data, {String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
return Tag(
|
||||
rowId: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}row_id'])!,
|
||||
server: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}server'])!,
|
||||
tagId: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}tag_id'])!,
|
||||
displayName: const StringType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}display_name'])!,
|
||||
userVisible: const BoolType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}user_visible']),
|
||||
userAssignable: const BoolType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}user_assignable']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['row_id'] = Variable<int>(rowId);
|
||||
map['server'] = Variable<int>(server);
|
||||
map['tag_id'] = Variable<int>(tagId);
|
||||
map['display_name'] = Variable<String>(displayName);
|
||||
if (!nullToAbsent || userVisible != null) {
|
||||
map['user_visible'] = Variable<bool?>(userVisible);
|
||||
}
|
||||
if (!nullToAbsent || userAssignable != null) {
|
||||
map['user_assignable'] = Variable<bool?>(userAssignable);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
TagsCompanion toCompanion(bool nullToAbsent) {
|
||||
return TagsCompanion(
|
||||
rowId: Value(rowId),
|
||||
server: Value(server),
|
||||
tagId: Value(tagId),
|
||||
displayName: Value(displayName),
|
||||
userVisible: userVisible == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(userVisible),
|
||||
userAssignable: userAssignable == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(userAssignable),
|
||||
);
|
||||
}
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return Tag(
|
||||
rowId: serializer.fromJson<int>(json['rowId']),
|
||||
server: serializer.fromJson<int>(json['server']),
|
||||
tagId: serializer.fromJson<int>(json['tagId']),
|
||||
displayName: serializer.fromJson<String>(json['displayName']),
|
||||
userVisible: serializer.fromJson<bool?>(json['userVisible']),
|
||||
userAssignable: serializer.fromJson<bool?>(json['userAssignable']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'rowId': serializer.toJson<int>(rowId),
|
||||
'server': serializer.toJson<int>(server),
|
||||
'tagId': serializer.toJson<int>(tagId),
|
||||
'displayName': serializer.toJson<String>(displayName),
|
||||
'userVisible': serializer.toJson<bool?>(userVisible),
|
||||
'userAssignable': serializer.toJson<bool?>(userAssignable),
|
||||
};
|
||||
}
|
||||
|
||||
Tag copyWith(
|
||||
{int? rowId,
|
||||
int? server,
|
||||
int? tagId,
|
||||
String? displayName,
|
||||
Value<bool?> userVisible = const Value.absent(),
|
||||
Value<bool?> userAssignable = const Value.absent()}) =>
|
||||
Tag(
|
||||
rowId: rowId ?? this.rowId,
|
||||
server: server ?? this.server,
|
||||
tagId: tagId ?? this.tagId,
|
||||
displayName: displayName ?? this.displayName,
|
||||
userVisible: userVisible.present ? userVisible.value : this.userVisible,
|
||||
userAssignable:
|
||||
userAssignable.present ? userAssignable.value : this.userAssignable,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Tag(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('server: $server, ')
|
||||
..write('tagId: $tagId, ')
|
||||
..write('displayName: $displayName, ')
|
||||
..write('userVisible: $userVisible, ')
|
||||
..write('userAssignable: $userAssignable')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
rowId, server, tagId, displayName, userVisible, userAssignable);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is Tag &&
|
||||
other.rowId == this.rowId &&
|
||||
other.server == this.server &&
|
||||
other.tagId == this.tagId &&
|
||||
other.displayName == this.displayName &&
|
||||
other.userVisible == this.userVisible &&
|
||||
other.userAssignable == this.userAssignable);
|
||||
}
|
||||
|
||||
class TagsCompanion extends UpdateCompanion<Tag> {
|
||||
final Value<int> rowId;
|
||||
final Value<int> server;
|
||||
final Value<int> tagId;
|
||||
final Value<String> displayName;
|
||||
final Value<bool?> userVisible;
|
||||
final Value<bool?> userAssignable;
|
||||
const TagsCompanion({
|
||||
this.rowId = const Value.absent(),
|
||||
this.server = const Value.absent(),
|
||||
this.tagId = const Value.absent(),
|
||||
this.displayName = const Value.absent(),
|
||||
this.userVisible = const Value.absent(),
|
||||
this.userAssignable = const Value.absent(),
|
||||
});
|
||||
TagsCompanion.insert({
|
||||
this.rowId = const Value.absent(),
|
||||
required int server,
|
||||
required int tagId,
|
||||
required String displayName,
|
||||
this.userVisible = const Value.absent(),
|
||||
this.userAssignable = const Value.absent(),
|
||||
}) : server = Value(server),
|
||||
tagId = Value(tagId),
|
||||
displayName = Value(displayName);
|
||||
static Insertable<Tag> custom({
|
||||
Expression<int>? rowId,
|
||||
Expression<int>? server,
|
||||
Expression<int>? tagId,
|
||||
Expression<String>? displayName,
|
||||
Expression<bool?>? userVisible,
|
||||
Expression<bool?>? userAssignable,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (rowId != null) 'row_id': rowId,
|
||||
if (server != null) 'server': server,
|
||||
if (tagId != null) 'tag_id': tagId,
|
||||
if (displayName != null) 'display_name': displayName,
|
||||
if (userVisible != null) 'user_visible': userVisible,
|
||||
if (userAssignable != null) 'user_assignable': userAssignable,
|
||||
});
|
||||
}
|
||||
|
||||
TagsCompanion copyWith(
|
||||
{Value<int>? rowId,
|
||||
Value<int>? server,
|
||||
Value<int>? tagId,
|
||||
Value<String>? displayName,
|
||||
Value<bool?>? userVisible,
|
||||
Value<bool?>? userAssignable}) {
|
||||
return TagsCompanion(
|
||||
rowId: rowId ?? this.rowId,
|
||||
server: server ?? this.server,
|
||||
tagId: tagId ?? this.tagId,
|
||||
displayName: displayName ?? this.displayName,
|
||||
userVisible: userVisible ?? this.userVisible,
|
||||
userAssignable: userAssignable ?? this.userAssignable,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (rowId.present) {
|
||||
map['row_id'] = Variable<int>(rowId.value);
|
||||
}
|
||||
if (server.present) {
|
||||
map['server'] = Variable<int>(server.value);
|
||||
}
|
||||
if (tagId.present) {
|
||||
map['tag_id'] = Variable<int>(tagId.value);
|
||||
}
|
||||
if (displayName.present) {
|
||||
map['display_name'] = Variable<String>(displayName.value);
|
||||
}
|
||||
if (userVisible.present) {
|
||||
map['user_visible'] = Variable<bool?>(userVisible.value);
|
||||
}
|
||||
if (userAssignable.present) {
|
||||
map['user_assignable'] = Variable<bool?>(userAssignable.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TagsCompanion(')
|
||||
..write('rowId: $rowId, ')
|
||||
..write('server: $server, ')
|
||||
..write('tagId: $tagId, ')
|
||||
..write('displayName: $displayName, ')
|
||||
..write('userVisible: $userVisible, ')
|
||||
..write('userAssignable: $userAssignable')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $TagsTable extends Tags with TableInfo<$TagsTable, Tag> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$TagsTable(this.attachedDatabase, [this._alias]);
|
||||
final VerificationMeta _rowIdMeta = const VerificationMeta('rowId');
|
||||
@override
|
||||
late final GeneratedColumn<int?> rowId = GeneratedColumn<int?>(
|
||||
'row_id', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'PRIMARY KEY AUTOINCREMENT');
|
||||
final VerificationMeta _serverMeta = const VerificationMeta('server');
|
||||
@override
|
||||
late final GeneratedColumn<int?> server = GeneratedColumn<int?>(
|
||||
'server', aliasedName, false,
|
||||
type: const IntType(),
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: 'REFERENCES servers (row_id) ON DELETE CASCADE');
|
||||
final VerificationMeta _tagIdMeta = const VerificationMeta('tagId');
|
||||
@override
|
||||
late final GeneratedColumn<int?> tagId = GeneratedColumn<int?>(
|
||||
'tag_id', aliasedName, false,
|
||||
type: const IntType(), requiredDuringInsert: true);
|
||||
final VerificationMeta _displayNameMeta =
|
||||
const VerificationMeta('displayName');
|
||||
@override
|
||||
late final GeneratedColumn<String?> displayName = GeneratedColumn<String?>(
|
||||
'display_name', aliasedName, false,
|
||||
type: const StringType(), requiredDuringInsert: true);
|
||||
final VerificationMeta _userVisibleMeta =
|
||||
const VerificationMeta('userVisible');
|
||||
@override
|
||||
late final GeneratedColumn<bool?> userVisible = GeneratedColumn<bool?>(
|
||||
'user_visible', aliasedName, true,
|
||||
type: const BoolType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'CHECK (user_visible IN (0, 1))');
|
||||
final VerificationMeta _userAssignableMeta =
|
||||
const VerificationMeta('userAssignable');
|
||||
@override
|
||||
late final GeneratedColumn<bool?> userAssignable = GeneratedColumn<bool?>(
|
||||
'user_assignable', aliasedName, true,
|
||||
type: const BoolType(),
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: 'CHECK (user_assignable IN (0, 1))');
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[rowId, server, tagId, displayName, userVisible, userAssignable];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'tags';
|
||||
@override
|
||||
String get actualTableName => 'tags';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<Tag> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('row_id')) {
|
||||
context.handle(
|
||||
_rowIdMeta, rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta));
|
||||
}
|
||||
if (data.containsKey('server')) {
|
||||
context.handle(_serverMeta,
|
||||
server.isAcceptableOrUnknown(data['server']!, _serverMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_serverMeta);
|
||||
}
|
||||
if (data.containsKey('tag_id')) {
|
||||
context.handle(
|
||||
_tagIdMeta, tagId.isAcceptableOrUnknown(data['tag_id']!, _tagIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_tagIdMeta);
|
||||
}
|
||||
if (data.containsKey('display_name')) {
|
||||
context.handle(
|
||||
_displayNameMeta,
|
||||
displayName.isAcceptableOrUnknown(
|
||||
data['display_name']!, _displayNameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_displayNameMeta);
|
||||
}
|
||||
if (data.containsKey('user_visible')) {
|
||||
context.handle(
|
||||
_userVisibleMeta,
|
||||
userVisible.isAcceptableOrUnknown(
|
||||
data['user_visible']!, _userVisibleMeta));
|
||||
}
|
||||
if (data.containsKey('user_assignable')) {
|
||||
context.handle(
|
||||
_userAssignableMeta,
|
||||
userAssignable.isAcceptableOrUnknown(
|
||||
data['user_assignable']!, _userAssignableMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {rowId};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{server, tagId},
|
||||
];
|
||||
@override
|
||||
Tag map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
return Tag.fromData(data,
|
||||
prefix: tablePrefix != null ? '$tablePrefix.' : null);
|
||||
}
|
||||
|
||||
@override
|
||||
$TagsTable createAlias(String alias) {
|
||||
return $TagsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$SqliteDb extends GeneratedDatabase {
|
||||
_$SqliteDb(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
_$SqliteDb.connect(DatabaseConnection c) : super.connect(c);
|
||||
|
@ -3082,6 +3426,7 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
late final $DirFilesTable dirFiles = $DirFilesTable(this);
|
||||
late final $AlbumsTable albums = $AlbumsTable(this);
|
||||
late final $AlbumSharesTable albumShares = $AlbumSharesTable(this);
|
||||
late final $TagsTable tags = $TagsTable(this);
|
||||
@override
|
||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||
@override
|
||||
|
@ -3094,6 +3439,7 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
trashes,
|
||||
dirFiles,
|
||||
albums,
|
||||
albumShares
|
||||
albumShares,
|
||||
tags
|
||||
];
|
||||
}
|
||||
|
|
|
@ -10,8 +10,26 @@ import 'package:nc_photos/entity/exif.dart';
|
|||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/entity/sqlite_table.dart' as sql;
|
||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
|
||||
extension SqlTagListExtension on List<sql.Tag> {
|
||||
Future<List<Tag>> convertToAppTag() {
|
||||
return computeAll(SqliteTagConverter.fromSql);
|
||||
}
|
||||
}
|
||||
|
||||
extension AppTagListExtension on List<Tag> {
|
||||
Future<List<sql.TagsCompanion>> convertToTagCompanion(
|
||||
sql.Account? dbAccount) {
|
||||
return map((t) => {
|
||||
"account": dbAccount,
|
||||
"tag": t,
|
||||
}).computeAll(_convertAppTag);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteAlbumConverter {
|
||||
static Album fromSql(
|
||||
sql.Album album, File albumFile, List<sql.AlbumShare> shares) {
|
||||
|
@ -142,3 +160,29 @@ class SqliteFileConverter {
|
|||
return sql.CompleteFileCompanion(dbFile, dbAccountFile, dbImage, dbTrash);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteTagConverter {
|
||||
static Tag fromSql(sql.Tag tag) => Tag(
|
||||
id: tag.tagId,
|
||||
displayName: tag.displayName,
|
||||
userVisible: tag.userVisible,
|
||||
userAssignable: tag.userAssignable,
|
||||
);
|
||||
|
||||
static sql.TagsCompanion toSql(sql.Account? dbAccount, Tag tag) =>
|
||||
sql.TagsCompanion(
|
||||
server:
|
||||
dbAccount == null ? const Value.absent() : Value(dbAccount.server),
|
||||
tagId: Value(tag.id),
|
||||
displayName: Value(tag.displayName),
|
||||
userVisible: Value(tag.userVisible),
|
||||
userAssignable: Value(tag.userAssignable),
|
||||
);
|
||||
}
|
||||
|
||||
sql.TagsCompanion _convertAppTag(Map map) {
|
||||
final account = map["account"] as sql.Account?;
|
||||
final tag = map["tag"] as Tag;
|
||||
return SqliteTagConverter.toSql(account, tag);
|
||||
}
|
||||
|
||||
|
|
|
@ -394,6 +394,25 @@ extension SqliteDbExtension on SqliteDb {
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<List<Tag>> allTags({
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
}) {
|
||||
assert((sqlAccount != null) != (appAccount != null));
|
||||
if (sqlAccount != null) {
|
||||
final query = select(tags)
|
||||
..where((t) => t.server.equals(sqlAccount.server));
|
||||
return query.get();
|
||||
} else {
|
||||
final query = select(tags).join([
|
||||
innerJoin(servers, servers.rowId.equalsExp(tags.server),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(servers.address.equals(appAccount!.url));
|
||||
return query.map((r) => r.readTable(tags)).get();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> truncate() async {
|
||||
await delete(servers).go();
|
||||
// technically deleting Servers table is enough to clear the followings, but
|
||||
|
@ -406,6 +425,7 @@ extension SqliteDbExtension on SqliteDb {
|
|||
await delete(dirFiles).go();
|
||||
await delete(albums).go();
|
||||
await delete(albumShares).go();
|
||||
await delete(tags).go();
|
||||
|
||||
// reset the auto increment counter
|
||||
await customStatement("UPDATE sqlite_sequence SET seq=0;");
|
||||
|
|
|
@ -2,6 +2,9 @@ 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/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;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/webdav_response_parser.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
|
@ -55,3 +58,26 @@ class TagRemoteDataSource implements TagDataSource {
|
|||
|
||||
static final _log = Logger("entity.tag.data_source.TagRemoteDataSource");
|
||||
}
|
||||
|
||||
class TagSqliteDbDataSource implements TagDataSource {
|
||||
const TagSqliteDbDataSource(this.sqliteDb);
|
||||
|
||||
@override
|
||||
list(Account account) async {
|
||||
_log.info("[list] $account");
|
||||
final dbTags = await sqliteDb.use((db) async {
|
||||
return await db.allTags(appAccount: account);
|
||||
});
|
||||
return dbTags.convertToAppTag();
|
||||
}
|
||||
|
||||
@override
|
||||
listByFile(Account account, File file) async {
|
||||
_log.info("[listByFile] ${file.path}");
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
final sql.SqliteDb sqliteDb;
|
||||
|
||||
static final _log = Logger("entity.tag.data_source.TagSqliteDbDataSource");
|
||||
}
|
||||
|
|
|
@ -11,12 +11,14 @@ import 'package:nc_photos/event/event.dart';
|
|||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/type.dart';
|
||||
import 'package:nc_photos/use_case/sync_favorite.dart';
|
||||
import 'package:nc_photos/use_case/sync_tag.dart';
|
||||
|
||||
/// Sync various properties with server during startup
|
||||
class StartupSync {
|
||||
StartupSync(this._c)
|
||||
: assert(require(_c)),
|
||||
assert(SyncFavorite.require(_c));
|
||||
assert(SyncFavorite.require(_c)),
|
||||
assert(SyncTag.require(_c));
|
||||
|
||||
static bool require(DiContainer c) => true;
|
||||
|
||||
|
@ -49,6 +51,11 @@ class StartupSync {
|
|||
_log.shout("[_run] Failed while SyncFavorite", e, stackTrace);
|
||||
syncFavoriteCount = -1;
|
||||
}
|
||||
try {
|
||||
await SyncTag(_c)(account);
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout("[_run] Failed while SyncTag", e, stackTrace);
|
||||
}
|
||||
_log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms");
|
||||
return SyncResult(syncFavoriteCount);
|
||||
}
|
||||
|
|
72
app/lib/use_case/sync_tag.dart
Normal file
72
app/lib/use_case/sync_tag.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' as sql;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/di_container.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;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/list_util.dart' as list_util;
|
||||
|
||||
class SyncTag {
|
||||
SyncTag(this._c) : assert(require(_c));
|
||||
|
||||
static bool require(DiContainer c) =>
|
||||
DiContainer.has(c, DiType.tagRepoRemote) &&
|
||||
DiContainer.has(c, DiType.tagRepoLocal);
|
||||
|
||||
/// Sync tags in cache db with remote server
|
||||
Future<void> call(Account account) async {
|
||||
_log.info("[call] Sync tags with remote");
|
||||
int tagSorter(Tag a, Tag b) => a.id.compareTo(b.id);
|
||||
final remote = (await _c.tagRepoRemote.list(account))..sort(tagSorter);
|
||||
final cache = (await _c.tagRepoLocal.list(account))..sort(tagSorter);
|
||||
final diff = list_util.diffWith<Tag>(cache, remote, tagSorter);
|
||||
final inserts = diff.item1;
|
||||
_log.info("[call] New tags: ${inserts.toReadableString()}");
|
||||
final deletes = diff.item2;
|
||||
_log.info("[call] Removed tags: ${deletes.toReadableString()}");
|
||||
final updates = remote.where((r) {
|
||||
final c = cache.firstWhereOrNull((c) => c.id == r.id);
|
||||
return c != null && c != r;
|
||||
}).toList();
|
||||
_log.info("[call] Updated tags: ${updates.toReadableString()}");
|
||||
|
||||
if (inserts.isNotEmpty || deletes.isNotEmpty || updates.isNotEmpty) {
|
||||
await _c.sqliteDb.use((db) async {
|
||||
final dbAccount = await db.accountOf(account);
|
||||
await db.batch((batch) {
|
||||
for (final d in deletes) {
|
||||
batch.deleteWhere(
|
||||
db.tags,
|
||||
(sql.$TagsTable t) =>
|
||||
t.server.equals(dbAccount.server) & t.tagId.equals(d.id),
|
||||
);
|
||||
}
|
||||
for (final u in updates) {
|
||||
batch.update(
|
||||
db.tags,
|
||||
sql.TagsCompanion(
|
||||
displayName: sql.Value(u.displayName),
|
||||
userVisible: sql.Value(u.userVisible),
|
||||
userAssignable: sql.Value(u.userAssignable),
|
||||
),
|
||||
where: (sql.$TagsTable t) =>
|
||||
t.server.equals(dbAccount.server) & t.tagId.equals(u.id),
|
||||
);
|
||||
}
|
||||
for (final i in inserts) {
|
||||
batch.insert(db.tags, SqliteTagConverter.toSql(dbAccount, i),
|
||||
mode: sql.InsertMode.insertOrIgnore);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final DiContainer _c;
|
||||
|
||||
static final _log = Logger("use_case.sync_tag.SyncTag");
|
||||
}
|
|
@ -318,6 +318,7 @@ Future<void> _truncate() async {
|
|||
"dir_files",
|
||||
"albums",
|
||||
"album_shares",
|
||||
"tags",
|
||||
});
|
||||
for (final t in tables) {
|
||||
expect(
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:nc_photos/entity/file/data_source.dart';
|
|||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/exception_event.dart';
|
||||
import 'package:nc_photos/future_util.dart' as future_util;
|
||||
import 'package:nc_photos/or_null.dart';
|
||||
|
@ -422,6 +423,34 @@ class MockShareeMemoryRepo extends MockShareeRepo {
|
|||
final List<Sharee> sharees;
|
||||
}
|
||||
|
||||
class MockTagRepo implements TagRepo {
|
||||
@override
|
||||
TagDataSource get dataSrc => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<List<Tag>> list(Account account) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Tag>> listByFile(Account account, File file) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class MockTagMemoryRepo extends MockTagRepo {
|
||||
MockTagMemoryRepo([
|
||||
Map<String, List<Tag>> initialData = const {},
|
||||
]) : tags = initialData.map((key, value) => MapEntry(key, List.of(value)));
|
||||
|
||||
@override
|
||||
list(Account account) async {
|
||||
return tags[account.url]!;
|
||||
}
|
||||
|
||||
final Map<String, List<Tag>> tags;
|
||||
}
|
||||
|
||||
extension MockDiContainerExtension on DiContainer {
|
||||
MockAlbumMemoryRepo get albumMemoryRepo => albumRepo as MockAlbumMemoryRepo;
|
||||
MockFileMemoryRepo get fileMemoryRepo => fileRepo as MockFileMemoryRepo;
|
||||
|
|
153
app/test/use_case/sync_tag_test.dart
Normal file
153
app/test/use_case/sync_tag_test.dart
Normal file
|
@ -0,0 +1,153 @@
|
|||
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/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;
|
||||
import 'package:nc_photos/entity/tag.dart';
|
||||
import 'package:nc_photos/entity/tag/data_source.dart';
|
||||
import 'package:nc_photos/use_case/sync_tag.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../mock_type.dart';
|
||||
import '../test_util.dart' as util;
|
||||
|
||||
void main() {
|
||||
group("SyncTag", () {
|
||||
test("new", _new);
|
||||
test("remove", _remove);
|
||||
test("update", _update);
|
||||
});
|
||||
}
|
||||
|
||||
/// Sync with remote where there are new tags
|
||||
///
|
||||
/// Remote: [tag0, tag1, tag2]
|
||||
/// Local: [tag0]
|
||||
/// Expect: [tag0, tag1, tag2]
|
||||
Future<void> _new() async {
|
||||
final account = util.buildAccount();
|
||||
final c = DiContainer.late();
|
||||
c.sqliteDb = util.buildTestDb();
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
c.tagRepoRemote = MockTagMemoryRepo({
|
||||
account.url: [
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
const Tag(id: 11, displayName: "tag1"),
|
||||
const Tag(id: 12, displayName: "tag2"),
|
||||
],
|
||||
});
|
||||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.batch((batch) {
|
||||
batch.insert(c.sqliteDb.tags,
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"));
|
||||
});
|
||||
});
|
||||
|
||||
await SyncTag(c)(account);
|
||||
expect(
|
||||
await _listSqliteDbTags(c.sqliteDb),
|
||||
{
|
||||
account.url: {
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
const Tag(id: 11, displayName: "tag1"),
|
||||
const Tag(id: 12, displayName: "tag2"),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Sync with remote where there are removed tags
|
||||
///
|
||||
/// Remote: [tag0]
|
||||
/// Local: [tag0, tag1, tag2]
|
||||
/// Expect: [tag0]
|
||||
Future<void> _remove() async {
|
||||
final account = util.buildAccount();
|
||||
final c = DiContainer.late();
|
||||
c.sqliteDb = util.buildTestDb();
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
c.tagRepoRemote = MockTagMemoryRepo({
|
||||
account.url: [
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
],
|
||||
});
|
||||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.batch((batch) {
|
||||
batch.insertAll(c.sqliteDb.tags, [
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 12, displayName: "tag2"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
await SyncTag(c)(account);
|
||||
expect(
|
||||
await _listSqliteDbTags(c.sqliteDb),
|
||||
{
|
||||
account.url: {
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Sync with remote where there are updated tags (i.e, same id, different
|
||||
/// properties)
|
||||
///
|
||||
/// Remote: [tag0, new tag1]
|
||||
/// Local: [tag0, tag1]
|
||||
/// Expect: [tag0, new tag1]
|
||||
Future<void> _update() async {
|
||||
final account = util.buildAccount();
|
||||
final c = DiContainer.late();
|
||||
c.sqliteDb = util.buildTestDb();
|
||||
addTearDown(() => c.sqliteDb.close());
|
||||
c.tagRepoRemote = MockTagMemoryRepo({
|
||||
account.url: [
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
const Tag(id: 11, displayName: "new tag1"),
|
||||
],
|
||||
});
|
||||
c.tagRepoLocal = TagRepo(TagSqliteDbDataSource(c.sqliteDb));
|
||||
await c.sqliteDb.transaction(() async {
|
||||
await c.sqliteDb.insertAccountOf(account);
|
||||
await c.sqliteDb.batch((batch) {
|
||||
batch.insertAll(c.sqliteDb.tags, [
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 10, displayName: "tag0"),
|
||||
sql.TagsCompanion.insert(server: 1, tagId: 11, displayName: "tag1"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
await SyncTag(c)(account);
|
||||
expect(
|
||||
await _listSqliteDbTags(c.sqliteDb),
|
||||
{
|
||||
account.url: {
|
||||
const Tag(id: 10, displayName: "tag0"),
|
||||
const Tag(id: 11, displayName: "new tag1"),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, Set<Tag>>> _listSqliteDbTags(sql.SqliteDb db) async {
|
||||
final query = db.select(db.tags).join([
|
||||
sql.innerJoin(db.servers, db.servers.rowId.equalsExp(db.tags.server)),
|
||||
]);
|
||||
final result = await query
|
||||
.map((r) => Tuple2(r.readTable(db.servers), r.readTable(db.tags)))
|
||||
.get();
|
||||
final product = <String, Set<Tag>>{};
|
||||
for (final r in result) {
|
||||
(product[r.item1.address] ??= {}).add(SqliteTagConverter.fromSql(r.item2));
|
||||
}
|
||||
return product;
|
||||
}
|
Loading…
Add table
Reference in a new issue