diff --git a/client/lib/libphylum/db/db.dart b/client/lib/libphylum/db/db.dart index 0cc824da..4f265f29 100644 --- a/client/lib/libphylum/db/db.dart +++ b/client/lib/libphylum/db/db.dart @@ -13,7 +13,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase(String id) : super(_openConnection(id)); @override - int get schemaVersion => 3; + int get schemaVersion => 4; Future insertResources(Iterable> list) async { await batch((batch) { @@ -25,10 +25,8 @@ class AppDatabase extends _$AppDatabase { MigrationStrategy get migration => MigrationStrategy( onCreate: (m) => m.createAll(), onUpgrade: (m, from, to) async { - if (from == 1) { - await m.drop(resources); - await m.create(resources); - } + await m.drop(resources); + await m.createAll(); }); static QueryExecutor _openConnection(String id) { diff --git a/client/lib/libphylum/db/db.g.dart b/client/lib/libphylum/db/db.g.dart index 0dd977a5..4e522d47 100644 --- a/client/lib/libphylum/db/db.g.dart +++ b/client/lib/libphylum/db/db.g.dart @@ -61,9 +61,15 @@ class $ResourcesTable extends Resources defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _lastFetchMeta = + const VerificationMeta('lastFetch'); + @override + late final GeneratedColumn lastFetch = GeneratedColumn( + 'last_fetch', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); @override List get $columns => - [id, parent, name, dir, modified, size, etag, deleted]; + [id, parent, name, dir, modified, size, etag, deleted, lastFetch]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -117,6 +123,10 @@ class $ResourcesTable extends Resources context.handle(_deletedMeta, deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta)); } + if (data.containsKey('last_fetch')) { + context.handle(_lastFetchMeta, + lastFetch.isAcceptableOrUnknown(data['last_fetch']!, _lastFetchMeta)); + } return context; } @@ -146,6 +156,8 @@ class $ResourcesTable extends Resources .read(DriftSqlType.string, data['${effectivePrefix}etag'])!, deleted: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}deleted'])!, + lastFetch: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_fetch']), ); } @@ -164,6 +176,7 @@ class Resource extends DataClass implements Insertable { final int size; final String etag; final bool deleted; + final DateTime? lastFetch; const Resource( {required this.id, this.parent, @@ -172,7 +185,8 @@ class Resource extends DataClass implements Insertable { required this.modified, required this.size, required this.etag, - required this.deleted}); + required this.deleted, + this.lastFetch}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -186,6 +200,9 @@ class Resource extends DataClass implements Insertable { map['size'] = Variable(size); map['etag'] = Variable(etag); map['deleted'] = Variable(deleted); + if (!nullToAbsent || lastFetch != null) { + map['last_fetch'] = Variable(lastFetch); + } return map; } @@ -200,6 +217,9 @@ class Resource extends DataClass implements Insertable { size: Value(size), etag: Value(etag), deleted: Value(deleted), + lastFetch: lastFetch == null && nullToAbsent + ? const Value.absent() + : Value(lastFetch), ); } @@ -215,6 +235,7 @@ class Resource extends DataClass implements Insertable { size: serializer.fromJson(json['size']), etag: serializer.fromJson(json['etag']), deleted: serializer.fromJson(json['deleted']), + lastFetch: serializer.fromJson(json['lastFetch']), ); } @override @@ -229,6 +250,7 @@ class Resource extends DataClass implements Insertable { 'size': serializer.toJson(size), 'etag': serializer.toJson(etag), 'deleted': serializer.toJson(deleted), + 'lastFetch': serializer.toJson(lastFetch), }; } @@ -240,7 +262,8 @@ class Resource extends DataClass implements Insertable { DateTime? modified, int? size, String? etag, - bool? deleted}) => + bool? deleted, + Value lastFetch = const Value.absent()}) => Resource( id: id ?? this.id, parent: parent.present ? parent.value : this.parent, @@ -250,6 +273,7 @@ class Resource extends DataClass implements Insertable { size: size ?? this.size, etag: etag ?? this.etag, deleted: deleted ?? this.deleted, + lastFetch: lastFetch.present ? lastFetch.value : this.lastFetch, ); Resource copyWithCompanion(ResourcesCompanion data) { return Resource( @@ -261,6 +285,7 @@ class Resource extends DataClass implements Insertable { size: data.size.present ? data.size.value : this.size, etag: data.etag.present ? data.etag.value : this.etag, deleted: data.deleted.present ? data.deleted.value : this.deleted, + lastFetch: data.lastFetch.present ? data.lastFetch.value : this.lastFetch, ); } @@ -274,14 +299,15 @@ class Resource extends DataClass implements Insertable { ..write('modified: $modified, ') ..write('size: $size, ') ..write('etag: $etag, ') - ..write('deleted: $deleted') + ..write('deleted: $deleted, ') + ..write('lastFetch: $lastFetch') ..write(')')) .toString(); } @override - int get hashCode => - Object.hash(id, parent, name, dir, modified, size, etag, deleted); + int get hashCode => Object.hash( + id, parent, name, dir, modified, size, etag, deleted, lastFetch); @override bool operator ==(Object other) => identical(this, other) || @@ -293,7 +319,8 @@ class Resource extends DataClass implements Insertable { other.modified == this.modified && other.size == this.size && other.etag == this.etag && - other.deleted == this.deleted); + other.deleted == this.deleted && + other.lastFetch == this.lastFetch); } class ResourcesCompanion extends UpdateCompanion { @@ -305,6 +332,7 @@ class ResourcesCompanion extends UpdateCompanion { final Value size; final Value etag; final Value deleted; + final Value lastFetch; final Value rowid; const ResourcesCompanion({ this.id = const Value.absent(), @@ -315,6 +343,7 @@ class ResourcesCompanion extends UpdateCompanion { this.size = const Value.absent(), this.etag = const Value.absent(), this.deleted = const Value.absent(), + this.lastFetch = const Value.absent(), this.rowid = const Value.absent(), }); ResourcesCompanion.insert({ @@ -326,6 +355,7 @@ class ResourcesCompanion extends UpdateCompanion { required int size, required String etag, this.deleted = const Value.absent(), + this.lastFetch = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), @@ -342,6 +372,7 @@ class ResourcesCompanion extends UpdateCompanion { Expression? size, Expression? etag, Expression? deleted, + Expression? lastFetch, Expression? rowid, }) { return RawValuesInsertable({ @@ -353,6 +384,7 @@ class ResourcesCompanion extends UpdateCompanion { if (size != null) 'size': size, if (etag != null) 'etag': etag, if (deleted != null) 'deleted': deleted, + if (lastFetch != null) 'last_fetch': lastFetch, if (rowid != null) 'rowid': rowid, }); } @@ -366,6 +398,7 @@ class ResourcesCompanion extends UpdateCompanion { Value? size, Value? etag, Value? deleted, + Value? lastFetch, Value? rowid}) { return ResourcesCompanion( id: id ?? this.id, @@ -376,6 +409,7 @@ class ResourcesCompanion extends UpdateCompanion { size: size ?? this.size, etag: etag ?? this.etag, deleted: deleted ?? this.deleted, + lastFetch: lastFetch ?? this.lastFetch, rowid: rowid ?? this.rowid, ); } @@ -407,6 +441,9 @@ class ResourcesCompanion extends UpdateCompanion { if (deleted.present) { map['deleted'] = Variable(deleted.value); } + if (lastFetch.present) { + map['last_fetch'] = Variable(lastFetch.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -424,6 +461,7 @@ class ResourcesCompanion extends UpdateCompanion { ..write('size: $size, ') ..write('etag: $etag, ') ..write('deleted: $deleted, ') + ..write('lastFetch: $lastFetch, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -450,6 +488,7 @@ typedef $$ResourcesTableCreateCompanionBuilder = ResourcesCompanion Function({ required int size, required String etag, Value deleted, + Value lastFetch, Value rowid, }); typedef $$ResourcesTableUpdateCompanionBuilder = ResourcesCompanion Function({ @@ -461,6 +500,7 @@ typedef $$ResourcesTableUpdateCompanionBuilder = ResourcesCompanion Function({ Value size, Value etag, Value deleted, + Value lastFetch, Value rowid, }); @@ -489,6 +529,7 @@ class $$ResourcesTableTableManager extends RootTableManager< Value size = const Value.absent(), Value etag = const Value.absent(), Value deleted = const Value.absent(), + Value lastFetch = const Value.absent(), Value rowid = const Value.absent(), }) => ResourcesCompanion( @@ -500,6 +541,7 @@ class $$ResourcesTableTableManager extends RootTableManager< size: size, etag: etag, deleted: deleted, + lastFetch: lastFetch, rowid: rowid, ), createCompanionCallback: ({ @@ -511,6 +553,7 @@ class $$ResourcesTableTableManager extends RootTableManager< required int size, required String etag, Value deleted = const Value.absent(), + Value lastFetch = const Value.absent(), Value rowid = const Value.absent(), }) => ResourcesCompanion.insert( @@ -522,6 +565,7 @@ class $$ResourcesTableTableManager extends RootTableManager< size: size, etag: etag, deleted: deleted, + lastFetch: lastFetch, rowid: rowid, ), )); @@ -565,6 +609,11 @@ class $$ResourcesTableFilterComposer builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); + ColumnFilters get lastFetch => $state.composableBuilder( + column: $state.table.lastFetch, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + $$ResourcesTableFilterComposer get parent { final $$ResourcesTableFilterComposer composer = $state.composerBuilder( composer: this, @@ -616,6 +665,11 @@ class $$ResourcesTableOrderingComposer builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); + ColumnOrderings get lastFetch => $state.composableBuilder( + column: $state.table.lastFetch, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + $$ResourcesTableOrderingComposer get parent { final $$ResourcesTableOrderingComposer composer = $state.composerBuilder( composer: this, diff --git a/client/lib/libphylum/db/resources.dart b/client/lib/libphylum/db/resources.dart index f1c81437..c6cfb6c0 100644 --- a/client/lib/libphylum/db/resources.dart +++ b/client/lib/libphylum/db/resources.dart @@ -9,6 +9,7 @@ class Resources extends Table { IntColumn get size => integer()(); TextColumn get etag => text()(); BoolColumn get deleted => boolean().withDefault(const Constant(false))(); + DateTimeColumn get lastFetch => dateTime().nullable()(); @override Set get primaryKey => {id}; diff --git a/client/lib/libphylum/phylum_datastore.dart b/client/lib/libphylum/phylum_datastore.dart index cee6c069..568d2e99 100644 --- a/client/lib/libphylum/phylum_datastore.dart +++ b/client/lib/libphylum/phylum_datastore.dart @@ -28,7 +28,7 @@ class PhylumDatastore with AccountListener> { } Future parseResourceDetails(Map data) async { - final details = parseResourceSummary(data['metadata']); + final details = parseResourceSummary(data['metadata']).copyWith(lastFetch: Value(DateTime.now())); final existing = Set.from(await db.managers.resources.filter((f) => f.parent.id.equals(data['metadata']['id'])).map((r) => r.id).get()); final children = data.containsKey('children') ? (data['children'] as List).cast().map((c) { diff --git a/client/lib/ui/folder/folder_contents_view.dart b/client/lib/ui/folder/folder_contents_view.dart index 075faee9..0d3faeb2 100644 --- a/client/lib/ui/folder/folder_contents_view.dart +++ b/client/lib/ui/folder/folder_contents_view.dart @@ -70,8 +70,6 @@ class _FolderContentsViewState extends State { Widget build(BuildContext context) { return Actions( actions: { - NextFocusIntent: CallbackAction(onInvoke: (i) => null), - PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), FocusUpIntent: CallbackAction(onInvoke: (i) { if (resources.isEmpty) return; final index = focusIndex == -1 @@ -104,10 +102,6 @@ class _FolderContentsViewState extends State { } return null; }), - NavUpIntent: CallbackAction(onInvoke: (i) { - context.read().pop(); - return null; - }), ActivateIntent: CallbackAction(onInvoke: (i) { open(resources[focusIndex]); return null; diff --git a/client/lib/ui/folder/folder_view.dart b/client/lib/ui/folder/folder_view.dart index 200be260..dce0cd5d 100644 --- a/client/lib/ui/folder/folder_view.dart +++ b/client/lib/ui/folder/folder_view.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_state_notifier/flutter_state_notifier.dart'; +import 'package:phylum/app_shortcuts.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/requests/resource_detail_request.dart'; import 'package:phylum/ui/folder/folder_contents_view.dart'; +import 'package:phylum/ui/folder/folder_navigator_stack.dart'; import 'package:phylum/ui/folder/folder_selection_manager.dart'; import 'package:provider/provider.dart'; @@ -31,28 +33,72 @@ class _FolderViewState extends State { final account = context.read(); final theme = Theme.of(context); - return ListTileTheme( - data: ListTileThemeData( - mouseCursor: const WidgetStatePropertyAll(SystemMouseCursors.basic), - selectedTileColor: theme.colorScheme.primaryContainer, - ), - child: RefreshIndicator( - onRefresh: _refresh, + return Actions( + actions: { + NextFocusIntent: CallbackAction(onInvoke: (i) => null), + PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), + NavUpIntent: CallbackAction(onInvoke: (i) { + context.read().pop(); + return null; + }), + }, + child: ListTileTheme( + data: ListTileThemeData( + mouseCursor: const WidgetStatePropertyAll(SystemMouseCursors.basic), + selectedTileColor: theme.colorScheme.primaryContainer, + ), child: StateNotifierProvider( create: (context) => FolderSelectionManager(), child: StreamBuilder( - stream: account.datastore.db.managers.resources - .filter((f) => f.parent.id.equals(widget.id) & f.deleted.equals(false)) - .orderBy((o) => o.dir.desc() & o.name.asc()) - .watch(), + stream: account.datastore.db.managers.resources.filter((f) => f.id.equals(widget.id)).watchSingleOrNull(), builder: (context, snapshot) { - if (!snapshot.hasData) { - return Container(); + final folder = snapshot.data; + if (folder?.lastFetch == null) { + return buildEmptyView('Loading'); } - return FolderContentsView(folderId: widget.id, resources: snapshot.data!); + return StreamBuilder( + stream: account.datastore.db.managers.resources + .filter((f) => f.parent.id.equals(widget.id) & f.deleted.equals(false)) + .orderBy((o) => o.dir.desc() & o.name.asc()) + .watch(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + if (snapshot.data!.isEmpty) { + return buildEmptyView('Nothing here'); + } + return RefreshIndicator( + onRefresh: _refresh, + child: FolderContentsView( + folderId: widget.id, + resources: snapshot.data!, + )); + }); }), ), ), ); } + + Widget buildEmptyView(String text) { + return Focus( + autofocus: true, + child: LayoutBuilder( + builder: (context, constraints) => RefreshIndicator( + onRefresh: _refresh, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + minHeight: constraints.maxHeight, + ), + child: Center(child: Text(text)), + ), + ), + ), + ), + ); + } }