From 77115cb511017d7d8547beafefe8c2286a6d3dcf Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Fri, 11 Apr 2025 20:53:48 +0530 Subject: [PATCH] [client] Trashed resources --- .../actions/action_resource_copy.dart | 8 +- .../actions/action_resource_delete.dart | 26 +- .../actions/action_resource_mkdir.dart | 8 +- .../actions/action_resource_move.dart | 34 +- .../actions/action_resource_upload.dart | 8 +- ...dart => mark_resource_deleted_change.dart} | 7 +- .../move_resource_to_trash_change.dart | 17 + client/lib/libphylum/db/db.dart | 6 +- client/lib/libphylum/db/db.g.dart | 297 +++++++++++++++++- client/lib/libphylum/db/sql/resources.drift | 4 - client/lib/libphylum/db/sql/trash.drift | 8 + client/lib/libphylum/explorer/page.dart | 4 +- client/lib/libphylum/phylum_account.dart | 3 + .../repositories/resource_repository.dart | 10 +- .../repositories/trash_repository.dart | 58 ++++ .../responses/trash_list_response.dart | 13 +- client/lib/ui/explorer/explorer_actions.dart | 2 +- client/lib/ui/menu/menu_option.dart | 2 +- 18 files changed, 459 insertions(+), 56 deletions(-) rename client/lib/libphylum/actions/changes/{soft_delete_resource_change.dart => mark_resource_deleted_change.dart} (54%) create mode 100644 client/lib/libphylum/actions/changes/move_resource_to_trash_change.dart create mode 100644 client/lib/libphylum/db/sql/trash.drift create mode 100644 client/lib/libphylum/repositories/trash_repository.dart diff --git a/client/lib/libphylum/actions/action_resource_copy.dart b/client/lib/libphylum/actions/action_resource_copy.dart index d2fcc2fd..d73fd14f 100644 --- a/client/lib/libphylum/actions/action_resource_copy.dart +++ b/client/lib/libphylum/actions/action_resource_copy.dart @@ -1,7 +1,7 @@ import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/actions/action_resource_create.dart'; import 'package:phylum/libphylum/actions/changes/create_resource_change.dart'; -import 'package:phylum/libphylum/actions/changes/soft_delete_resource_change.dart'; +import 'package:phylum/libphylum/actions/changes/mark_resource_deleted_change.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; class ResourceCopyAction extends ResourceCreateAction with JsonApiAction { @@ -33,7 +33,11 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction { contentType: contentType, contentSha256: contentSha256, ), - if (deletedId != null) SoftDeleteResourceChange(objectId: deletedId!), + if (deletedId != null) + MarkResourceDeletedChange( + objectId: deletedId!, + timestamp: timestamp, + ), ]; @override diff --git a/client/lib/libphylum/actions/action_resource_delete.dart b/client/lib/libphylum/actions/action_resource_delete.dart index f27b1dbe..1764c8b9 100644 --- a/client/lib/libphylum/actions/action_resource_delete.dart +++ b/client/lib/libphylum/actions/action_resource_delete.dart @@ -1,5 +1,6 @@ import 'package:offtheline/offtheline.dart'; -import 'package:phylum/libphylum/actions/changes/soft_delete_resource_change.dart'; +import 'package:phylum/libphylum/actions/changes/mark_resource_deleted_change.dart'; +import 'package:phylum/libphylum/actions/changes/move_resource_to_trash_change.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; @@ -18,7 +19,13 @@ class ResourceDeleteAction extends ResourceAction with JsonApiAction { String get endpoint => '/api/v1/fs/delete/$resourceId'; @override - List get localChanges => [SoftDeleteResourceChange(objectId: resourceId)]; + List get localChanges => [ + MarkResourceDeletedChange( + objectId: resourceId, + timestamp: timestamp, + ), + MoveResourceToTrashChange(objectId: resourceId), + ]; @override String get description => "Deleting $resourceName"; @@ -27,23 +34,28 @@ class ResourceDeleteAction extends ResourceAction with JsonApiAction { Map get props => { 'resourceId': resourceId, 'resourceName': resourceName, + 'timestamp': timestamp.millisecondsSinceEpoch, }; final String resourceName; + final DateTime timestamp; - ResourceDeleteAction._({ - required super.resourceId, - required this.resourceName, - }); + ResourceDeleteAction._({required super.resourceId, required this.resourceName, required this.timestamp}); ResourceDeleteAction({ required Resource r, - }) : this._(resourceId: r.id, resourceName: r.name); + required DateTime timestamp, + }) : this._( + resourceId: r.id, + resourceName: r.name, + timestamp: timestamp, + ); factory ResourceDeleteAction.fromMap(Map map) { return ResourceDeleteAction._( resourceId: map['resourceId'], resourceName: map['resourceName'], + timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']), ); } diff --git a/client/lib/libphylum/actions/action_resource_mkdir.dart b/client/lib/libphylum/actions/action_resource_mkdir.dart index ce920607..4ed334d1 100644 --- a/client/lib/libphylum/actions/action_resource_mkdir.dart +++ b/client/lib/libphylum/actions/action_resource_mkdir.dart @@ -1,7 +1,7 @@ import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/actions/action_resource_create.dart'; import 'package:phylum/libphylum/actions/changes/create_resource_change.dart'; -import 'package:phylum/libphylum/actions/changes/soft_delete_resource_change.dart'; +import 'package:phylum/libphylum/actions/changes/mark_resource_deleted_change.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; class ResourceMkdirAction extends ResourceCreateAction with JsonApiAction { @@ -33,7 +33,11 @@ class ResourceMkdirAction extends ResourceCreateAction with JsonApiAction { contentType: '', contentSha256: '', ), - if (deletedId != null) SoftDeleteResourceChange(objectId: resourceId), + if (deletedId != null) + MarkResourceDeletedChange( + objectId: resourceId, + timestamp: timestamp, + ), ]; @override diff --git a/client/lib/libphylum/actions/action_resource_move.dart b/client/lib/libphylum/actions/action_resource_move.dart index eb81cd10..a431749e 100644 --- a/client/lib/libphylum/actions/action_resource_move.dart +++ b/client/lib/libphylum/actions/action_resource_move.dart @@ -3,7 +3,7 @@ import 'package:phylum/libphylum/actions/action_resource_bind.dart'; import 'package:phylum/libphylum/actions/action_resource_create.dart'; import 'package:phylum/libphylum/actions/changes/mark_resource_modified_change.dart'; import 'package:phylum/libphylum/actions/changes/move_resource_change.dart'; -import 'package:phylum/libphylum/actions/changes/soft_delete_resource_change.dart'; +import 'package:phylum/libphylum/actions/changes/mark_resource_deleted_change.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; class ResourceMoveAction extends ResourceBindAction with JsonApiAction { @@ -26,10 +26,26 @@ class ResourceMoveAction extends ResourceBindAction with JsonApiAction { @override List get localChanges => [ - MoveResourceChange(objectId: resourceId, parent: parent, name: localName, timestamp: modified), - MarkResourceModifiedChange(objectId: parent, timestamp: modified), - if (oldParent != null) MarkResourceModifiedChange(objectId: oldParent!, timestamp: modified), - if (deletedId != null) SoftDeleteResourceChange(objectId: resourceId), + MoveResourceChange( + objectId: resourceId, + parent: parent, + name: localName, + timestamp: timestamp, + ), + MarkResourceModifiedChange( + objectId: parent, + timestamp: timestamp, + ), + if (oldParent != null) + MarkResourceModifiedChange( + objectId: oldParent!, + timestamp: timestamp, + ), + if (deletedId != null) + MarkResourceDeletedChange( + objectId: resourceId, + timestamp: timestamp, + ), ]; @override @@ -42,13 +58,13 @@ class ResourceMoveAction extends ResourceBindAction with JsonApiAction { 'resourceName': resourceName, 'conflictResolution': conflictResolution, 'description': description, - 'modified': modified.millisecondsSinceEpoch, + 'modified': timestamp.millisecondsSinceEpoch, 'localName': localName, 'oldParent': oldParent, 'deletedId': deletedId, }; - final DateTime modified; + final DateTime timestamp; final String localName; final String? oldParent; final String? deletedId; @@ -59,7 +75,7 @@ class ResourceMoveAction extends ResourceBindAction with JsonApiAction { required super.resourceName, required super.conflictResolution, required this.description, - required this.modified, + required this.timestamp, required this.oldParent, required this.localName, required this.deletedId, @@ -72,7 +88,7 @@ class ResourceMoveAction extends ResourceBindAction with JsonApiAction { resourceName: map['resourceName'], conflictResolution: map['conflictResolution'], description: map['description'], - modified: DateTime.fromMillisecondsSinceEpoch(map['modified']), + timestamp: DateTime.fromMillisecondsSinceEpoch(map['modified']), localName: map['localName'], oldParent: map['oldParent'], deletedId: map['deletedId'], diff --git a/client/lib/libphylum/actions/action_resource_upload.dart b/client/lib/libphylum/actions/action_resource_upload.dart index e3b85753..75ee0f21 100644 --- a/client/lib/libphylum/actions/action_resource_upload.dart +++ b/client/lib/libphylum/actions/action_resource_upload.dart @@ -3,7 +3,7 @@ import 'package:http_parser/http_parser.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/actions/action_resource_create.dart'; import 'package:phylum/libphylum/actions/changes/create_resource_change.dart'; -import 'package:phylum/libphylum/actions/changes/soft_delete_resource_change.dart'; +import 'package:phylum/libphylum/actions/changes/mark_resource_deleted_change.dart'; import 'package:phylum/libphylum/phylum_api_types.dart'; class ResourceUploadAction extends ResourceCreateAction with FileUploadApiAction { @@ -39,7 +39,11 @@ class ResourceUploadAction extends ResourceCreateAction with FileUploadApiAction contentType: contentType, contentSha256: '', ), - if (deletedId != null) SoftDeleteResourceChange(objectId: deletedId!), + if (deletedId != null) + MarkResourceDeletedChange( + objectId: deletedId!, + timestamp: timestamp, + ), ]; @override diff --git a/client/lib/libphylum/actions/changes/soft_delete_resource_change.dart b/client/lib/libphylum/actions/changes/mark_resource_deleted_change.dart similarity index 54% rename from client/lib/libphylum/actions/changes/soft_delete_resource_change.dart rename to client/lib/libphylum/actions/changes/mark_resource_deleted_change.dart index 0fba93a9..cf0136d9 100644 --- a/client/lib/libphylum/actions/changes/soft_delete_resource_change.dart +++ b/client/lib/libphylum/actions/changes/mark_resource_deleted_change.dart @@ -2,17 +2,18 @@ import 'package:drift/drift.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; -class SoftDeleteResourceChange extends LocalChange { +class MarkResourceDeletedChange extends LocalChange { @override final String objectId; + final DateTime timestamp; @override String get description => 'Deleting'; - const SoftDeleteResourceChange({required this.objectId}); + const MarkResourceDeletedChange({required this.objectId, required this.timestamp}); @override Resource? apply(Resource? data) { - return data?.copyWith(deleted: Value(DateTime.now())); + return data?.copyWith(deleted: Value(timestamp)); } } diff --git a/client/lib/libphylum/actions/changes/move_resource_to_trash_change.dart b/client/lib/libphylum/actions/changes/move_resource_to_trash_change.dart new file mode 100644 index 00000000..a7d04689 --- /dev/null +++ b/client/lib/libphylum/actions/changes/move_resource_to_trash_change.dart @@ -0,0 +1,17 @@ +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/db/db.dart'; + +class MoveResourceToTrashChange extends LocalChange { + @override + final String objectId; + + @override + String get description => 'Moving To Trash'; + + const MoveResourceToTrashChange({required this.objectId}); + + @override + TrashedResource? apply(TrashedResource? data) { + return data ?? TrashedResource(id: objectId); + } +} diff --git a/client/lib/libphylum/db/db.dart b/client/lib/libphylum/db/db.dart index e13c6a28..de97000b 100644 --- a/client/lib/libphylum/db/db.dart +++ b/client/lib/libphylum/db/db.dart @@ -11,6 +11,7 @@ part 'db.g.dart'; 'sql/recents.drift', 'sql/resources.drift', 'sql/shared.drift', + 'sql/trash.drift', 'sql/users.drift', }) class AppDatabase extends _$AppDatabase { @@ -22,7 +23,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase.fromExecutor({required this.accountId, required QueryExecutor executor}) : super(executor); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( @@ -40,6 +41,9 @@ class AppDatabase extends _$AppDatabase { m.drop(resources); m.create(resources); } + if (from < 4) { + m.create(trashedResources); + } }, ); diff --git a/client/lib/libphylum/db/db.g.dart b/client/lib/libphylum/db/db.g.dart index e69d3ecf..bdbeb7b1 100644 --- a/client/lib/libphylum/db/db.g.dart +++ b/client/lib/libphylum/db/db.g.dart @@ -898,6 +898,164 @@ class ResourcesCompanion extends UpdateCompanion { } } +class TrashedResources extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedResources(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY REFERENCES resources'); + @override + List get $columns => [id]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_resources'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TrashedResource map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedResource( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + ); + } + + @override + TrashedResources createAlias(String alias) { + return TrashedResources(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class TrashedResource extends DataClass implements Insertable { + final String id; + const TrashedResource({required this.id}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + return map; + } + + TrashedResourcesCompanion toCompanion(bool nullToAbsent) { + return TrashedResourcesCompanion( + id: Value(id), + ); + } + + factory TrashedResource.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedResource( + id: serializer.fromJson(json['id']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + }; + } + + TrashedResource copyWith({String? id}) => TrashedResource( + id: id ?? this.id, + ); + TrashedResource copyWithCompanion(TrashedResourcesCompanion data) { + return TrashedResource( + id: data.id.present ? data.id.value : this.id, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedResource(') + ..write('id: $id') + ..write(')')) + .toString(); + } + + @override + int get hashCode => id.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedResource && other.id == this.id); +} + +class TrashedResourcesCompanion extends UpdateCompanion { + final Value id; + final Value rowid; + const TrashedResourcesCompanion({ + this.id = const Value.absent(), + this.rowid = const Value.absent(), + }); + TrashedResourcesCompanion.insert({ + required String id, + this.rowid = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (rowid != null) 'rowid': rowid, + }); + } + + TrashedResourcesCompanion copyWith({Value? id, Value? rowid}) { + return TrashedResourcesCompanion( + id: id ?? this.id, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedResourcesCompanion(') + ..write('id: $id, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class Shared extends Table with TableInfo { @override final GeneratedDatabase attachedDatabase; @@ -1621,17 +1779,26 @@ abstract class _$AppDatabase extends GeneratedDatabase { $AppDatabaseManager get managers => $AppDatabaseManager(this); late final Users users = Users(this); late final Resources resources = Resources(this); + late final TrashedResources trashedResources = TrashedResources(this); late final Shared shared = Shared(this); late final Index sharedResources = Index('shared_resources', 'CREATE INDEX IF NOT EXISTS shared_resources ON shared (seq)'); - late final Index deletedResources = Index('deleted_resources', - 'CREATE INDEX IF NOT EXISTS deleted_resources ON resources (deleted DESC)'); late final Recents recents = Recents(this); late final Index recentAccesses = Index('recent_accesses', 'CREATE INDEX IF NOT EXISTS recent_accesses ON recents (accessed DESC)'); late final Bookmarks bookmarks = Bookmarks(this); late final Index orderedBookmarks = Index('ordered_bookmarks', 'CREATE INDEX IF NOT EXISTS ordered_bookmarks ON bookmarks (created)'); + Selectable selectTrash() { + return customSelect( + 'SELECT r.* FROM trashed_resources AS t JOIN resources AS r ON t.id = r.id ORDER BY r.deleted DESC', + variables: [], + readsFrom: { + trashedResources, + resources, + }).asyncMap(resources.mapFromRow); + } + Selectable selectShared() { return customSelect( 'SELECT r.* FROM shared AS s LEFT JOIN resources AS r ON s.resource_id = r.id ORDER BY s.seq', @@ -1642,15 +1809,6 @@ abstract class _$AppDatabase extends GeneratedDatabase { }).asyncMap(resources.mapFromRow); } - Selectable selectDeleted() { - return customSelect( - 'SELECT * FROM resources WHERE deleted IS NOT NULL ORDER BY deleted DESC', - variables: [], - readsFrom: { - resources, - }).asyncMap(resources.mapFromRow); - } - Selectable parents(String id) { return customSelect( 'WITH RECURSIVE parents (id, parent, name, permissions) AS (SELECT id, parent, name, permissions FROM resources WHERE id = ?1 UNION ALL SELECT r.id, r.parent, r.name, r.permissions FROM resources AS r JOIN parents AS p ON r.id = p.parent) SELECT id, name, permissions FROM parents', @@ -1714,9 +1872,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { List get allSchemaEntities => [ users, resources, + trashedResources, shared, sharedResources, - deletedResources, recents, recentAccesses, bookmarks, @@ -2369,6 +2527,119 @@ typedef $ResourcesProcessedTableManager = ProcessedTableManager< Resource, PrefetchHooks Function( {bool sharedRefs, bool recentsRefs, bool bookmarksRefs})>; +typedef $TrashedResourcesCreateCompanionBuilder = TrashedResourcesCompanion + Function({ + required String id, + Value rowid, +}); +typedef $TrashedResourcesUpdateCompanionBuilder = TrashedResourcesCompanion + Function({ + Value id, + Value rowid, +}); + +class $TrashedResourcesFilterComposer + extends Composer<_$AppDatabase, TrashedResources> { + $TrashedResourcesFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); +} + +class $TrashedResourcesOrderingComposer + extends Composer<_$AppDatabase, TrashedResources> { + $TrashedResourcesOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); +} + +class $TrashedResourcesAnnotationComposer + extends Composer<_$AppDatabase, TrashedResources> { + $TrashedResourcesAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); +} + +class $TrashedResourcesTableManager extends RootTableManager< + _$AppDatabase, + TrashedResources, + TrashedResource, + $TrashedResourcesFilterComposer, + $TrashedResourcesOrderingComposer, + $TrashedResourcesAnnotationComposer, + $TrashedResourcesCreateCompanionBuilder, + $TrashedResourcesUpdateCompanionBuilder, + ( + TrashedResource, + BaseReferences<_$AppDatabase, TrashedResources, TrashedResource> + ), + TrashedResource, + PrefetchHooks Function()> { + $TrashedResourcesTableManager(_$AppDatabase db, TrashedResources table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $TrashedResourcesFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $TrashedResourcesOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $TrashedResourcesAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value rowid = const Value.absent(), + }) => + TrashedResourcesCompanion( + id: id, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + Value rowid = const Value.absent(), + }) => + TrashedResourcesCompanion.insert( + id: id, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $TrashedResourcesProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + TrashedResources, + TrashedResource, + $TrashedResourcesFilterComposer, + $TrashedResourcesOrderingComposer, + $TrashedResourcesAnnotationComposer, + $TrashedResourcesCreateCompanionBuilder, + $TrashedResourcesUpdateCompanionBuilder, + ( + TrashedResource, + BaseReferences<_$AppDatabase, TrashedResources, TrashedResource> + ), + TrashedResource, + PrefetchHooks Function()>; typedef $SharedCreateCompanionBuilder = SharedCompanion Function({ required String resourceId, required int seq, @@ -3084,6 +3355,8 @@ class $AppDatabaseManager { $UsersTableManager get users => $UsersTableManager(_db, _db.users); $ResourcesTableManager get resources => $ResourcesTableManager(_db, _db.resources); + $TrashedResourcesTableManager get trashedResources => + $TrashedResourcesTableManager(_db, _db.trashedResources); $SharedTableManager get shared => $SharedTableManager(_db, _db.shared); $RecentsTableManager get recents => $RecentsTableManager(_db, _db.recents); $BookmarksTableManager get bookmarks => diff --git a/client/lib/libphylum/db/sql/resources.drift b/client/lib/libphylum/db/sql/resources.drift index ec5e8deb..774b609a 100644 --- a/client/lib/libphylum/db/sql/resources.drift +++ b/client/lib/libphylum/db/sql/resources.drift @@ -15,10 +15,6 @@ CREATE TABLE IF NOT EXISTS resources( last_refresh DATETIME ); -CREATE INDEX IF NOT EXISTS deleted_resources ON resources(deleted DESC); - -selectDeleted: SELECT * FROM resources WHERE deleted IS NOT NULL ORDER BY deleted DESC; - parents: WITH RECURSIVE parents(id, parent, name, permissions) AS ( SELECT id, parent, name, permissions FROM resources diff --git a/client/lib/libphylum/db/sql/trash.drift b/client/lib/libphylum/db/sql/trash.drift new file mode 100644 index 00000000..39b98932 --- /dev/null +++ b/client/lib/libphylum/db/sql/trash.drift @@ -0,0 +1,8 @@ +import 'resources.drift'; + +CREATE TABLE IF NOT EXISTS trashed_resources( + id TEXT NOT NULL PRIMARY KEY REFERENCES resources +); + + +selectTrash: SELECT r.* FROM trashed_resources t JOIN resources r ON t.id = r.id ORDER BY r.deleted DESC; diff --git a/client/lib/libphylum/explorer/page.dart b/client/lib/libphylum/explorer/page.dart index 8da616ca..01389181 100644 --- a/client/lib/libphylum/explorer/page.dart +++ b/client/lib/libphylum/explorer/page.dart @@ -106,7 +106,7 @@ class ExplorerPageTrash extends ExplorerPage { @override Future refresh(PhylumAccount account) { - return account.resourceRepository.requestDeletedResources().then((result) => result is ApiSuccessResponse); + return account.trashedResourcRepository.requestDeletedResources().then((result) => result is ApiSuccessResponse); } @override @@ -116,7 +116,7 @@ class ExplorerPageTrash extends ExplorerPage { @override Stream> watchChildren(PhylumAccount account) { - return account.db.selectDeleted().watch(); + return account.db.selectTrash().watch(); } @override diff --git a/client/lib/libphylum/phylum_account.dart b/client/lib/libphylum/phylum_account.dart index a441e3f9..f056b353 100644 --- a/client/lib/libphylum/phylum_account.dart +++ b/client/lib/libphylum/phylum_account.dart @@ -5,6 +5,7 @@ import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/repositories/bookmark_repository.dart'; import 'package:phylum/libphylum/repositories/shared_resources_repository.dart'; import 'package:phylum/libphylum/repositories/resource_repository.dart'; +import 'package:phylum/libphylum/repositories/trash_repository.dart'; import 'package:phylum/libphylum/repositories/user_repository.dart'; import 'package:phylum/libphylum/responses/responses.dart'; import 'package:phylum/util/logging.dart'; @@ -18,6 +19,7 @@ class PhylumAccount extends Account { late final db = AppDatabase(accountId: id); final resourceRepository = ResourceRepository(); final bookmarkRepository = BookmarkRepository(); + final trashedResourcRepository = TrashedResourceRepository(); late final userRepository = UserRepository(account: this); late final sharedResourcesRepository = SharedResourcesRepository(account: this); @@ -25,6 +27,7 @@ class PhylumAccount extends Account { Map> get repositories => { Resource: resourceRepository, Bookmark: bookmarkRepository, + TrashedResource: trashedResourcRepository, }; final _dispatcher = SimulatedBadNetworkDispatcher.good(dropRate: 0); diff --git a/client/lib/libphylum/repositories/resource_repository.dart b/client/lib/libphylum/repositories/resource_repository.dart index aa68d299..4bcda6ca 100644 --- a/client/lib/libphylum/repositories/resource_repository.dart +++ b/client/lib/libphylum/repositories/resource_repository.dart @@ -15,7 +15,6 @@ import 'package:phylum/libphylum/db/resource_helpers.dart'; import 'package:phylum/libphylum/name_conflict.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/requests/resource_info_request.dart'; -import 'package:phylum/libphylum/requests/trash_list_request.dart'; import 'package:phylum/libphylum/responses/responses.dart'; import 'package:phylum/libphylum/util/uuid.dart'; @@ -48,13 +47,6 @@ class ResourceRepository extends Repository { ); } - Future requestDeletedResources({String? cursor}) async { - return _account.apiClient.sendRequest( - TrashListRequest(cursor: cursor), - (request, response) => parseJsonMapResponse(response, TrashListResponse.fromResponse), - ); - } - @override Future persistRemoteData(Map remoteData) { return _remoteDataBox.putAll(remoteData); @@ -203,7 +195,7 @@ class ResourceRepository extends Repository { resourceName: name, conflictResolution: conflictResolution, description: parent == resource.parent ? 'Renaming ${resource.name} to $localName' : 'Moving ${resource.name}', - modified: DateTime.now(), + timestamp: DateTime.now(), localName: localName, oldParent: parent == resource.parent ? null : resource.parent, deletedId: deletedId, diff --git a/client/lib/libphylum/repositories/trash_repository.dart b/client/lib/libphylum/repositories/trash_repository.dart new file mode 100644 index 00000000..672d9b5d --- /dev/null +++ b/client/lib/libphylum/repositories/trash_repository.dart @@ -0,0 +1,58 @@ +import 'package:drift/drift.dart'; +import 'package:hive/hive.dart'; +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/libphylum/requests/trash_list_request.dart'; +import 'package:phylum/libphylum/responses/responses.dart'; + +const _remoteDataBoxName = 'remote_trashed_resources'; + +class TrashedResourceRepository extends Repository { + late final PhylumAccount _account; + late final LazyBox _remoteDataBox; + + TrashedResourceRepository(); + + @override + Future> initializeAndRestoreRemoteData(PhylumAccount account) async { + final box = await account.openBox(_remoteDataBoxName); + final remoteData = Map.fromIterable(box.keys, value: (key) => TrashedResource(id: key)); + await box.close(); + _account = account; + _remoteDataBox = await account.openLazyBox(_remoteDataBoxName); + return remoteData; + } + + @override + Future persistRemoteData(Map remoteData) { + return _remoteDataBox.putAll(remoteData.map((k, v) => MapEntry(k, k))); + } + + @override + Future discardRemoteData(Iterable objectIds) { + return _remoteDataBox.deleteAll(objectIds); + } + + @override + Future load(String objectId) async { + return (_account.db.trashedResources.select()..where((t) => t.id.equals(objectId))).getSingleOrNull(); + } + + @override + Future save(String objectId, TrashedResource data) { + return _account.db.trashedResources.insertOne(data, mode: InsertMode.insertOrReplace); + } + + @override + Future delete(String objectId) async { + await _account.db.trashedResources.deleteWhere((t) => t.id.equals(objectId)); + } + + Future requestDeletedResources({String? cursor}) async { + return _account.apiClient.sendRequest( + TrashListRequest(cursor: cursor), + (request, response) => parseJsonMapResponse(response, TrashListResponse.fromResponse), + ); + } +} diff --git a/client/lib/libphylum/responses/trash_list_response.dart b/client/lib/libphylum/responses/trash_list_response.dart index a2c2344e..34470b3e 100644 --- a/client/lib/libphylum/responses/trash_list_response.dart +++ b/client/lib/libphylum/responses/trash_list_response.dart @@ -3,25 +3,36 @@ part of 'responses.dart'; class TrashListResponse extends PhylumApiSuccessResponse { final Iterable resources; final String? cursor; + final bool clear; TrashListResponse({ required this.resources, required this.cursor, + required this.clear, }); factory TrashListResponse.fromResponse(Map data) { final resources = (data['resources'] as List).cast().map((a) => parseFullResource(a.cast())); final cursor = data['cursor'] as String; + final clear = data['clear'] as bool; return TrashListResponse( resources: resources, cursor: cursor.isEmpty ? null : cursor, + clear: clear, ); } @override Future process(PhylumAccount account) async { - account.db.resources.insertAll(resources, mode: InsertMode.insertOrReplace); + account.db.batch((batch) { + batch.insertAll(account.db.resources, resources, mode: InsertMode.insertOrReplace); + if (clear) { + batch.deleteAll(account.db.trashedResources); + } + batch.insertAll(account.db.trashedResources, resources.map((r) => TrashedResource(id: r.id)), + mode: InsertMode.insertOrReplace); + }); final repo = account.datastore.get(); for (final r in resources) { diff --git a/client/lib/ui/explorer/explorer_actions.dart b/client/lib/ui/explorer/explorer_actions.dart index af8d1fde..412d5371 100644 --- a/client/lib/ui/explorer/explorer_actions.dart +++ b/client/lib/ui/explorer/explorer_actions.dart @@ -116,7 +116,7 @@ class ExplorerActions extends StatelessWidget { if (!confirm) return; for (final r in resources) { - account.addAction(ResourceDeleteAction(r: r)); + account.addAction(ResourceDeleteAction(r: r, timestamp: DateTime.now())); } } } diff --git a/client/lib/ui/menu/menu_option.dart b/client/lib/ui/menu/menu_option.dart index c6588137..82a3f0e5 100644 --- a/client/lib/ui/menu/menu_option.dart +++ b/client/lib/ui/menu/menu_option.dart @@ -110,7 +110,7 @@ void handleOption(BuildContext context, Iterable resources, MenuOption await showAlertDialog(context, title: 'Delete $name?', positiveText: 'YES', negativeText: 'NO') ?? false; if (confirm) { for (final r in resources) { - account.addAction(ResourceDeleteAction(r: r)); + account.addAction(ResourceDeleteAction(r: r, timestamp: DateTime.now())); } } break;