Add sqlite store

Optimize android quick action

Optimize backup and restore

Optimize more details
This commit is contained in:
chen08209
2025-12-16 11:23:09 +08:00
parent 243b3037d9
commit 2fbb96f5c1
214 changed files with 15659 additions and 10751 deletions

View File

@@ -0,0 +1,78 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
part 'generated/database.g.dart';
part 'links.dart';
part 'profiles.dart';
part 'rules.dart';
part 'scripts.dart';
@DriftDatabase(
tables: [Profiles, Scripts, Rules, ProfileRuleLinks],
daos: [ProfilesDao, ScriptsDao, RulesDao],
)
class Database extends _$Database {
Database([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
static LazyDatabase _openConnection() {
return LazyDatabase(() async {
final databaseFile = File(await appPath.databasePath);
return NativeDatabase.createInBackground(databaseFile);
});
}
Future<void> restore(
List<Profile> profiles,
List<Script> scripts,
List<Rule> rules,
List<ProfileRuleLink> links, {
bool isOverride = false,
}) async {
if (profiles.isNotEmpty ||
scripts.isNotEmpty ||
rules.isNotEmpty ||
links.isNotEmpty) {
await batch((b) {
isOverride
? profilesDao.setAllWithBatch(b, profiles)
: profilesDao.putAllWithBatch(
b,
profiles.map((item) => item.toCompanion()),
);
scriptsDao.setAllWithBatch(b, scripts);
rulesDao.restoreWithBatch(b, rules, links);
});
}
}
}
extension TableInfoExt<Tbl extends Table, Row> on TableInfo<Tbl, Row> {
void setAll(
Batch batch,
Iterable<Insertable<Row>> items, {
required Expression<bool> Function(Tbl tbl) deleteFilter,
}) async {
batch.insertAllOnConflictUpdate(this, items);
batch.deleteWhere(this, deleteFilter);
}
Future<int> remove(Expression<bool> Function(Tbl tbl) filter) async {
return await (delete()..where(filter)).go();
}
Future<int> put(Insertable<Row> item) async {
return await insertOnConflictUpdate(item);
}
}
final database = Database();

File diff suppressed because it is too large Load Diff

52
lib/database/links.dart Normal file
View File

@@ -0,0 +1,52 @@
part of 'database.dart';
@DataClassName('RawProfileRuleLink')
@TableIndex(
name: 'idx_profile_scene_order',
columns: {#profileId, #scene, #order},
)
class ProfileRuleLinks extends Table {
@override
String get tableName => 'profile_rule_mapping';
TextColumn get id => text()();
IntColumn get profileId => integer().nullable().references(
Profiles,
#id,
onDelete: KeyAction.cascade,
)();
IntColumn get ruleId =>
integer().references(Rules, #id, onDelete: KeyAction.cascade)();
TextColumn get scene => textEnum<RuleScene>().nullable()();
TextColumn get order => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
extension RawProfileRuleLinkExt on RawProfileRuleLink {
ProfileRuleLink toLink() {
return ProfileRuleLink(
profileId: profileId,
ruleId: ruleId,
scene: scene,
order: order,
);
}
}
extension ProfileRuleLinksCompanionExt on ProfileRuleLink {
ProfileRuleLinksCompanion toCompanion() {
return ProfileRuleLinksCompanion.insert(
id: key,
ruleId: ruleId,
scene: Value(scene),
profileId: Value(profileId),
order: Value(order),
);
}
}

168
lib/database/profiles.dart Normal file
View File

@@ -0,0 +1,168 @@
part of 'database.dart';
@DataClassName('RawProfile')
class Profiles extends Table {
@override
String get tableName => 'profiles';
IntColumn get id => integer()();
TextColumn get label => text()();
TextColumn get currentGroupName => text().nullable()();
TextColumn get url => text()();
DateTimeColumn get lastUpdateDate => dateTime().nullable()();
TextColumn get overwriteType => textEnum<OverwriteType>()();
IntColumn get scriptId => integer().nullable()();
IntColumn get autoUpdateDurationMillis => integer()();
TextColumn get subscriptionInfo =>
text().map(const SubscriptionInfoConverter()).nullable()();
BoolColumn get autoUpdate => boolean()();
TextColumn get selectedMap => text().map(const StringMapConverter())();
TextColumn get unfoldSet => text().map(const StringSetConverter())();
IntColumn get order => integer().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class SubscriptionInfoConverter
extends TypeConverter<SubscriptionInfo?, String?> {
const SubscriptionInfoConverter();
@override
SubscriptionInfo? fromSql(String? fromDb) {
if (fromDb == null) return null;
return SubscriptionInfo.fromJson(json.decode(fromDb));
}
@override
String? toSql(SubscriptionInfo? value) {
if (value == null) return null;
return json.encode(value.toJson());
}
}
@DriftAccessor(tables: [Profiles])
class ProfilesDao extends DatabaseAccessor<Database> with _$ProfilesDaoMixin {
ProfilesDao(super.attachedDatabase);
Selectable<Profile> all() {
final stmt = profiles.select();
stmt.orderBy([
(t) => OrderingTerm(expression: t.order, nulls: NullsOrder.last),
(t) => OrderingTerm.asc(t.id),
]);
return stmt.map((item) => item.toProfile());
}
Future<void> setAll(Iterable<Profile> profiles) async {
await batch((b) async {
setAllWithBatch(b, profiles);
});
}
Future<void> putAll<T extends Table, D extends DataClass>(
Iterable<Insertable<D>> items,
) async {
await batch((b) async {
putAllWithBatch(b, items);
});
}
void putAllWithBatch<T extends Table, D extends DataClass>(
Batch batch,
Iterable<Insertable<D>> items,
) {
batch.insertAllOnConflictUpdate(profiles, items);
}
void setAllWithBatch(Batch batch, Iterable<Profile> profiles) {
final List<ProfilesCompanion> items = [];
final List<int> ids = [];
profiles.forEachIndexed((index, profile) {
ids.add(profile.id);
items.add(profile.toCompanion(index));
});
this.profiles.setAll(batch, items, deleteFilter: (t) => t.id.isNotIn(ids));
}
}
class StringMapConverter extends TypeConverter<Map<String, String>, String> {
const StringMapConverter();
@override
Map<String, String> fromSql(String fromDb) {
return Map<String, String>.from(json.decode(fromDb));
}
@override
String toSql(Map<String, String> value) {
return json.encode(value);
}
}
class StringSetConverter extends TypeConverter<Set<String>, String> {
const StringSetConverter();
@override
Set<String> fromSql(String fromDb) {
return Set<String>.from(json.decode(fromDb));
}
@override
String toSql(Set<String> value) {
return json.encode(value.toList());
}
}
extension RawProfilExt on RawProfile {
Profile toProfile() {
return Profile(
id: id,
label: label,
currentGroupName: currentGroupName,
url: url,
lastUpdateDate: lastUpdateDate,
autoUpdateDuration: Duration(milliseconds: autoUpdateDurationMillis),
subscriptionInfo: subscriptionInfo,
autoUpdate: autoUpdate,
selectedMap: selectedMap,
unfoldSet: unfoldSet,
overwriteType: overwriteType,
scriptId: scriptId,
order: order,
);
}
}
extension ProfilesCompanionExt on Profile {
ProfilesCompanion toCompanion([int? order]) {
return ProfilesCompanion.insert(
id: Value(id),
label: label,
currentGroupName: Value(currentGroupName),
url: url,
lastUpdateDate: Value(lastUpdateDate),
autoUpdateDurationMillis: autoUpdateDuration.inMilliseconds,
subscriptionInfo: Value(subscriptionInfo),
autoUpdate: autoUpdate,
selectedMap: selectedMap,
unfoldSet: unfoldSet,
overwriteType: overwriteType,
scriptId: Value(scriptId),
order: Value(order ?? this.order),
);
}
}

283
lib/database/rules.dart Normal file
View File

@@ -0,0 +1,283 @@
part of 'database.dart';
@DataClassName('RawRule')
class Rules extends Table {
@override
String get tableName => 'rules';
IntColumn get id => integer()();
TextColumn get value => text()();
@override
Set<Column> get primaryKey => {id};
}
@DriftAccessor(tables: [Rules, ProfileRuleLinks])
class RulesDao extends DatabaseAccessor<Database> with _$RulesDaoMixin {
RulesDao(super.attachedDatabase);
Selectable<Rule> allGlobalAddedRules() {
return _get();
}
Selectable<Rule> allProfileAddedRules(int profileId) {
return _get(profileId: profileId, scene: RuleScene.added);
}
Selectable<Rule> allProfileDisabledRules(int profileId) {
return _get(profileId: profileId, scene: RuleScene.disabled);
}
Selectable<Rule> allAddedRules(int profileId) {
final disabledIdsQuery = selectOnly(profileRuleLinks)
..addColumns([profileRuleLinks.ruleId])
..where(
profileRuleLinks.profileId.equals(profileId) &
profileRuleLinks.scene.equalsValue(RuleScene.disabled),
);
final query = select(rules).join([
innerJoin(profileRuleLinks, profileRuleLinks.ruleId.equalsExp(rules.id)),
]);
query.where(
(profileRuleLinks.profileId.isNull() |
(profileRuleLinks.profileId.equals(profileId) &
profileRuleLinks.scene.equalsValue(RuleScene.added))) &
profileRuleLinks.ruleId.isNotInQuery(disabledIdsQuery),
);
query.orderBy([
OrderingTerm.desc(
profileRuleLinks.profileId.isNull().caseMatch<int>(
when: {const Constant(true): const Constant(1)},
orElse: const Constant(0),
),
),
OrderingTerm.desc(profileRuleLinks.order),
]);
return query.map((row) {
final ruleData = row.readTable(rules);
final order = row.read(profileRuleLinks.order);
return ruleData.toRule(order);
});
}
void restoreWithBatch(
Batch batch,
Iterable<Rule> rules,
Iterable<ProfileRuleLink> links,
) {
batch.insertAllOnConflictUpdate(
this.rules,
rules.map((item) => item.toCompanion()),
);
final ruleIds = rules.map((item) => item.id);
batch.deleteWhere(this.rules, (t) => t.id.isNotIn(ruleIds));
batch.insertAllOnConflictUpdate(
profileRuleLinks,
links.map((item) => item.toCompanion()),
);
final linkKeys = links.map((item) => item.key);
batch.deleteWhere(profileRuleLinks, (t) => t.id.isNotIn(linkKeys));
}
Future<void> delRules(Iterable<int> ruleIds) {
return _delAll(ruleIds);
}
Future<void> putGlobalRule(Rule rule) {
return _put(rule);
}
Future<void> putProfileAddedRule(int profileId, Rule rule) {
return _put(rule, profileId: profileId, scene: RuleScene.added);
}
Future<void> putProfileDisabledRule(int profileId, Rule rule) {
return _put(rule, profileId: profileId, scene: RuleScene.added);
}
Future<void> putGlobalRules(Iterable<Rule> rules) {
return _putAll(rules);
}
Future<void> setGlobalRules(Iterable<Rule> rules) {
return _set(rules);
}
Future<int> putDisabledLink(int profileId, int ruleId) async {
return await profileRuleLinks.insertOnConflictUpdate(
ProfileRuleLink(
ruleId: ruleId,
profileId: profileId,
scene: RuleScene.disabled,
).toCompanion(),
);
}
Future<bool> delDisabledLink(int profileId, int ruleId) async {
return await profileRuleLinks.deleteOne(
ProfileRuleLink(
profileId: profileId,
ruleId: ruleId,
scene: RuleScene.disabled,
).toCompanion(),
);
}
Future<int> orderGlobalRule({
required int ruleId,
required String order,
}) async {
return await _order(ruleId: ruleId, order: order);
}
Future<int> orderProfileAddedRule(
int profileId, {
required int ruleId,
required String order,
}) async {
return await _order(
ruleId: ruleId,
order: order,
profileId: profileId,
scene: RuleScene.added,
);
}
Selectable<Rule> _get({int? profileId, RuleScene? scene}) {
final query = select(rules).join([
innerJoin(profileRuleLinks, profileRuleLinks.ruleId.equalsExp(rules.id)),
]);
query.where(
profileId == null
? profileRuleLinks.profileId.isNull()
: profileRuleLinks.profileId.equals(profileId) &
profileRuleLinks.scene.equalsValue(scene),
);
query.orderBy([
OrderingTerm.desc(profileRuleLinks.order),
OrderingTerm.desc(profileRuleLinks.id),
]);
return query.map((row) {
return row.readTable(rules).toRule(row.read(profileRuleLinks.order));
});
}
Future<int> _order({
required int ruleId,
required String order,
int? profileId,
RuleScene? scene,
}) async {
final stmt = profileRuleLinks.update();
stmt.where((t) {
return (profileId == null
? t.profileId.isNull()
: t.profileId.equals(profileId)) &
t.ruleId.equals(ruleId) &
t.scene.equalsValue(scene);
});
return await stmt.write(ProfileRuleLinksCompanion(order: Value(order)));
}
Future<int> _put(Rule rule, {int? profileId, RuleScene? scene}) async {
return transaction(() async {
final row = await rules.insertOnConflictUpdate(rule.toCompanion());
if (row == 0) {
return 0;
}
return await profileRuleLinks.insertOnConflictUpdate(
ProfileRuleLink(
ruleId: rule.id,
profileId: profileId,
scene: scene,
).toCompanion(),
);
});
}
Future<void> _delAll(Iterable<int> ruleIds) async {
await rules.deleteWhere((t) => t.id.isIn(ruleIds));
}
Future<void> _putAll(
Iterable<Rule> rules, {
int? profileId,
RuleScene? scene,
}) async {
await batch((b) {
b.insertAllOnConflictUpdate(
this.rules,
rules.map((item) => item.toCompanion()),
);
b.insertAllOnConflictUpdate(
profileRuleLinks,
rules.map(
(item) => ProfileRuleLink(
ruleId: item.id,
profileId: profileId,
scene: scene,
).toCompanion(),
),
);
});
}
Future<void> _set(
Iterable<Rule> rules, {
int? profileId,
RuleScene? scene,
}) async {
await batch((b) {
b.insertAllOnConflictUpdate(
this.rules,
rules.map((item) => item.toCompanion()),
);
b.deleteWhere(
profileRuleLinks,
(t) =>
(profileId == null
? t.profileId.isNull()
: t.profileId.equals(profileId)) &
(scene == null ? const Constant(true) : t.scene.equalsValue(scene)),
);
b.insertAllOnConflictUpdate(
profileRuleLinks,
rules.map(
(item) => ProfileRuleLink(
ruleId: item.id,
profileId: profileId,
scene: scene,
).toCompanion(),
),
);
b.deleteWhere(this.rules, (r) {
final linkedIds = selectOnly(profileRuleLinks);
linkedIds.addColumns([profileRuleLinks.ruleId]);
return r.id.isNotInQuery(linkedIds);
});
});
}
}
extension RawRuleExt on RawRule {
Rule toRule([String? order]) {
return Rule(id: id, value: value, order: order);
}
}
extension RulesCompanionExt on Rule {
RulesCompanion toCompanion() {
return RulesCompanion.insert(id: Value(id), value: value);
}
}

63
lib/database/scripts.dart Normal file
View File

@@ -0,0 +1,63 @@
part of 'database.dart';
@DataClassName('RawScript')
class Scripts extends Table {
@override
String get tableName => 'scripts';
IntColumn get id => integer()();
TextColumn get label => text()();
DateTimeColumn get lastUpdateTime => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
@DriftAccessor(tables: [Scripts])
class ScriptsDao extends DatabaseAccessor<Database> with _$ScriptsDaoMixin {
ScriptsDao(super.attachedDatabase);
Selectable<Script> all() {
return scripts.select().map((item) => item.toScript());
}
Selectable<Script> get(int scriptId) {
final stmt = scripts.select();
stmt.where((t) => t.id.equals(scriptId));
return stmt.map((it) => it.toScript());
}
Future<void> setAll(Iterable<Script> scripts) async {
await batch((b) async {
await setAllWithBatch(b, scripts);
});
}
Future<void> setAllWithBatch(Batch batch, Iterable<Script> scripts) async {
final List<ScriptsCompanion> items = [];
final List<int> ids = [];
for (final script in scripts) {
ids.add(script.id);
items.add(script.toCompanion());
}
this.scripts.setAll(batch, items, deleteFilter: (t) => t.id.isNotIn(ids));
}
}
extension RawScriptExt on RawScript {
Script toScript() {
return Script(id: id, label: label, lastUpdateTime: lastUpdateTime);
}
}
extension ScriptsCompanionExt on Script {
ScriptsCompanion toCompanion() {
return ScriptsCompanion.insert(
id: Value(id),
label: label,
lastUpdateTime: lastUpdateTime,
);
}
}