252 lines
7.4 KiB
Dart
252 lines
7.4 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_typography.dart';
|
|
import 'mono_text.dart';
|
|
|
|
class TasQColumn<T> {
|
|
const TasQColumn({
|
|
required this.header,
|
|
required this.cellBuilder,
|
|
this.technical = false,
|
|
});
|
|
|
|
final String header;
|
|
final Widget Function(BuildContext context, T item) cellBuilder;
|
|
final bool technical;
|
|
}
|
|
|
|
typedef TasQMobileTileBuilder<T> =
|
|
Widget Function(BuildContext context, T item, List<Widget> actions);
|
|
|
|
typedef TasQRowActions<T> = List<Widget> Function(T item);
|
|
|
|
typedef TasQRowTap<T> = void Function(T item);
|
|
|
|
class TasQAdaptiveList<T> extends StatelessWidget {
|
|
const TasQAdaptiveList({
|
|
super.key,
|
|
required this.items,
|
|
required this.columns,
|
|
required this.mobileTileBuilder,
|
|
this.rowActions,
|
|
this.onRowTap,
|
|
this.rowsPerPage = 25,
|
|
this.tableHeader,
|
|
this.filterHeader,
|
|
this.summaryDashboard,
|
|
});
|
|
|
|
final List<T> items;
|
|
final List<TasQColumn<T>> columns;
|
|
final TasQMobileTileBuilder<T> mobileTileBuilder;
|
|
final TasQRowActions<T>? rowActions;
|
|
final TasQRowTap<T>? onRowTap;
|
|
final int rowsPerPage;
|
|
final Widget? tableHeader;
|
|
final Widget? filterHeader;
|
|
final Widget? summaryDashboard;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isMobile = constraints.maxWidth < 600;
|
|
final hasBoundedHeight = constraints.hasBoundedHeight;
|
|
|
|
if (isMobile) {
|
|
final listView = ListView.separated(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
itemCount: items.length,
|
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final item = items[index];
|
|
final actions = rowActions?.call(item) ?? const <Widget>[];
|
|
return mobileTileBuilder(context, item, actions);
|
|
},
|
|
);
|
|
final shrinkWrappedList = ListView.separated(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
itemCount: items.length,
|
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final item = items[index];
|
|
final actions = rowActions?.call(item) ?? const <Widget>[];
|
|
return mobileTileBuilder(context, item, actions);
|
|
},
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
);
|
|
final summarySection = summaryDashboard == null
|
|
? null
|
|
: <Widget>[
|
|
SizedBox(width: double.infinity, child: summaryDashboard!),
|
|
const SizedBox(height: 12),
|
|
];
|
|
final filterSection = filterHeader == null
|
|
? null
|
|
: <Widget>[
|
|
ExpansionTile(
|
|
title: const Text('Filters'),
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: filterHeader!,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
];
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
...?summarySection,
|
|
...?filterSection,
|
|
if (hasBoundedHeight) Expanded(child: listView),
|
|
if (!hasBoundedHeight) shrinkWrappedList,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
final dataSource = _TasQTableSource<T>(
|
|
context: context,
|
|
items: items,
|
|
columns: columns,
|
|
rowActions: rowActions,
|
|
onRowTap: onRowTap,
|
|
);
|
|
final contentWidth = constraints.maxWidth * 0.8;
|
|
final tableWidth = math.max(
|
|
contentWidth,
|
|
(columns.length + (rowActions == null ? 0 : 1)) * 140.0,
|
|
);
|
|
final effectiveRowsPerPage = math.min(
|
|
rowsPerPage,
|
|
math.max(1, items.length),
|
|
);
|
|
final tableWidget = SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: SizedBox(
|
|
width: tableWidth,
|
|
child: PaginatedDataTable(
|
|
header: tableHeader,
|
|
rowsPerPage: effectiveRowsPerPage,
|
|
columnSpacing: 20,
|
|
horizontalMargin: 16,
|
|
showCheckboxColumn: false,
|
|
headingRowColor: WidgetStateProperty.resolveWith(
|
|
(states) => Theme.of(context).colorScheme.surfaceContainer,
|
|
),
|
|
columns: [
|
|
for (final column in columns)
|
|
DataColumn(label: Text(column.header)),
|
|
if (rowActions != null)
|
|
const DataColumn(label: Text('Actions')),
|
|
],
|
|
source: dataSource,
|
|
),
|
|
),
|
|
);
|
|
|
|
final summarySection = summaryDashboard == null
|
|
? null
|
|
: <Widget>[
|
|
SizedBox(width: contentWidth, child: summaryDashboard!),
|
|
const SizedBox(height: 12),
|
|
];
|
|
final filterSection = filterHeader == null
|
|
? null
|
|
: <Widget>[filterHeader!, const SizedBox(height: 12)];
|
|
|
|
return SingleChildScrollView(
|
|
primary: hasBoundedHeight,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: contentWidth,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [...?summarySection, ...?filterSection, tableWidget],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TasQTableSource<T> extends DataTableSource {
|
|
_TasQTableSource({
|
|
required this.context,
|
|
required this.items,
|
|
required this.columns,
|
|
required this.rowActions,
|
|
required this.onRowTap,
|
|
});
|
|
|
|
final BuildContext context;
|
|
final List<T> items;
|
|
final List<TasQColumn<T>> columns;
|
|
final TasQRowActions<T>? rowActions;
|
|
final TasQRowTap<T>? onRowTap;
|
|
|
|
@override
|
|
DataRow? getRow(int index) {
|
|
if (index >= items.length) return null;
|
|
final item = items[index];
|
|
final cells = <DataCell>[];
|
|
|
|
for (final column in columns) {
|
|
final widget = column.cellBuilder(context, item);
|
|
cells.add(DataCell(_applyTechnicalStyle(context, widget, column)));
|
|
}
|
|
|
|
if (rowActions != null) {
|
|
final actions = rowActions!.call(item);
|
|
cells.add(
|
|
DataCell(Row(mainAxisSize: MainAxisSize.min, children: actions)),
|
|
);
|
|
}
|
|
|
|
return DataRow(
|
|
onSelectChanged: onRowTap == null ? null : (_) => onRowTap!(item),
|
|
cells: cells,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get isRowCountApproximate => false;
|
|
|
|
@override
|
|
int get rowCount => items.length;
|
|
|
|
@override
|
|
int get selectedRowCount => 0;
|
|
}
|
|
|
|
Widget _applyTechnicalStyle<T>(
|
|
BuildContext context,
|
|
Widget child,
|
|
TasQColumn<T> column,
|
|
) {
|
|
if (!column.technical) return child;
|
|
if (child is Text && child.data != null) {
|
|
return MonoText(
|
|
child.data ?? '',
|
|
style: child.style,
|
|
maxLines: child.maxLines,
|
|
overflow: child.overflow,
|
|
textAlign: child.textAlign,
|
|
);
|
|
}
|
|
|
|
final mono = AppMonoText.of(context).body;
|
|
return DefaultTextStyle.merge(style: mono, child: child);
|
|
}
|