[client] Show loading and empty states

This commit is contained in:
Abhishek Shroff
2024-09-07 01:15:18 +05:30
parent d522509f31
commit 9d27030775
6 changed files with 126 additions and 33 deletions
+3 -5
View File
@@ -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<Insertable<Resource>> 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) {
+61 -7
View File
@@ -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<DateTime> lastFetch = GeneratedColumn<DateTime>(
'last_fetch', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
@override
List<GeneratedColumn> 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<Resource> {
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<Resource> {
required this.modified,
required this.size,
required this.etag,
required this.deleted});
required this.deleted,
this.lastFetch});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@@ -186,6 +200,9 @@ class Resource extends DataClass implements Insertable<Resource> {
map['size'] = Variable<int>(size);
map['etag'] = Variable<String>(etag);
map['deleted'] = Variable<bool>(deleted);
if (!nullToAbsent || lastFetch != null) {
map['last_fetch'] = Variable<DateTime>(lastFetch);
}
return map;
}
@@ -200,6 +217,9 @@ class Resource extends DataClass implements Insertable<Resource> {
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<Resource> {
size: serializer.fromJson<int>(json['size']),
etag: serializer.fromJson<String>(json['etag']),
deleted: serializer.fromJson<bool>(json['deleted']),
lastFetch: serializer.fromJson<DateTime?>(json['lastFetch']),
);
}
@override
@@ -229,6 +250,7 @@ class Resource extends DataClass implements Insertable<Resource> {
'size': serializer.toJson<int>(size),
'etag': serializer.toJson<String>(etag),
'deleted': serializer.toJson<bool>(deleted),
'lastFetch': serializer.toJson<DateTime?>(lastFetch),
};
}
@@ -240,7 +262,8 @@ class Resource extends DataClass implements Insertable<Resource> {
DateTime? modified,
int? size,
String? etag,
bool? deleted}) =>
bool? deleted,
Value<DateTime?> 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<Resource> {
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<Resource> {
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<Resource> {
..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<Resource> {
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<Resource> {
@@ -305,6 +332,7 @@ class ResourcesCompanion extends UpdateCompanion<Resource> {
final Value<int> size;
final Value<String> etag;
final Value<bool> deleted;
final Value<DateTime?> lastFetch;
final Value<int> rowid;
const ResourcesCompanion({
this.id = const Value.absent(),
@@ -315,6 +343,7 @@ class ResourcesCompanion extends UpdateCompanion<Resource> {
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<Resource> {
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<Resource> {
Expression<int>? size,
Expression<String>? etag,
Expression<bool>? deleted,
Expression<DateTime>? lastFetch,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
@@ -353,6 +384,7 @@ class ResourcesCompanion extends UpdateCompanion<Resource> {
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<Resource> {
Value<int>? size,
Value<String>? etag,
Value<bool>? deleted,
Value<DateTime?>? lastFetch,
Value<int>? rowid}) {
return ResourcesCompanion(
id: id ?? this.id,
@@ -376,6 +409,7 @@ class ResourcesCompanion extends UpdateCompanion<Resource> {
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<Resource> {
if (deleted.present) {
map['deleted'] = Variable<bool>(deleted.value);
}
if (lastFetch.present) {
map['last_fetch'] = Variable<DateTime>(lastFetch.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -424,6 +461,7 @@ class ResourcesCompanion extends UpdateCompanion<Resource> {
..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<bool> deleted,
Value<DateTime?> lastFetch,
Value<int> rowid,
});
typedef $$ResourcesTableUpdateCompanionBuilder = ResourcesCompanion Function({
@@ -461,6 +500,7 @@ typedef $$ResourcesTableUpdateCompanionBuilder = ResourcesCompanion Function({
Value<int> size,
Value<String> etag,
Value<bool> deleted,
Value<DateTime?> lastFetch,
Value<int> rowid,
});
@@ -489,6 +529,7 @@ class $$ResourcesTableTableManager extends RootTableManager<
Value<int> size = const Value.absent(),
Value<String> etag = const Value.absent(),
Value<bool> deleted = const Value.absent(),
Value<DateTime?> lastFetch = const Value.absent(),
Value<int> 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<bool> deleted = const Value.absent(),
Value<DateTime?> lastFetch = const Value.absent(),
Value<int> 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<DateTime> 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<DateTime> 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,
+1
View File
@@ -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<Column> get primaryKey => {id};
+1 -1
View File
@@ -28,7 +28,7 @@ class PhylumDatastore with AccountListener<Map<String, dynamic>> {
}
Future parseResourceDetails(Map<String, dynamic> 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>().map((c) {
@@ -70,8 +70,6 @@ class _FolderContentsViewState extends State<FolderContentsView> {
Widget build(BuildContext context) {
return Actions(
actions: {
NextFocusIntent: CallbackAction<NextFocusIntent>(onInvoke: (i) => null),
PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(onInvoke: (i) => null),
FocusUpIntent: CallbackAction<FocusUpIntent>(onInvoke: (i) {
if (resources.isEmpty) return;
final index = focusIndex == -1
@@ -104,10 +102,6 @@ class _FolderContentsViewState extends State<FolderContentsView> {
}
return null;
}),
NavUpIntent: CallbackAction<NavUpIntent>(onInvoke: (i) {
context.read<FolderNavigatorStack>().pop();
return null;
}),
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: (i) {
open(resources[focusIndex]);
return null;
+60 -14
View File
@@ -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<FolderView> {
final account = context.read<PhylumAccount>();
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<NextFocusIntent>(onInvoke: (i) => null),
PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(onInvoke: (i) => null),
NavUpIntent: CallbackAction<NavUpIntent>(onInvoke: (i) {
context.read<FolderNavigatorStack>().pop();
return null;
}),
},
child: ListTileTheme(
data: ListTileThemeData(
mouseCursor: const WidgetStatePropertyAll(SystemMouseCursors.basic),
selectedTileColor: theme.colorScheme.primaryContainer,
),
child: StateNotifierProvider<FolderSelectionManager, FolderSelectionState>(
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)),
),
),
),
),
);
}
}