mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-05-07 04:39:25 -05:00
[client] Show loading and empty states
This commit is contained in:
@@ -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,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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user