mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-06-01 17:15:48 +08:00
This commit is contained in:
121
lib/ui/component/multi_select_controller.dart
Normal file
121
lib/ui/component/multi_select_controller.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:proxypin/utils/listenable_list.dart';
|
||||
|
||||
class MultiSelectController {
|
||||
final ListenableList<String> selectedIds = ListenableList<String>();
|
||||
|
||||
String? _anchorId;
|
||||
RxBool selectionMode = false.obs;
|
||||
|
||||
bool get isSelectionMode => selectionMode.value;
|
||||
|
||||
int get selectedCount => selectedIds.length;
|
||||
|
||||
bool contains(String requestId) => selectedIds.contains(requestId);
|
||||
|
||||
void clear() {
|
||||
if (selectedIds.isEmpty && !selectionMode.value) {
|
||||
return;
|
||||
}
|
||||
selectedIds.clear();
|
||||
_anchorId = null;
|
||||
selectionMode.value = false;
|
||||
}
|
||||
|
||||
void enterSelectionMode([String? requestId]) {
|
||||
selectionMode.value = true;
|
||||
if (requestId != null) {
|
||||
selectedIds.add(requestId);
|
||||
_anchorId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
void selectOnly(String requestId) {
|
||||
selectionMode.value = true;
|
||||
selectedIds
|
||||
..clear()
|
||||
..add(requestId);
|
||||
_anchorId = requestId;
|
||||
}
|
||||
|
||||
void toggleSelectionMode([String? requestId]) {
|
||||
if (selectionMode.value) {
|
||||
clear();
|
||||
} else {
|
||||
enterSelectionMode(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void toggle(String requestId) {
|
||||
selectionMode.value = true;
|
||||
if (selectedIds.contains(requestId)) {
|
||||
selectedIds.remove(requestId);
|
||||
} else {
|
||||
selectedIds.add(requestId);
|
||||
}
|
||||
|
||||
if (selectedIds.isEmpty) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
_anchorId = requestId;
|
||||
}
|
||||
|
||||
void selectRange(List<String> orderedIds, String requestId) {
|
||||
final targetIndex = orderedIds.indexOf(requestId);
|
||||
if (targetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final anchorIndex = _anchorId == null ? -1 : orderedIds.indexOf(_anchorId!);
|
||||
if (anchorIndex < 0) {
|
||||
selectOnly(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
final start = anchorIndex < targetIndex ? anchorIndex : targetIndex;
|
||||
final end = anchorIndex > targetIndex ? anchorIndex : targetIndex;
|
||||
selectionMode.value = true;
|
||||
selectedIds
|
||||
..clear()
|
||||
..addAll(orderedIds.sublist(start, end + 1));
|
||||
_anchorId = requestId;
|
||||
}
|
||||
|
||||
void prune(Iterable<String> visibleIds) {
|
||||
final visibleIdSet = visibleIds.toSet();
|
||||
selectedIds.removeWhere((requestId) => !visibleIdSet.contains(requestId));
|
||||
|
||||
if (selectedIds.isEmpty) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
selectionMode.value = true;
|
||||
if (_anchorId == null || !selectedIds.contains(_anchorId)) {
|
||||
_anchorId = selectedIds.last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectListener<T> extends ListenerListEvent<T> {
|
||||
final Function(List<T> items) onChange;
|
||||
|
||||
MultiSelectListener(this.onChange);
|
||||
|
||||
@override
|
||||
void onAdd(T item) => onChange.call([item]);
|
||||
|
||||
@override
|
||||
void onRemove(T item) => onChange.call([item]);
|
||||
|
||||
@override
|
||||
void onUpdate(T item) => onChange.call([item]);
|
||||
|
||||
@override
|
||||
void onBatchRemove(List<T> items) => onChange.call(items);
|
||||
|
||||
@override
|
||||
void clear(List<T> items) => onChange.call(items);
|
||||
}
|
||||
85
lib/ui/component/selection_action_bar.dart
Normal file
85
lib/ui/component/selection_action_bar.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:proxypin/l10n/app_localizations.dart';
|
||||
import 'package:proxypin/ui/component/multi_select_controller.dart';
|
||||
import 'package:proxypin/utils/listenable_list.dart';
|
||||
|
||||
class SelectionActionBar extends StatelessWidget {
|
||||
final MultiSelectController selectionController;
|
||||
final VoidCallback? onRepeat;
|
||||
final VoidCallback? onExport;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const SelectionActionBar({super.key, required this.selectionController, this.onRepeat, this.onExport, this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
_SelectLabel(selectionController: selectionController),
|
||||
const Spacer(),
|
||||
if (onRepeat != null)
|
||||
IconButton(onPressed: onRepeat, tooltip: localizations?.repeat, icon: const Icon(Icons.repeat, size: 18)),
|
||||
if (onExport != null)
|
||||
IconButton(
|
||||
onPressed: onExport, tooltip: localizations?.export, icon: const Icon(Icons.share_outlined, size: 18)),
|
||||
if (onDelete != null)
|
||||
IconButton(
|
||||
onPressed: onDelete, tooltip: localizations?.delete, icon: const Icon(Icons.delete_outline, size: 18)),
|
||||
IconButton(onPressed: _onCancel, tooltip: localizations?.cancel, icon: const Icon(Icons.close, size: 18)),
|
||||
]));
|
||||
}
|
||||
|
||||
void _onCancel() {
|
||||
selectionController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectLabel extends StatefulWidget {
|
||||
final MultiSelectController selectionController;
|
||||
|
||||
const _SelectLabel({required this.selectionController});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SelectLabelState();
|
||||
}
|
||||
|
||||
class _SelectLabelState extends State<_SelectLabel> {
|
||||
late final OnchangeListEvent<String> _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_listener = OnchangeListEvent(_onSelectionChanged);
|
||||
widget.selectionController.selectedIds.addListener(_listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.selectionController.selectedIds.removeListener(_listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSelectionChanged() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectLabel = AppLocalizations.of(context)?.selectAction;
|
||||
final selectedCount = widget.selectionController.selectedIds.length;
|
||||
final label = (selectLabel == null || selectLabel.isEmpty) ? '$selectedCount' : '$selectedCount $selectLabel';
|
||||
|
||||
return Text(label, style: Theme.of(context).textTheme.bodyMedium, maxLines: 1, overflow: TextOverflow.ellipsis);
|
||||
}
|
||||
}
|
||||
|
||||
class ClearSelectionIntent extends Intent {
|
||||
const ClearSelectionIntent();
|
||||
}
|
||||
Reference in New Issue
Block a user