This commit is contained in:
chen08209
2026-03-03 16:23:33 +08:00
parent 09ca576752
commit b61d985657
7 changed files with 359 additions and 69 deletions

View File

@@ -101,6 +101,12 @@ extension TableInfoExt<Tbl extends Table, Row> on TableInfo<Tbl, Row> {
}
}
Selectable<int?> get count {
final countExp = countAll();
final query = select().addColumns([countExp]);
return query.map((row) => row.read(countExp));
}
Future<int> remove(Expression<bool> Function(Tbl tbl) filter) async {
return await (delete()..where(filter)).go();
}
@@ -110,4 +116,22 @@ extension TableInfoExt<Tbl extends Table, Row> on TableInfo<Tbl, Row> {
}
}
extension SimpleSelectStatementExt<T extends HasResultSet, D>
on SimpleSelectStatement<T, D> {
Selectable<int> get count {
final countExp = countAll();
final query = addColumns([countExp]);
return query.map((row) => row.read(countExp)!);
}
}
extension JoinedSelectStatementExt<T extends HasResultSet, D>
on JoinedSelectStatement<T, D> {
Selectable<int> get count {
final countExp = countAll();
addColumns([countExp]);
return map((row) => row.read(countExp)!);
}
}
final database = Database();

View File

@@ -74,6 +74,15 @@ class ProxyGroupsDao extends DatabaseAccessor<Database>
return stmt.map((item) => item.toProxyGroup());
}
Selectable<int> count(int profileId) {
final stmt = proxyGroups.select();
stmt.where((row) => row.profileId.equals(profileId));
stmt.orderBy([
(t) => OrderingTerm(expression: t.order, nulls: NullsOrder.last),
]);
return stmt.count;
}
Future<int> order(
int profileId, {
required ProxyGroup proxyGroup,

View File

@@ -33,6 +33,14 @@ class RulesDao extends DatabaseAccessor<Database> with _$RulesDaoMixin {
return _get(profileId: profileId, scene: RuleScene.custom);
}
Selectable<int> profileCustomRulesCount(int profileId) {
final query = _getSelectStatement(
profileId: profileId,
scene: RuleScene.custom,
);
return query.count;
}
Selectable<Rule> allAddedRules(int profileId) {
final disabledIdsQuery = selectOnly(profileRuleLinks)
..addColumns([profileRuleLinks.ruleId])
@@ -186,7 +194,10 @@ class RulesDao extends DatabaseAccessor<Database> with _$RulesDaoMixin {
);
}
Selectable<Rule> _get({int? profileId, RuleScene? scene}) {
JoinedSelectStatement<HasResultSet, dynamic> _getSelectStatement({
int? profileId,
RuleScene? scene,
}) {
final query = select(rules).join([
innerJoin(profileRuleLinks, profileRuleLinks.ruleId.equalsExp(rules.id)),
]);
@@ -200,6 +211,11 @@ class RulesDao extends DatabaseAccessor<Database> with _$RulesDaoMixin {
query.orderBy([OrderingTerm.asc(profileRuleLinks.order)]);
return query;
}
Selectable<Rule> _get({int? profileId, RuleScene? scene}) {
final query = _getSelectStatement(profileId: profileId, scene: scene);
return query.map((row) {
return row.readTable(rules).toRule(row.read(profileRuleLinks.order));
});

View File

@@ -23,6 +23,16 @@ Future<List<Rule>> addedRules(Ref ref, int profileId) {
return database.rulesDao.allAddedRules(profileId).get();
}
@riverpod
Stream<int> customRulesCount(Ref ref, int profileId) {
return database.rulesDao.profileCustomRulesCount(profileId).watchSingle();
}
@riverpod
Stream<int> proxyGroupsCount(Ref ref, int profileId) {
return database.proxyGroupsDao.count(profileId).watchSingle();
}
@Riverpod(keepAlive: true)
class Profiles extends _$Profiles {
@override
@@ -306,6 +316,9 @@ class ProxyGroups extends _$ProxyGroups with AsyncNotifierMixin {
}
void order(int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final nextItems = List<ProxyGroup>.from(value);
final item = nextItems.removeAt(oldIndex);
nextItems.insert(newIndex, item);

View File

@@ -196,6 +196,144 @@ final class AddedRulesFamily extends $Family
String toString() => r'addedRulesProvider';
}
@ProviderFor(customRulesCount)
const customRulesCountProvider = CustomRulesCountFamily._();
final class CustomRulesCountProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
const CustomRulesCountProvider._({
required CustomRulesCountFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'customRulesCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$customRulesCountHash();
@override
String toString() {
return r'customRulesCountProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as int;
return customRulesCount(ref, argument);
}
@override
bool operator ==(Object other) {
return other is CustomRulesCountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$customRulesCountHash() => r'a3ff7941bcbb2696ba48c82b9310d81d7238536f';
final class CustomRulesCountFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, int> {
const CustomRulesCountFamily._()
: super(
retry: null,
name: r'customRulesCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
CustomRulesCountProvider call(int profileId) =>
CustomRulesCountProvider._(argument: profileId, from: this);
@override
String toString() => r'customRulesCountProvider';
}
@ProviderFor(proxyGroupsCount)
const proxyGroupsCountProvider = ProxyGroupsCountFamily._();
final class ProxyGroupsCountProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
const ProxyGroupsCountProvider._({
required ProxyGroupsCountFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'proxyGroupsCountProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$proxyGroupsCountHash();
@override
String toString() {
return r'proxyGroupsCountProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as int;
return proxyGroupsCount(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ProxyGroupsCountProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$proxyGroupsCountHash() => r'9bf90fc25a9ae3b9ab7aa0784d4e47786f4c4d52';
final class ProxyGroupsCountFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, int> {
const ProxyGroupsCountFamily._()
: super(
retry: null,
name: r'proxyGroupsCountProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ProxyGroupsCountProvider call(int profileId) =>
ProxyGroupsCountProvider._(argument: profileId, from: this);
@override
String toString() => r'proxyGroupsCountProvider';
}
@ProviderFor(Profiles)
const profilesProvider = ProfilesProvider._();
@@ -561,7 +699,7 @@ final class ProxyGroupsProvider
}
}
String _$proxyGroupsHash() => r'f771e6e80885aa662ee97a7e60984db1ba883c95';
String _$proxyGroupsHash() => r'b747de5d114e8e6d764befca26e9a8dc81d9d127';
final class ProxyGroupsFamily extends $Family
with

View File

@@ -15,18 +15,19 @@ class _CustomContent extends ConsumerWidget {
);
}
void _handleToProxyGroupsView(BuildContext context) {
BaseNavigator.push(context, _CustomProxyGroupsView(profileId));
}
void _handleToRulesView(BuildContext context) {
BaseNavigator.push(context, _CustomRulesView(profileId));
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final proxyGroupNum = ref.watch(
proxyGroupsProvider(
profileId,
).select((state) => state.value?.length ?? -1),
);
final ruleNum = ref.watch(
profileCustomRulesProvider(
profileId,
).select((state) => state.value?.length ?? -1),
);
final proxyGroupNum =
ref.watch(proxyGroupsCountProvider(profileId)).value ?? -1;
final ruleNum = ref.watch(customRulesCountProvider(profileId)).value ?? -1;
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(child: SizedBox(height: 24)),
@@ -39,7 +40,9 @@ class _CustomContent extends ConsumerWidget {
SliverToBoxAdapter(
child: _MoreActionButton(
label: '代理组',
onPressed: () {},
onPressed: () {
_handleToProxyGroupsView(context);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
@@ -62,7 +65,9 @@ class _CustomContent extends ConsumerWidget {
SliverToBoxAdapter(
child: _MoreActionButton(
label: '规则',
onPressed: () {},
onPressed: () {
_handleToRulesView(context);
},
trailing: Card.filled(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
@@ -134,81 +139,168 @@ class _CustomContent extends ConsumerWidget {
}
}
class _CustomProxyGroups extends ConsumerStatefulWidget {
class _CustomProxyGroupsView extends ConsumerWidget {
final int profileId;
const _CustomProxyGroups(this.profileId);
const _CustomProxyGroupsView(this.profileId);
@override
ConsumerState createState() => _CustomProxyGroupsState();
}
class _CustomProxyGroupsState extends ConsumerState<_CustomProxyGroups> {
@override
void initState() {
super.initState();
}
void _handleReorder(int oldIndex, int newIndex) {
ref
.read(proxyGroupsProvider(widget.profileId).notifier)
.order(oldIndex, newIndex);
void _handleReorder(WidgetRef ref, int oldIndex, int newIndex) {
ref.read(proxyGroupsProvider(profileId).notifier).order(oldIndex, newIndex);
}
@override
Widget build(BuildContext context) {
final proxyGroups =
ref.watch(proxyGroupsProvider(widget.profileId)).value ?? [];
return SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverReorderableGrid(
onReorder: _handleReorder,
itemCount: proxyGroups.length,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 16 / 8,
),
proxyDecorator: commonProxyDecorator,
Widget build(BuildContext context, WidgetRef ref) {
final proxyGroups = ref.watch(proxyGroupsProvider(profileId)).value ?? [];
return CommonScaffold(
title: '代理组',
body: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemBuilder: (_, index) {
final proxyGroup = proxyGroups[index];
return ReorderableGridDelayedDragStartListener(
return ReorderableDelayedDragStartListener(
key: ValueKey(proxyGroup),
index: index,
child: CommonCard(
radius: 12,
type: CommonCardType.filled,
padding: EdgeInsets.all(16),
onPressed: () {},
child: Text(proxyGroup.name),
child: Container(
margin: EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: CommonCard(
radius: 16,
padding: EdgeInsets.all(16),
onPressed: () {},
child: ListTile(
minTileHeight: 0,
minVerticalPadding: 0,
titleTextStyle: context.textTheme.bodyMedium?.toJetBrainsMono,
contentPadding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 4,
),
title: Text(proxyGroup.name),
subtitle: Text(proxyGroup.type.name),
),
),
),
);
},
itemCount: proxyGroups.length,
onReorder: (oldIndex, newIndex) {
_handleReorder(ref, oldIndex, newIndex);
},
),
);
}
}
class _CustomRules extends ConsumerWidget {
class _CustomRulesView extends ConsumerStatefulWidget {
final int profileId;
const _CustomRules({required this.profileId});
const _CustomRulesView(this.profileId);
@override
Widget build(context, ref) {
final rules = ref.watch(profileCustomRulesProvider(profileId)).value ?? [];
return SuperSliverList(
extentEstimation: (_, _) => 100,
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
final rule = rules[index];
return RuleItem(
isSelected: false,
rule: rule,
onSelected: () {},
onEdit: (_) {},
);
}, childCount: rules.length),
ConsumerState createState() => __CustomRulesViewState();
}
class __CustomRulesViewState extends ConsumerState<_CustomRulesView> {
final _key = utils.id;
void _handleReorder(int oldIndex, int newIndex) {
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.order(oldIndex, newIndex);
}
void _handleSelected(int ruleId) {
ref.read(selectedItemsProvider(_key).notifier).update((selectedRules) {
final newSelectedRules = Set<int>.from(selectedRules)
..addOrRemove(ruleId);
return newSelectedRules;
});
}
void _handleSelectAll() {
final ids =
ref
.read(profileCustomRulesProvider(widget.profileId))
.value
?.map((item) => item.id)
.toSet() ??
{};
ref.read(selectedItemsProvider(_key).notifier).update((selected) {
return selected.containsAll(ids) ? {} : ids;
});
}
Future<void> _handleDelete() async {
final res = await globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.deleteMultipTip(appLocalizations.rule),
),
);
if (res != true) {
return;
}
final selectedRules = ref.read(selectedItemsProvider(_key));
ref
.read(profileCustomRulesProvider(widget.profileId).notifier)
.delAll(selectedRules.cast<int>());
ref.read(selectedItemsProvider(_key).notifier).value = {};
}
@override
Widget build(context) {
final rules =
ref.watch(profileCustomRulesProvider(widget.profileId)).value ?? [];
final selectedRules = ref.watch(selectedItemsProvider(_key));
return CommonScaffold(
title: appLocalizations.rule,
actions: [
if (selectedRules.isNotEmpty) ...[
CommonMinIconButtonTheme(
child: IconButton.filledTonal(
onPressed: _handleDelete,
icon: Icon(Icons.delete),
),
),
SizedBox(width: 2),
],
CommonMinFilledButtonTheme(
child: selectedRules.isNotEmpty
? FilledButton(
onPressed: _handleSelectAll,
child: Text(appLocalizations.selectAll),
)
: FilledButton.tonal(
onPressed: () {
// _handleAddOrUpdate();
},
child: Text(appLocalizations.add),
),
),
SizedBox(width: 8),
],
body: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemBuilder: (_, index) {
final rule = rules[index];
return ReorderableDelayedDragStartListener(
key: ObjectKey(rule),
index: index,
child: RuleItem(
isEditing: selectedRules.isNotEmpty,
isSelected: selectedRules.contains(rule.id),
rule: rule,
onSelected: () {
_handleSelected(rule.id);
},
onEdit: (rule) {
// _handleAddOrUpdate(rule);
},
),
);
},
itemCount: rules.length,
onReorder: _handleReorder,
),
);
}
}

View File

@@ -15,8 +15,6 @@ import 'package:fl_clash/views/profiles/preview.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:reorderable_grid/reorderable_grid.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
part 'custom.dart';
part 'widgets.dart';