Skip to content

Commit

Permalink
fix(neon_dashboard): Remove scrolling inside dashboard widgets
Browse files Browse the repository at this point in the history
Signed-off-by: jld3103 <jld3103yt@gmail.com>
  • Loading branch information
provokateurin committed Nov 16, 2023
1 parent a86b79c commit ea0f008
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 102 deletions.
134 changes: 130 additions & 4 deletions packages/neon/neon_dashboard/lib/src/pages/main.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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(),
Expand All @@ -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),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 77 deletions packages/neon/neon_dashboard/lib/src/widgets/widget.dart
Original file line number Diff line number Diff line change
@@ -1,102 +1,33 @@
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.
class DashboardWidget extends StatelessWidget {
/// Creates a new dashboard widget items.
const DashboardWidget({
required this.widget,
required this.items,
required this.children,
super.key,
});

/// The dashboard widget to be displayed.
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),
],
),
);
}
);
}
Binary file modified packages/neon/neon_dashboard/test/goldens/widget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/neon/neon_dashboard/test/goldens/widget_not_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/neon/neon_dashboard/test/goldens/widget_with_empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit ea0f008

Please sign in to comment.