Fladder/lib/screens/shared/chips/category_chip.dart
PartyDonut e7b5bb40ff
feat: UI 2.0 and other Improvements (#357)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
2025-06-01 10:37:19 +02:00

270 lines
8.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
class CategoryChip<T> extends StatelessWidget {
final Map<T, bool> items;
final Widget label;
final Widget? dialogueTitle;
final Widget Function(T item) labelBuilder;
final IconData? activeIcon;
final Function(Map<T, bool> value)? onSave;
final VoidCallback? onCancel;
final VoidCallback? onClear;
final VoidCallback? onDismiss;
const CategoryChip({
required this.label,
this.dialogueTitle,
this.activeIcon,
required this.items,
required this.labelBuilder,
this.onSave,
this.onCancel,
this.onClear,
this.onDismiss,
super.key,
});
@override
Widget build(BuildContext context) {
var selection = items.included.isNotEmpty;
return FilterChip(
selected: selection,
showCheckmark: activeIcon == null,
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (activeIcon != null)
AnimatedSize(
duration: const Duration(milliseconds: 250),
child: selection
? Padding(
padding: const EdgeInsets.only(right: 12),
child: Icon(
activeIcon!,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
: const SizedBox(),
),
label,
const SizedBox(width: 8),
Icon(
Icons.arrow_drop_down_rounded,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
onSelected: items.isNotEmpty
? (_) async {
final newEntry = await openActionSheet(context);
if (newEntry != null) {
onSave?.call(newEntry);
}
}
: null,
);
}
Future<Map<T, bool>?> openActionSheet(BuildContext context) async {
Map<T, bool>? newEntry;
List<Widget> actions() => [
FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onCancel?.call();
},
child: Text(context.localized.cancel),
),
if (onClear != null)
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6));
Widget header(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Material(
color: Colors.transparent,
textStyle: Theme.of(context).textTheme.titleLarge,
child: dialogueTitle ?? label,
),
Row(
children: [
FilledButton.tonal(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onCancel?.call();
},
child: Text(context.localized.cancel),
),
if (onClear != null)
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
newEntry = null;
onClear!();
},
icon: const Icon(IconsaxPlusLinear.back_square),
label: Text(context.localized.clear),
)
].addInBetween(const SizedBox(width: 6)),
),
],
);
if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) {
await showModalSideSheet(
context,
addDivider: true,
header: dialogueTitle ?? label,
actions: actions(),
content: CategoryChipEditor(
labelBuilder: labelBuilder,
items: items,
onChanged: (value) {
newEntry = value;
}),
onDismiss: () {
if (newEntry != null) {
onSave?.call(newEntry!);
}
},
);
} else {
await showBottomSheetPill(
context: context,
content: (context, scrollController) => ListView(
shrinkWrap: true,
controller: scrollController,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: header(context),
),
const Divider(),
CategoryChipEditor(
labelBuilder: labelBuilder,
controller: scrollController,
items: items,
onChanged: (value) => newEntry = value),
],
),
onDismiss: () {
if (newEntry != null) {
onSave?.call(newEntry!);
}
},
);
}
return newEntry;
}
}
class CategoryChipEditor<T> extends StatefulWidget {
final Map<T, bool> items;
final Widget Function(T item) labelBuilder;
final Function(Map<T, bool> value) onChanged;
final ScrollController? controller;
const CategoryChipEditor({
required this.items,
required this.labelBuilder,
required this.onChanged,
this.controller,
super.key,
});
@override
State<CategoryChipEditor<T>> createState() => _CategoryChipEditorState<T>();
}
class _CategoryChipEditorState<T> extends State<CategoryChipEditor<T>> {
late Map<T, bool?> currentState = Map.fromEntries(widget.items.entries);
@override
Widget build(BuildContext context) {
Iterable<MapEntry<T, bool>> activeItems = widget.items.entries.where((element) => element.value);
Iterable<MapEntry<T, bool>> otherItems = widget.items.entries.where((element) => !element.value);
return ListView(
shrinkWrap: true,
controller: widget.controller,
children: [
if (activeItems.isNotEmpty == true) ...{
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
context.localized.active,
style: Theme.of(context).textTheme.titleLarge,
),
),
...activeItems.mapIndexed((index, element) {
return CheckboxListTile.adaptive(
value: currentState[element.key],
title: widget.labelBuilder(element.key),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (currentState[element.key] == null) {
return Colors.redAccent;
}
return null;
},
),
tristate: true,
onChanged: (value) => updateKey(MapEntry(element.key, value == null ? null : element.value)),
);
}),
const Divider(),
},
...otherItems.mapIndexed((index, element) {
return CheckboxListTile.adaptive(
value: currentState[element.key],
title: widget.labelBuilder(element.key),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (currentState[element.key] == null || states.contains(WidgetState.selected)) {
return Colors.greenAccent;
}
return null;
},
),
tristate: true,
onChanged: (value) => updateKey(MapEntry(element.key, value != false ? null : element.value)),
);
}),
],
);
}
void updateKey(MapEntry<T, bool?> entry) {
setState(() {
currentState.update(
entry.key,
(value) => entry.value,
);
});
widget.onChanged(Map.from(currentState.map(
(key, value) {
final origKey = widget.items[key] == true;
return MapEntry(key, origKey ? (value == null ? false : origKey) : (value == null ? true : origKey));
},
)));
}
}