Compare commits

...

1 Commits

Author SHA1 Message Date
jld3103 ea0f008dcf
fix(neon_dashboard): Remove scrolling inside dashboard widgets 1 year ago
  1. 134
      packages/neon/neon_dashboard/lib/src/pages/main.dart
  2. 33
      packages/neon/neon_dashboard/lib/src/widgets/dry_intrinsic_height.dart
  3. 81
      packages/neon/neon_dashboard/lib/src/widgets/widget.dart
  4. BIN
      packages/neon/neon_dashboard/test/goldens/widget.png
  5. BIN
      packages/neon/neon_dashboard/test/goldens/widget_not_round.png
  6. BIN
      packages/neon/neon_dashboard/test/goldens/widget_with_empty.png
  7. BIN
      packages/neon/neon_dashboard/test/goldens/widget_with_half_empty.png
  8. BIN
      packages/neon/neon_dashboard/test/goldens/widget_without_buttons.png
  9. BIN
      packages/neon/neon_dashboard/test/goldens/widget_without_items.png
  10. 68
      packages/neon/neon_dashboard/test/widget_test.dart

134
packages/neon/neon_dashboard/lib/src/pages/main.dart

@ -1,9 +1,17 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:neon/blocs.dart'; import 'package:neon/blocs.dart';
import 'package:neon/theme.dart';
import 'package:neon/utils.dart'; import 'package:neon/utils.dart';
import 'package:neon/widgets.dart'; import 'package:neon/widgets.dart';
import 'package:neon_dashboard/l10n/localizations.dart';
import 'package:neon_dashboard/src/blocs/dashboard.dart'; import 'package:neon_dashboard/src/blocs/dashboard.dart';
import 'package:neon_dashboard/src/widgets/dry_intrinsic_height.dart';
import 'package:neon_dashboard/src/widgets/widget.dart'; import 'package:neon_dashboard/src/widgets/widget.dart';
import 'package:neon_dashboard/src/widgets/widget_button.dart';
import 'package:neon_dashboard/src/widgets/widget_item.dart';
import 'package:nextcloud/dashboard.dart' as dashboard;
/// Displays the whole dashboard page layout. /// Displays the whole dashboard page layout.
class DashboardMainPage extends StatelessWidget { class DashboardMainPage extends StatelessWidget {
@ -21,15 +29,37 @@ class DashboardMainPage extends StatelessWidget {
builder: (final context, final snapshot) { builder: (final context, final snapshot) {
Widget? child; Widget? child;
if (snapshot.hasData) { if (snapshot.hasData) {
var minHeight = 504.0;
final children = <Widget>[];
for (final widget in snapshot.requireData.entries) {
final items = buildWidgetItems(
context: context,
widget: widget.key,
items: widget.value,
).toList();
final height = items.map((final i) => i.height!).reduce((final a, final b) => a + b);
minHeight = max(minHeight, height);
children.add(
DashboardWidget(
widget: widget.key,
children: items,
),
);
}
child = Wrap( child = Wrap(
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: snapshot.requireData.entries children: children
.map( .map(
(final widget) => DashboardWidget( (final widget) => SizedBox(
widget: widget.key, width: 320,
items: widget.value, height: minHeight + 24,
child: widget,
), ),
) )
.toList(), .toList(),
@ -53,4 +83,100 @@ class DashboardMainPage extends StatelessWidget {
}, },
); );
} }
/// Builds the list of messages, [items] and buttons for a [widget].
@visibleForTesting
static Iterable<SizedBox> buildWidgetItems({
required final BuildContext context,
required final dashboard.Widget widget,
required final dashboard.WidgetItems? items,
}) sync* {
yield SizedBox(
height: 64,
child: DryIntrinsicHeight(
child: ListTile(
title: Text(
widget.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
leading: SizedBox.square(
dimension: largeIconSize,
child: NeonUrlImage(
url: widget.iconUrl,
svgColorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
size: const Size.square(largeIconSize),
),
),
),
),
);
yield const SizedBox(
height: 20,
);
final halfEmptyContentMessage = _buildMessage(items?.halfEmptyContentMessage);
final emptyContentMessage = _buildMessage(items?.emptyContentMessage);
if (halfEmptyContentMessage != null) {
yield halfEmptyContentMessage;
}
if (emptyContentMessage != null) {
yield emptyContentMessage;
}
if (halfEmptyContentMessage == null && emptyContentMessage == null && (items?.items.isEmpty ?? true)) {
yield _buildMessage(DashboardLocalizations.of(context).noEntries)!;
}
if (items?.items != null) {
for (final item in items!.items) {
yield SizedBox(
height: 64,
child: DryIntrinsicHeight(
child: DashboardWidgetItem(
item: item,
roundIcon: widget.itemIconsRound,
),
),
);
}
}
yield const SizedBox(
height: 20,
);
if (widget.buttons != null) {
for (final button in widget.buttons!) {
yield SizedBox(
height: 32,
child: DashboardWidgetButton(
button: button,
),
);
}
}
}
static SizedBox? _buildMessage(final String? message) {
if (message == null || message.isEmpty) {
return null;
}
return SizedBox(
height: 60,
child: Center(
child: Column(
children: [
const Icon(
Icons.check,
size: largeIconSize,
),
Text(message),
],
),
),
);
}
} }

33
packages/neon/neon_dashboard/lib/src/widgets/dry_intrinsic_height.dart

@ -0,0 +1,33 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// NOTE ref: https://github.com/flutter/flutter/issues/71687 & https://gist.github.com/matthew-carroll/65411529a5fafa1b527a25b7130187c6
/// Same as `IntrinsicHeight` except that when this widget is instructed
/// to `computeDryLayout()`, it doesn't invoke that on its child, instead
/// it computes the child's intrinsic height.
///
/// This widget is useful in situations where the `child` does not
/// support dry layout, e.g., `TextField` as of 01/02/2021.
class DryIntrinsicHeight extends SingleChildRenderObjectWidget {
const DryIntrinsicHeight({
super.key,
super.child,
});
@override
RenderDryIntrinsicHeight createRenderObject(final BuildContext context) => RenderDryIntrinsicHeight();
}
class RenderDryIntrinsicHeight extends RenderIntrinsicHeight {
@override
Size computeDryLayout(final BoxConstraints constraints) {
if (child != null) {
final height = child!.computeMinIntrinsicHeight(constraints.maxWidth);
final width = child!.computeMinIntrinsicWidth(height);
return Size(width, height);
} else {
return Size.zero;
}
}
}

81
packages/neon/neon_dashboard/lib/src/widgets/widget.dart

@ -1,10 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:neon/theme.dart';
import 'package:neon/widgets.dart';
import 'package:neon_dashboard/l10n/localizations.dart';
import 'package:neon_dashboard/src/widgets/widget_button.dart';
import 'package:neon_dashboard/src/widgets/widget_item.dart';
import 'package:nextcloud/dashboard.dart' as dashboard; import 'package:nextcloud/dashboard.dart' as dashboard;
/// Displays a single dashboard widget and its items. /// Displays a single dashboard widget and its items.
@ -12,7 +7,7 @@ class DashboardWidget extends StatelessWidget {
/// Creates a new dashboard widget items. /// Creates a new dashboard widget items.
const DashboardWidget({ const DashboardWidget({
required this.widget, required this.widget,
required this.items, required this.children,
super.key, super.key,
}); });
@ -20,83 +15,19 @@ class DashboardWidget extends StatelessWidget {
final dashboard.Widget widget; final dashboard.Widget widget;
/// The items of the widget to be displayed. /// The items of the widget to be displayed.
final dashboard.WidgetItems? items; final List<Widget> children;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) => Card(
final halfEmptyContentMessage = _renderMessage(items?.halfEmptyContentMessage);
final emptyContentMessage = _renderMessage(items?.emptyContentMessage);
return SizedBox(
width: 320,
height: 560,
child: Card(
child: InkWell( child: InkWell(
onTap: widget.widgetUrl != null && widget.widgetUrl!.isNotEmpty ? () => context.go(widget.widgetUrl!) : null, onTap: widget.widgetUrl != null && widget.widgetUrl!.isNotEmpty ? () => context.go(widget.widgetUrl!) : null,
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: ListView( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
key: PageStorageKey<String>('dashboard-${widget.id}'),
children: [
ListTile(
title: Text(
widget.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
leading: SizedBox.square(
dimension: largeIconSize,
child: NeonUrlImage(
url: widget.iconUrl,
svgColorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
size: const Size.square(largeIconSize),
),
),
),
const SizedBox(
height: 20,
),
if (halfEmptyContentMessage != null) halfEmptyContentMessage,
if (emptyContentMessage != null) emptyContentMessage,
if (halfEmptyContentMessage == null && emptyContentMessage == null && (items?.items.isEmpty ?? true))
_renderMessage(DashboardLocalizations.of(context).noEntries)!,
...?items?.items.map(
(final item) => DashboardWidgetItem(
item: item,
roundIcon: widget.itemIconsRound,
),
),
const SizedBox(
height: 20,
),
...?widget.buttons?.map(
(final button) => DashboardWidgetButton(
button: button,
),
),
],
),
),
),
);
}
Widget? _renderMessage(final String? message) {
if (message == null || message.isEmpty) {
return null;
}
return Center(
child: Column( child: Column(
children: [ children: children,
const Icon( ),
Icons.check,
size: largeIconSize,
), ),
Text(message),
],
), ),
); );
}
} }

BIN
packages/neon/neon_dashboard/test/goldens/widget.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
packages/neon/neon_dashboard/test/goldens/widget_not_round.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
packages/neon/neon_dashboard/test/goldens/widget_with_empty.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
packages/neon/neon_dashboard/test/goldens/widget_with_half_empty.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
packages/neon/neon_dashboard/test/goldens/widget_without_buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
packages/neon/neon_dashboard/test/goldens/widget_without_items.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

68
packages/neon/neon_dashboard/test/widget_test.dart

@ -9,6 +9,7 @@ import 'package:neon/theme.dart';
import 'package:neon/utils.dart'; import 'package:neon/utils.dart';
import 'package:neon/widgets.dart'; import 'package:neon/widgets.dart';
import 'package:neon_dashboard/l10n/localizations.dart'; import 'package:neon_dashboard/l10n/localizations.dart';
import 'package:neon_dashboard/src/pages/main.dart';
import 'package:neon_dashboard/src/widgets/widget.dart'; import 'package:neon_dashboard/src/widgets/widget.dart';
import 'package:neon_dashboard/src/widgets/widget_button.dart'; import 'package:neon_dashboard/src/widgets/widget_button.dart';
import 'package:neon_dashboard/src/widgets/widget_item.dart'; import 'package:neon_dashboard/src/widgets/widget_item.dart';
@ -263,9 +264,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
builder: (final context) => DashboardWidget(
widget: widget,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widget, widget: widget,
items: items, items: items,
).toList(),
),
), ),
), ),
); );
@ -299,12 +306,19 @@ void main() {
}); });
testWidgets('Without widgetUrl', (final tester) async { testWidgets('Without widgetUrl', (final tester) async {
final widgetEmptyURL = widget.rebuild((final b) => b.widgetUrl = '');
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
widget: widget.rebuild((final b) => b.widgetUrl = ''), builder: (final context) => DashboardWidget(
widget: widgetEmptyURL,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widgetEmptyURL,
items: items, items: items,
).toList(),
),
), ),
), ),
); );
@ -320,12 +334,19 @@ void main() {
}); });
testWidgets('Not round', (final tester) async { testWidgets('Not round', (final tester) async {
final widgetNotRound = widget.rebuild((final b) => b.itemIconsRound = false);
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
widget: widget.rebuild((final b) => b.itemIconsRound = false), builder: (final context) => DashboardWidget(
widget: widgetNotRound,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widgetNotRound,
items: items, items: items,
).toList(),
),
), ),
), ),
); );
@ -346,9 +367,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
builder: (final context) => DashboardWidget(
widget: widget,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widget, widget: widget,
items: items.rebuild((final b) => b.halfEmptyContentMessage = 'Half empty'), items: items.rebuild((final b) => b.halfEmptyContentMessage = 'Half empty'),
).toList(),
),
), ),
), ),
); );
@ -363,9 +390,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
builder: (final context) => DashboardWidget(
widget: widget,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widget, widget: widget,
items: items.rebuild((final b) => b.emptyContentMessage = 'Empty'), items: items.rebuild((final b) => b.emptyContentMessage = 'Empty'),
).toList(),
),
), ),
), ),
); );
@ -380,9 +413,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
builder: (final context) => DashboardWidget(
widget: widget,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widget, widget: widget,
items: null, items: null,
).toList(),
),
), ),
), ),
); );
@ -394,12 +433,19 @@ void main() {
}); });
testWidgets('Without buttons', (final tester) async { testWidgets('Without buttons', (final tester) async {
final widgetWithoutButtons = widget.rebuild((final b) => b.buttons.clear());
await tester.pumpWidget( await tester.pumpWidget(
wrapWidget( wrapWidget(
accountsBloc, accountsBloc,
DashboardWidget( Builder(
widget: widget.rebuild((final b) => b.buttons.clear()), builder: (final context) => DashboardWidget(
items: items, widget: widgetWithoutButtons,
children: DashboardMainPage.buildWidgetItems(
context: context,
widget: widgetWithoutButtons,
items: null,
).toList(),
),
), ),
), ),
); );

Loading…
Cancel
Save