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. 85
      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. 88
      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:neon/blocs.dart';
import 'package:neon/theme.dart';
import 'package:neon/utils.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/widgets/dry_intrinsic_height.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.
class DashboardMainPage extends StatelessWidget {
@ -21,15 +29,37 @@ class DashboardMainPage extends StatelessWidget {
builder: (final context, final snapshot) {
Widget? child;
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(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: snapshot.requireData.entries
children: children
.map(
(final widget) => DashboardWidget(
widget: widget.key,
items: widget.value,
(final widget) => SizedBox(
width: 320,
height: minHeight + 24,
child: widget,
),
)
.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;
}
}
}

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

@ -1,10 +1,5 @@
import 'package:flutter/material.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;
/// Displays a single dashboard widget and its items.
@ -12,7 +7,7 @@ class DashboardWidget extends StatelessWidget {
/// Creates a new dashboard widget items.
const DashboardWidget({
required this.widget,
required this.items,
required this.children,
super.key,
});
@ -20,83 +15,19 @@ class DashboardWidget extends StatelessWidget {
final dashboard.Widget widget;
/// The items of the widget to be displayed.
final dashboard.WidgetItems? items;
final List<Widget> children;
@override
Widget build(final BuildContext context) {
final halfEmptyContentMessage = _renderMessage(items?.halfEmptyContentMessage);
final emptyContentMessage = _renderMessage(items?.emptyContentMessage);
return SizedBox(
width: 320,
height: 560,
child: Card(
Widget build(final BuildContext context) => Card(
child: InkWell(
onTap: widget.widgetUrl != null && widget.widgetUrl!.isNotEmpty ? () => context.go(widget.widgetUrl!) : null,
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: ListView(
child: Padding(
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,
),
),
],
child: Column(
children: children,
),
),
),
),
);
}
Widget? _renderMessage(final String? message) {
if (message == null || message.isEmpty) {
return null;
}
return Center(
child: Column(
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

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

Loading…
Cancel
Save