diff --git a/packages/smooth_app/lib/helpers/html_extension.dart b/packages/smooth_app/lib/helpers/html_extension.dart
new file mode 100644
index 000000000000..a3c24743c648
--- /dev/null
+++ b/packages/smooth_app/lib/helpers/html_extension.dart
@@ -0,0 +1,15 @@
+import 'package:flutter/widgets.dart';
+
+extension AlignmentDirectionalExtension on AlignmentDirectional {
+ String toHTMLTextAlign() {
+ if (start == -1.0) {
+ return 'start';
+ } else if (start == 0.0) {
+ return 'center';
+ } else if (start == 1.0) {
+ return 'end';
+ } else {
+ return 'match-parent';
+ }
+ }
+}
diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_table_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_table_card.dart
index 814cc8841e96..94ad36e30595 100644
--- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_table_card.dart
+++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_table_card.dart
@@ -1,23 +1,30 @@
-import 'dart:math';
+import 'dart:math' as math;
+import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/smooth_html_widget.dart';
+import 'package:smooth_app/helpers/html_extension.dart';
import 'package:smooth_app/helpers/ui_helpers.dart';
import 'package:smooth_app/knowledge_panel/knowledge_panels/knowledge_panel_card.dart';
import 'package:smooth_app/pages/product/portion_calculator.dart';
-
-// Cells with a lot of text can get very large, we don't want to allocate
-// most of [availableWidth] to columns with large cells. So we cap the cell length
-// considered for width allocation to [kMaxCellLengthInARow]. Cells with
-// text larger than this limit will be wrapped in multiple rows.
+import 'package:smooth_app/themes/smooth_theme.dart';
+import 'package:smooth_app/themes/smooth_theme_colors.dart';
+import 'package:smooth_app/themes/theme_provider.dart';
+
+/// Cells with a lot of text can get very large, we don't want to allocate
+/// most of [availableWidth] to columns with large cells. So we cap the cell length
+/// considered for width allocation to [kMaxCellLengthInARow]. Cells with
+/// text larger than this limit will be wrapped in multiple rows.
const int kMaxCellLengthInARow = 40;
-// Minimum length of a cell, without this a column may look unnaturally small
-// when put next to larger columns.
-const int kMinCellLengthInARow = 20;
+/// Minimum length of a cell, without this a column may look unnaturally small
+/// when put next to larger columns.
+const int kMinCellLengthInARow = 2;
+// Min length when the cell has values (eg: percent or 100g)
+const int kMinCellLengthInARowValue = 4;
/// ColumnGroup is a group of columns collapsed into a single column. Purpose of
/// this is to show a dropdown menu which the users can use to select which column
@@ -77,6 +84,7 @@ class KnowledgePanelTableCard extends StatefulWidget {
class _KnowledgePanelTableCardState extends State {
final List _columnGroups = [];
final List _columnsMaxLength = [];
+ final List<_TableCellType> _columnsType = <_TableCellType>[];
@override
void initState() {
@@ -89,20 +97,21 @@ class _KnowledgePanelTableCardState extends State {
Widget build(BuildContext context) {
final bool withPortionCalculator =
widget.tableElement.id == KnowledgePanelCard.PANEL_NUTRITION_TABLE_ID;
- return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
+
+ return LayoutBuilder(builder: (
+ BuildContext context,
+ BoxConstraints constraints,
+ ) {
final List> rowsWidgets =
_buildRowWidgets(_buildRowCells(), constraints);
+
return Column(
children: [
for (final List row in rowsWidgets)
Semantics(
excludeSemantics: true,
value: _buildSemanticsValue(row),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: row,
- ),
+ child: IntrinsicHeight(child: Row(children: row)),
),
if (withPortionCalculator) const Divider(),
if (withPortionCalculator) PortionCalculator(widget.product)
@@ -125,20 +134,22 @@ class _KnowledgePanelTableCardState extends State {
case KnowledgePanelColumnType.TEXT:
rows[0].add(
TableCell(
- text: text,
- color: Colors.grey,
- isHeader: true,
- columnGroup: columnGroup),
+ text: text,
+ color: Colors.grey,
+ isHeader: true,
+ columnGroup: columnGroup,
+ ),
);
break;
case KnowledgePanelColumnType.PERCENT:
// TODO(jasmeet): Implement percent knowledge panels.
rows[0].add(
TableCell(
- text: text,
- color: Colors.grey,
- isHeader: true,
- columnGroup: columnGroup),
+ text: text,
+ color: Colors.grey,
+ isHeader: true,
+ columnGroup: columnGroup,
+ ),
);
break;
}
@@ -175,27 +186,76 @@ class _KnowledgePanelTableCardState extends State {
_columnsMaxLength.reduce((int sum, int width) => sum + width);
final List> rowsWidgets = >[];
+ final Widget verticalDivider = _verticalDivider;
+ final Widget horizontalDivider = _horizontalDivider;
+
for (final List row in rows) {
final List rowWidgets = [];
int index = 0;
for (final TableCell cell in row) {
- final double cellWidth =
- availableWidth / totalMaxColumnWidth * _columnsMaxLength[index++];
+ final int cellWidth =
+ (availableWidth / totalMaxColumnWidth * _columnsMaxLength[index++])
+ .toInt();
+
+ Widget tableCellWidget = _TableCellWidget(
+ cell: cell,
+ cellType: _columnsType[index - 1],
+ cellWidthPercent: cellWidth,
+ tableElement: widget.tableElement,
+ rebuildTable: setState,
+ isInitiallyExpanded: widget.isInitiallyExpanded,
+ );
+
+ /// Add a divider below the header cell
+ if (cell.isHeader) {
+ tableCellWidget = Column(
+ children: [
+ Expanded(child: tableCellWidget),
+ horizontalDivider,
+ ],
+ );
+ }
+
rowWidgets.add(
- TableCellWidget(
- cell: cell,
- cellWidth: cellWidth,
- tableElement: widget.tableElement,
- rebuildTable: setState,
- isInitiallyExpanded: widget.isInitiallyExpanded,
+ Expanded(
+ flex: cellWidth,
+ child: tableCellWidget,
),
);
+
+ if (index < row.length) {
+ rowWidgets.add(verticalDivider);
+ }
}
rowsWidgets.add(rowWidgets);
}
return rowsWidgets;
}
+ Widget get _verticalDivider {
+ final SmoothColorsThemeExtension extension =
+ context.extension();
+
+ return VerticalDivider(
+ width: 1.0,
+ color: context.lightTheme()
+ ? extension.primaryMedium
+ : extension.primarySemiDark,
+ );
+ }
+
+ Widget get _horizontalDivider {
+ final SmoothColorsThemeExtension extension =
+ context.extension();
+
+ return Divider(
+ height: 1.0,
+ color: context.lightTheme()
+ ? extension.primaryMedium
+ : extension.primarySemiDark,
+ );
+ }
+
void _initColumnGroups() {
int index = 0;
// Used to locate [columnGroup] for a given [column.columnGroupId].
@@ -239,31 +299,71 @@ class _KnowledgePanelTableCardState extends State {
void _initColumnsMaxLength() {
final List> rows = _buildRowCells();
- // [columnMaxLength] contains the length of the largest cell in the columns.
- // This helps us assign a dynamic width to the column depending upon the
- // largest cell in the column.
+
for (final List row in rows) {
int index = 0;
for (final TableCell cell in row) {
if (cell.isHeader) {
+ _TableCellType type;
+ if (cell.text == '100g') {
+ type = _TableCellType.PER_100G;
+ } else if (cell.text == '%') {
+ type = _TableCellType.PERCENT;
+ } else {
+ type = _TableCellType.TEXT;
+ }
+
// Set value for the header row.
- _columnsMaxLength.add(cell.text.length);
+ _columnsMaxLength.add(
+ math.max(
+ cell.text.length,
+ type != _TableCellType.TEXT
+ ? kMinCellLengthInARowValue
+ : kMinCellLengthInARow),
+ );
+
+ _columnsType.add(type);
} else {
- if (cell.text.length > _columnsMaxLength[index]) {
- _columnsMaxLength[index] = max(kMinCellLengthInARow,
- min(kMaxCellLengthInARow, cell.text.length));
+ /// When there is content, ensure the first word is fully visible
+ if (_columnsType[index] != _TableCellType.TEXT) {
+ _columnsMaxLength[index] = math.max(
+ _columnsMaxLength[index],
+ cell.text.split('(')[0].length,
+ );
+ } else {
+ _columnsMaxLength[index] = math.max(
+ _columnsMaxLength[index],
+ cell.text.split(' ')[0].length,
+ );
}
}
index++;
}
}
+
+ /// Ensure the columns are not too wide or too narrow.
+ final int sum = _columnsMaxLength.sum;
+ final int maxWidth = (sum ~/ _columnsMaxLength.length) - 4;
+ final int minWidth = maxWidth ~/ 4;
+
+ for (int i = 0; i < _columnsMaxLength.length; i++) {
+ if (_columnsType[i] == _TableCellType.PERCENT) {
+ _columnsMaxLength[i] = math.max(_columnsMaxLength[i] + 2, minWidth);
+ } else if (_columnsType[i] == _TableCellType.PER_100G) {
+ _columnsMaxLength[i] = math.max(_columnsMaxLength[i], minWidth);
+ } else if (_columnsMaxLength[i] > maxWidth) {
+ _columnsMaxLength[i] = maxWidth;
+ } else if (_columnsMaxLength[i] < minWidth) {
+ _columnsMaxLength[i] = minWidth;
+ }
+ }
}
String _buildSemanticsValue(List row) {
final StringBuffer buffer = StringBuffer();
for (final Widget widget in row) {
- if (widget is TableCellWidget && widget.cell.text.isNotEmpty) {
+ if (widget is _TableCellWidget && widget.cell.text.isNotEmpty) {
if (buffer.isNotEmpty) {
buffer.write(' - ');
}
@@ -283,26 +383,28 @@ class _KnowledgePanelTableCardState extends State {
}
}
-class TableCellWidget extends StatefulWidget {
- const TableCellWidget({
+class _TableCellWidget extends StatefulWidget {
+ const _TableCellWidget({
required this.cell,
- required this.cellWidth,
+ required this.cellType,
+ required this.cellWidthPercent,
required this.tableElement,
required this.rebuildTable,
required this.isInitiallyExpanded,
});
final TableCell cell;
- final double cellWidth;
+ final _TableCellType cellType;
+ final int cellWidthPercent;
final KnowledgePanelTableElement tableElement;
final void Function(VoidCallback fn) rebuildTable;
final bool isInitiallyExpanded;
@override
- State createState() => _TableCellWidgetState();
+ State<_TableCellWidget> createState() => _TableCellWidgetState();
}
-class _TableCellWidgetState extends State {
+class _TableCellWidgetState extends State<_TableCellWidget> {
late bool _isExpanded;
@override
@@ -313,47 +415,99 @@ class _TableCellWidgetState extends State {
@override
Widget build(BuildContext context) {
- EdgeInsetsGeometry padding =
- const EdgeInsetsDirectional.only(bottom: VERY_SMALL_SPACE);
- // header cells get a bigger vertical padding.
+ final SmoothColorsThemeExtension extension =
+ context.extension();
+
+ final EdgeInsetsGeometry padding;
+
if (widget.cell.isHeader) {
- padding = const EdgeInsets.symmetric(vertical: SMALL_SPACE);
+ padding = const EdgeInsetsDirectional.symmetric(
+ vertical: SMALL_SPACE,
+ horizontal: VERY_SMALL_SPACE,
+ );
+ } else {
+ padding = const EdgeInsetsDirectional.symmetric(
+ vertical: 6.0,
+ horizontal: VERY_SMALL_SPACE,
+ );
}
+
TextStyle style = Theme.of(context).textTheme.bodyMedium!;
if (widget.cell.color != null) {
- style = style.apply(color: widget.cell.color);
+ /// Override the default color
+ if (widget.cell.isHeader &&
+ widget.cell.color!.intValue == const Color(0xFF9e9e9e).intValue) {
+ if (context.lightTheme()) {
+ style = style.apply(color: extension.primaryDark);
+ } else {
+ style = style.apply(color: extension.primaryMedium);
+ }
+ } else {
+ style = style.apply(color: widget.cell.color);
+ }
}
+
if (widget.cell.isHeader && widget.cell.columnGroup!.columns.length == 1) {
- return _buildHtmlCell(padding, style, isSelectable: false);
+ return SizedBox(
+ height: double.infinity,
+ child: _buildHtmlCell(
+ padding,
+ style.copyWith(fontWeight: FontWeight.w600),
+ isSelectable: false,
+ alignment: widget.cellType.headerTextAlignment,
+ backgroundColor: context.lightTheme()
+ ? extension.primaryLight
+ : extension.primaryUltraBlack,
+ ),
+ );
} else if (!widget.cell.isHeader ||
widget.cell.columnGroup!.columns.length == 1) {
- return _buildHtmlCell(padding, style, isSelectable: true);
+ return _buildHtmlCell(
+ padding,
+ style,
+ isSelectable: true,
+ alignment: widget.cellType.contentTextAlignment,
+ );
}
- return _buildDropDownColumnHeader(padding, style);
+ return _buildDropDownColumnHeader(
+ padding,
+ widget.cellWidthPercent,
+ style.copyWith(fontWeight: FontWeight.w600),
+ backgroundColor: context.lightTheme()
+ ? extension.primaryLight
+ : extension.primaryUltraBlack,
+ );
}
Widget _buildHtmlCell(
EdgeInsetsGeometry padding,
TextStyle style, {
required bool isSelectable,
+ required AlignmentDirectional alignment,
+ Color? backgroundColor,
}) {
- String cellText = widget.cell.text;
+ final StringBuffer styleBuilder =
+ StringBuffer('text-align:${alignment.toHTMLTextAlign()}');
+
if (!_isExpanded) {
- const String htmlStyle = '''
- "text-overflow: ellipsis;
- overflow: hidden;
- max-lines: 2;"
- ''';
- cellText = '${widget.cell.text}
';
+ styleBuilder.write('''
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-lines: 2;
+ ''');
}
- return GestureDetector(
+
+ final String cellText =
+ '${widget.cell.text}
';
+
+ final Widget child = GestureDetector(
onTap: () => setState(() {
_isExpanded = true;
}),
child: Padding(
padding: padding,
- child: SizedBox(
- width: widget.cellWidth,
+ child: Align(
+ alignment: alignment,
child: SmoothHtmlWidget(
cellText,
textStyle: style,
@@ -362,17 +516,28 @@ class _TableCellWidgetState extends State {
),
),
);
+
+ if (backgroundColor != null) {
+ return ColoredBox(
+ color: backgroundColor,
+ child: child,
+ );
+ } else {
+ return child;
+ }
}
Widget _buildDropDownColumnHeader(
EdgeInsetsGeometry padding,
- TextStyle style,
- ) {
+ int width,
+ TextStyle style, {
+ required Color backgroundColor,
+ }) {
// Now we finally render [ColumnGroup]s as drop down menus.
- return Padding(
- padding: padding,
- child: SizedBox(
- width: widget.cellWidth,
+ return ColoredBox(
+ color: backgroundColor,
+ child: Padding(
+ padding: padding,
child: DropdownButtonHideUnderline(
child: ButtonTheme(
child: DropdownButton(
@@ -381,14 +546,16 @@ class _TableCellWidgetState extends State {
.map((KnowledgePanelTableColumn column) {
return DropdownMenuItem(
value: column,
- child: Container(
- // 24 dp buffer is to allow the dropdown arrow icon to be displayed.
- constraints: BoxConstraints(maxWidth: widget.cellWidth - 24)
- .normalize(),
- child: Text(column.textForSmallScreens ?? column.text),
+ child: SizedBox(
+ width: width.toDouble() - 21.0 - padding.horizontal,
+ child: Text(
+ column.textForSmallScreens ?? column.text,
+ overflow: TextOverflow.ellipsis,
+ maxLines: 2,
+ ),
),
);
- }).toList(),
+ }).toList(growable: false),
onChanged: (KnowledgePanelTableColumn? selectedColumn) {
setState(() {
widget.cell.columnGroup!.currentColumn = selectedColumn;
@@ -417,8 +584,28 @@ class _TableCellWidgetState extends State {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
- properties
- .add(DiagnosticsProperty('expanded', widget.isInitiallyExpanded));
- properties.add(DoubleProperty('cellWidth', widget.cellWidth));
+ properties.add(
+ DiagnosticsProperty('expanded', widget.isInitiallyExpanded),
+ );
}
}
+
+enum _TableCellType {
+ TEXT(
+ headerTextAlignment: AlignmentDirectional.centerStart,
+ contentTextAlignment: AlignmentDirectional.centerStart),
+ PERCENT(
+ headerTextAlignment: AlignmentDirectional.center,
+ contentTextAlignment: AlignmentDirectional.centerStart),
+ PER_100G(
+ headerTextAlignment: AlignmentDirectional.center,
+ contentTextAlignment: AlignmentDirectional.center);
+
+ const _TableCellType({
+ required this.headerTextAlignment,
+ required this.contentTextAlignment,
+ });
+
+ final AlignmentDirectional headerTextAlignment;
+ final AlignmentDirectional contentTextAlignment;
+}