Skip to content

Commit

Permalink
Merge pull request #1570 from nextcloud/refactor/neon_dashboard/cache…
Browse files Browse the repository at this point in the history
…-widgets-items
  • Loading branch information
provokateurin authored Feb 9, 2024
2 parents 7850338 + 5ec2b50 commit efa25f6
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 155 deletions.
176 changes: 101 additions & 75 deletions packages/neon/neon_dashboard/lib/src/blocs/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'dart:async';
import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/dashboard.dart' as dashboard;
import 'package:rxdart/rxdart.dart';

Expand All @@ -16,112 +16,138 @@ sealed class DashboardBloc implements InteractiveBloc {
factory DashboardBloc(Account account) => _DashboardBloc(account);

/// Dashboard widgets that are displayed.
BehaviorSubject<Result<Map<dashboard.Widget, dashboard.WidgetItems?>>> get widgets;
BehaviorSubject<Result<BuiltList<dashboard.Widget>>> get widgets;

/// Dashboard items that are displayed.
BehaviorSubject<Result<BuiltMap<String, dashboard.WidgetItems>>> get items;
}

/// Implementation of [DashboardBloc].
///
/// Automatically starts fetching the widgets and their items and refreshes everything every 30 seconds.
class _DashboardBloc extends InteractiveBloc implements DashboardBloc {
_DashboardBloc(this.account) {
itemsV1.listen((_) => _updateItems());
itemsV2.listen((_) => _updateItems());

widgets.listen((result) async {
if (!result.hasData) {
return;
}

final v1WidgetIDs = ListBuilder<String>();
final v2WidgetIDs = ListBuilder<String>();

for (final widget in result.requireData) {
final itemApiVersions = widget.itemApiVersions;
if (itemApiVersions != null && itemApiVersions.contains(2)) {
v2WidgetIDs.add(widget.id);
} else {
v1WidgetIDs.add(widget.id);
}
}

await Future.wait<void>([
if (v1WidgetIDs.isNotEmpty)
RequestManager.instance.wrapNextcloud(
account: account,
cacheKey: 'dashboard-widgets-v1',
subject: itemsV1,
rawResponse: account.client.dashboard.dashboardApi.getWidgetItemsRaw(
widgets: v1WidgetIDs.build(),
limit: maxItems,
),
unwrap: (response) => response.body.ocs.data,
),
if (v2WidgetIDs.isNotEmpty)
RequestManager.instance.wrapNextcloud(
account: account,
cacheKey: 'dashboard-widgets-v2',
subject: itemsV2,
rawResponse: account.client.dashboard.dashboardApi.getWidgetItemsV2Raw(
widgets: v2WidgetIDs.build(),
limit: maxItems,
),
unwrap: (response) => response.body.ocs.data,
),
]);
});

unawaited(refresh());

timer = TimerBloc().registerTimer(const Duration(seconds: 30), refresh);
}

final Account account;
late final NeonTimer timer;
final itemsV1 = BehaviorSubject<Result<BuiltMap<String, BuiltList<dashboard.WidgetItem>>>>();
final itemsV2 = BehaviorSubject<Result<BuiltMap<String, dashboard.WidgetItems>>>();
static const int maxItems = 7;

@override
BehaviorSubject<Result<Map<dashboard.Widget, dashboard.WidgetItems?>>> widgets = BehaviorSubject();
final widgets = BehaviorSubject<Result<BuiltList<dashboard.Widget>>>();

@override
final items = BehaviorSubject<Result<BuiltMap<String, dashboard.WidgetItems>>>();

@override
void dispose() {
timer.cancel();
unawaited(itemsV1.close());
unawaited(itemsV2.close());
unawaited(widgets.close());
unawaited(items.close());
super.dispose();
}

@override
Future<void> refresh() async {
widgets.add(widgets.valueOrNull?.asLoading() ?? Result.loading());

try {
final widgets = <String, dashboard.WidgetItems?>{};
final v1WidgetIDs = ListBuilder<String>();
final v2WidgetIDs = ListBuilder<String>();

final response = await account.client.dashboard.dashboardApi.getWidgets();

for (final widget in response.body.ocs.data.values) {
final itemApiVersions = widget.itemApiVersions;
if (itemApiVersions != null && itemApiVersions.contains(2)) {
v2WidgetIDs.add(widget.id);
} else if (itemApiVersions == null || itemApiVersions.contains(1)) {
// If the field isn't present the server only supports v1
v1WidgetIDs.add(widget.id);
} else {
debugPrint('Widget supports none of the API versions: ${widget.id}');
}
}

if (v1WidgetIDs.isNotEmpty) {
final widgetsIDs = v1WidgetIDs.build();
debugPrint('Loading v1 widgets: ${widgetsIDs.join(', ')}');
await RequestManager.instance.wrapNextcloud(
account: account,
cacheKey: 'dashboard-widgets',
subject: widgets,
rawResponse: account.client.dashboard.dashboardApi.getWidgetsRaw(),
// Filter all widgets that don't support v1 nor v2
unwrap: (response) => BuiltList(
response.body.ocs.data.values
.where((widget) => widget.itemApiVersions == null || widget.itemApiVersions!.isNotEmpty),
),
);
}

final response = await account.client.dashboard.dashboardApi.getWidgetItems(
widgets: widgetsIDs,
limit: maxItems,
void _updateItems() {
final data = MapBuilder<String, dashboard.WidgetItems>();

final resultV1 = itemsV1.valueOrNull;
if (resultV1 != null && resultV1.hasData) {
for (final entry in resultV1.requireData.entries) {
data[entry.key] = dashboard.WidgetItems(
(b) => b
..items.replace(entry.value.sublist(0, min(entry.value.length, maxItems)))
..emptyContentMessage = ''
..halfEmptyContentMessage = '',
);
for (final entry in response.body.ocs.data.entries) {
widgets[entry.key] = dashboard.WidgetItems(
(b) => b
..items = entry.value.sublist(0, min(entry.value.length, maxItems)).toBuilder()
..emptyContentMessage = ''
..halfEmptyContentMessage = '',
);
}
}
}

if (v2WidgetIDs.isNotEmpty) {
final widgetsIDs = v2WidgetIDs.build();
debugPrint('Loading v2 widgets: ${widgetsIDs.join(', ')}');

final response = await account.client.dashboard.dashboardApi.getWidgetItemsV2(
widgets: widgetsIDs,
limit: maxItems,
);
widgets.addEntries(
response.body.ocs.data.entries.map(
(entry) => MapEntry(
entry.key,
entry.value.rebuild((b) {
if (b.items.length > maxItems) {
b.items.removeRange(maxItems, b.items.length);
}
}),
),
),
);
final resultV2 = itemsV2.valueOrNull;
if (resultV2 != null && resultV2.hasData) {
for (final entry in resultV2.requireData.entries) {
data[entry.key] = entry.value.rebuild((b) {
if (b.items.length > maxItems) {
b.items.removeRange(maxItems, b.items.length);
}
});
}

this.widgets.add(
Result.success(
widgets.map(
(id, items) => MapEntry(
response.body.ocs.data.values.firstWhere((widget) => widget.id == id),
items,
),
),
),
);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());

widgets.add(Result.error(e));
return;
}

items.add(
Result(
data.build(),
resultV1?.error ?? resultV2?.error,
isLoading: (resultV1?.isLoading ?? true) || (resultV2?.isLoading ?? true),
isCached: (resultV1?.isCached ?? true) || (resultV2?.isCached ?? true),
),
);
}
}
139 changes: 71 additions & 68 deletions packages/neon/neon_dashboard/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,83 +34,86 @@ class DashboardMainPage extends StatelessWidget {
return NeonCustomBackground(
child: ResultBuilder.behaviorSubject(
subject: bloc.widgets,
builder: (context, result) {
final children = <Widget>[
_buildStatuses(
account: accountsBloc.activeAccount.value!,
userStatusBloc: userStatusBloc,
weatherStatusBloc: weatherStatusBloc,
),
];
builder: (context, widgetsResult) => ResultBuilder.behaviorSubject(
subject: bloc.items,
builder: (context, itemsResult) {
final children = <Widget>[
_buildStatuses(
account: accountsBloc.activeAccount.value!,
userStatusBloc: userStatusBloc,
weatherStatusBloc: weatherStatusBloc,
),
];

var minHeight = 504.0;
if (result.hasData) {
final widgets = <Widget>[];
for (final widget in result.requireData.entries) {
final items = buildWidgetItems(
context: context,
widget: widget.key,
items: widget.value,
).toList();
var minHeight = 504.0;
if (widgetsResult.hasData) {
final widgets = <Widget>[];
for (final widget in widgetsResult.requireData) {
final items = buildWidgetItems(
context: context,
widget: widget,
items: itemsResult.data?[widget.id],
).toList();

final height = items.map((i) => i.height!).reduce((a, b) => a + b);
minHeight = max(minHeight, height);
final height = items.map((i) => i.height!).reduce((a, b) => a + b);
minHeight = max(minHeight, height);

widgets.add(
DashboardWidget(
widget: widget.key,
children: items,
),
);
}

children.add(
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: widgets
.map(
(widget) => SizedBox(
width: 320,
height: minHeight + 24,
child: widget,
),
)
.toList(),
),
);
} else {
children.add(
SizedBox(
height: minHeight,
),
);
}
widgets.add(
DashboardWidget(
widget: widget,
children: items,
),
);
}

return Center(
child: NeonListView.custom(
scrollKey: 'dashboard',
isLoading: result.isLoading,
error: result.error,
onRefresh: bloc.refresh,
sliver: SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: children
.intersperse(
const SizedBox(
height: 40,
children.add(
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: widgets
.map(
(widget) => SizedBox(
width: 320,
height: minHeight + 24,
child: widget,
),
)
.toList(),
),
);
} else {
children.add(
SizedBox(
height: minHeight,
),
);
}

return Center(
child: NeonListView.custom(
scrollKey: 'dashboard',
isLoading: widgetsResult.isLoading || itemsResult.isLoading,
error: widgetsResult.error ?? itemsResult.error,
onRefresh: bloc.refresh,
sliver: SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: children
.intersperse(
const SizedBox(
height: 40,
),
)
.toList(),
),
),
),
),
);
},
);
},
),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/neon/neon_dashboard/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
go_router_builder: ^2.4.1
http: ^1.2.0
mocktail: ^1.0.3
neon_lints:
git:
Expand Down
Loading

0 comments on commit efa25f6

Please sign in to comment.