Add geo assets settings
This commit is contained in:
@@ -217,6 +217,15 @@
|
||||
"enableFakeDns": "Enable Fake DNS",
|
||||
"bypassLan": "Bypass Lan",
|
||||
"strictRoute": "Strict Route"
|
||||
},
|
||||
"geoAssets": {
|
||||
"pageTitle": "Routing Assets",
|
||||
"version": "Version ${version}",
|
||||
"fileMissing": "File Missing",
|
||||
"update": "Update",
|
||||
"download": "Download",
|
||||
"failureMsg": "Failed to update asset",
|
||||
"successMsg": "Successfully updated asset"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
@@ -283,6 +292,11 @@
|
||||
"badResponse": "Bad response",
|
||||
"connectionError": "Connection error",
|
||||
"badCertificate": "Bad certificate"
|
||||
},
|
||||
"geoAssets": {
|
||||
"unexpected": "Unexpected Error",
|
||||
"notUpdate": "No Update Available",
|
||||
"activeNotFound": "Active Geo Asset Not Found"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
|
||||
@@ -217,6 +217,15 @@
|
||||
"enableFakeDns": "Enable Fake DNS",
|
||||
"bypassLan": "Bypass Lan",
|
||||
"strictRoute": "Strict Route"
|
||||
},
|
||||
"geoAssets": {
|
||||
"pageTitle": "فایلهای مسیریابی",
|
||||
"version": "نسخه ${version}",
|
||||
"fileMissing": "فایل موجود نیست",
|
||||
"update": "به روز رسانی",
|
||||
"download": "دانلود",
|
||||
"failureMsg": "دارایی به روز نشد",
|
||||
"successMsg": "دارایی با موفقیت به روز شد"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
@@ -283,6 +292,11 @@
|
||||
"badResponse": "پاسخ نامعتبر",
|
||||
"connectionError": "خطای اتصال",
|
||||
"badCertificate": "خطای اعتبار سنجی"
|
||||
},
|
||||
"geoAssets": {
|
||||
"unexpected": "خطای غیرمنتظره",
|
||||
"notUpdate": "به روز رسانی موجود نیست",
|
||||
"activeNotFound": "Active Geo Asset یافت نشد"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
|
||||
@@ -217,6 +217,15 @@
|
||||
"enableFakeDns": "Использовать поддельную DNS",
|
||||
"bypassLan": "Обход локальной сети",
|
||||
"strictRoute": "Строгая маршрутизация"
|
||||
},
|
||||
"geoAssets": {
|
||||
"pageTitle": "Активы маршрутизации",
|
||||
"version": "Версия ${version}",
|
||||
"fileMissing": "Файл отсутствует",
|
||||
"update": "Обновлять",
|
||||
"download": "Скачать",
|
||||
"failureMsg": "Не удалось обновить объект.",
|
||||
"successMsg": "Объект успешно обновлен."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
@@ -283,6 +292,11 @@
|
||||
"badResponse": "Неправильный ответ",
|
||||
"connectionError": "Ошибка подключения",
|
||||
"badCertificate": "Неправильный сертификат"
|
||||
},
|
||||
"geoAssets": {
|
||||
"unexpected": "Неожиданная ошибка",
|
||||
"notUpdate": "Нет доступных обновлений",
|
||||
"activeNotFound": "Активный географический актив не найден"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
|
||||
@@ -217,6 +217,15 @@
|
||||
"enableFakeDns": "启用 Fake DNS",
|
||||
"bypassLan": "绕过局域网",
|
||||
"strictRoute": "严格路由"
|
||||
},
|
||||
"geoAssets": {
|
||||
"pageTitle": "路由资产",
|
||||
"version": "版本${version}",
|
||||
"fileMissing": "文件丢失",
|
||||
"update": "更新",
|
||||
"download": "下载",
|
||||
"failureMsg": "更新资产失败",
|
||||
"successMsg": "已成功更新资产"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
@@ -283,6 +292,11 @@
|
||||
"badResponse": "错误响应",
|
||||
"connectionError": "连接错误",
|
||||
"badCertificate": "证书无效"
|
||||
},
|
||||
"geoAssets": {
|
||||
"unexpected": "意外的错误",
|
||||
"notUpdate": "无可用更新",
|
||||
"activeNotFound": "未找到活动地理资产"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart';
|
||||
import 'package:hiddify/features/about/view/view.dart';
|
||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||
import 'package:hiddify/features/logs/view/view.dart';
|
||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart';
|
||||
import 'package:hiddify/features/settings/view/view.dart';
|
||||
|
||||
part 'desktop_routes.g.dart';
|
||||
@@ -48,6 +49,10 @@ part 'desktop_routes.g.dart';
|
||||
path: ConfigOptionsRoute.path,
|
||||
name: ConfigOptionsRoute.name,
|
||||
),
|
||||
TypedGoRoute<GeoAssetsRoute>(
|
||||
path: GeoAssetsRoute.path,
|
||||
name: GeoAssetsRoute.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<AboutRoute>(
|
||||
@@ -102,6 +107,21 @@ class ConfigOptionsRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class GeoAssetsRoute extends GoRouteData {
|
||||
const GeoAssetsRoute();
|
||||
static const path = 'routing-assets';
|
||||
static const name = 'Routing Assets';
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return const MaterialPage(
|
||||
fullscreenDialog: true,
|
||||
name: name,
|
||||
child: GeoAssetsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AboutRoute extends GoRouteData {
|
||||
const AboutRoute();
|
||||
static const path = '/about';
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hiddify/core/router/routes/shared_routes.dart';
|
||||
import 'package:hiddify/features/about/view/view.dart';
|
||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||
import 'package:hiddify/features/logs/view/view.dart';
|
||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart';
|
||||
import 'package:hiddify/features/settings/view/view.dart';
|
||||
|
||||
part 'mobile_routes.g.dart';
|
||||
@@ -47,6 +48,10 @@ part 'mobile_routes.g.dart';
|
||||
path: PerAppProxyRoute.path,
|
||||
name: PerAppProxyRoute.name,
|
||||
),
|
||||
TypedGoRoute<GeoAssetsRoute>(
|
||||
path: GeoAssetsRoute.path,
|
||||
name: GeoAssetsRoute.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<AboutRoute>(
|
||||
@@ -138,6 +143,23 @@ class PerAppProxyRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class GeoAssetsRoute extends GoRouteData {
|
||||
const GeoAssetsRoute();
|
||||
static const path = 'routing-assets';
|
||||
static const name = 'Routing Assets';
|
||||
|
||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return const MaterialPage(
|
||||
fullscreenDialog: true,
|
||||
name: name,
|
||||
child: GeoAssetsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AboutRoute extends GoRouteData {
|
||||
const AboutRoute();
|
||||
static const path = 'about';
|
||||
|
||||
@@ -8,11 +8,14 @@ import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/data/repository/app_repository_impl.dart';
|
||||
import 'package:hiddify/data/repository/config_options_store.dart';
|
||||
import 'package:hiddify/data/repository/geo_assets_repository.dart';
|
||||
import 'package:hiddify/data/repository/repository.dart';
|
||||
import 'package:hiddify/domain/app/app.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/domain/core_facade.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/domain/rules/geo_assets_repository.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -67,6 +70,44 @@ AppRepository appRepository(AppRepositoryRef ref) =>
|
||||
@Riverpod(keepAlive: true)
|
||||
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao(
|
||||
ref.watch(appDatabaseProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) {
|
||||
return GeoAssetsRepositoryImpl(
|
||||
geoAssetsDao: ref.watch(geoAssetsDaoProvider),
|
||||
dio: ref.watch(dioProvider),
|
||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<ConfigOptions> configOptions(ConfigOptionsRef ref) async {
|
||||
final geoAssets = await ref
|
||||
.watch(geoAssetsRepositoryProvider)
|
||||
.getActivePair()
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
final filesEditor = ref.watch(filesEditorServiceProvider);
|
||||
|
||||
final serviceMode = ref.watch(serviceModeStoreProvider);
|
||||
return ref.watch(configPreferencesProvider).copyWith(
|
||||
enableTun: serviceMode == ServiceMode.tun,
|
||||
setSystemProxy: serviceMode == ServiceMode.systemProxy,
|
||||
geoipPath: filesEditor.geoAssetRelativePath(
|
||||
geoAssets.geoip.providerName,
|
||||
geoAssets.geoip.fileName,
|
||||
),
|
||||
geositePath: filesEditor.geoAssetRelativePath(
|
||||
geoAssets.geosite.providerName,
|
||||
geoAssets.geosite.fileName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
||||
ref.watch(singboxServiceProvider),
|
||||
@@ -74,5 +115,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
||||
ref.watch(platformServicesProvider),
|
||||
ref.watch(clashApiProvider),
|
||||
ref.read(debugModeNotifierProvider),
|
||||
() => ref.read(configOptionsProvider),
|
||||
() => ref.read(configOptionsProvider.future),
|
||||
);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export 'geo_assets_dao.dart';
|
||||
export 'profiles_dao.dart';
|
||||
|
||||
36
lib/data/local/dao/geo_assets_dao.dart
Normal file
36
lib/data/local/dao/geo_assets_dao.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/data_mappers.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/data/local/tables.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
|
||||
part 'geo_assets_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GeoAssetEntries])
|
||||
class GeoAssetsDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$GeoAssetsDaoMixin, InfraLogger {
|
||||
GeoAssetsDao(super.db);
|
||||
|
||||
Future<GeoAsset?> getActive(GeoAssetType type) async {
|
||||
return (geoAssetEntries.select()
|
||||
..where((tbl) => tbl.active.equals(true))
|
||||
..where((tbl) => tbl.type.equalsValue(type))
|
||||
..limit(1))
|
||||
.map(GeoAssetMapper.fromEntry)
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<List<GeoAsset>> watchAll() {
|
||||
return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch();
|
||||
}
|
||||
|
||||
Future<void> edit(GeoAsset patch) async {
|
||||
await transaction(
|
||||
() async {
|
||||
await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id)))
|
||||
.write(patch.toCompanion());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
|
||||
extension ProfileMapper on Profile {
|
||||
ProfileEntriesCompanion toCompanion() {
|
||||
@@ -71,3 +72,29 @@ extension ProfileMapper on Profile {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension GeoAssetMapper on GeoAsset {
|
||||
GeoAssetEntriesCompanion toCompanion() {
|
||||
return GeoAssetEntriesCompanion.insert(
|
||||
id: id,
|
||||
type: type,
|
||||
active: active,
|
||||
name: name,
|
||||
providerName: providerName,
|
||||
version: Value(version),
|
||||
lastCheck: Value(lastCheck),
|
||||
);
|
||||
}
|
||||
|
||||
static GeoAsset fromEntry(GeoAssetEntry e) {
|
||||
return GeoAsset(
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
active: e.active,
|
||||
providerName: e.providerName,
|
||||
version: e.version,
|
||||
lastCheck: e.lastCheck,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,35 @@ import 'dart:io';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/data_mappers.dart';
|
||||
import 'package:hiddify/data/local/schema_versions.dart';
|
||||
import 'package:hiddify/data/local/tables.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [ProfileEntries], daos: [ProfilesDao])
|
||||
@DriftDatabase(
|
||||
tables: [ProfileEntries, GeoAssetEntries],
|
||||
daos: [ProfilesDao, GeoAssetsDao],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase({required QueryExecutor connection}) : super(connection);
|
||||
|
||||
AppDatabase.connect() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
int get schemaVersion => 3;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
await _prePopulateGeoAssets();
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
// add type column to profile entries table
|
||||
@@ -41,9 +47,22 @@ class AppDatabase extends _$AppDatabase {
|
||||
),
|
||||
);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.createTable(schema.geoAssetEntries);
|
||||
await _prePopulateGeoAssets();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _prePopulateGeoAssets() async {
|
||||
await transaction(() async {
|
||||
final geoAssets = defaultGeoAssets.map((e) => e.toCompanion());
|
||||
for (final geoAsset in geoAssets) {
|
||||
await into(geoAssetEntries).insert(geoAsset);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
|
||||
@@ -111,8 +111,96 @@ i1.GeneratedColumn<String> _column_11(String aliasedName) =>
|
||||
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('support_url', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
|
||||
final class _S3 extends i0.VersionedSchema {
|
||||
_S3({required super.database}) : super(version: 3);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
profileEntries,
|
||||
geoAssetEntries,
|
||||
];
|
||||
late final Shape0 profileEntries = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'profile_entries',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape1 geoAssetEntries = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'geo_asset_entries',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
'UNIQUE(name, provider_name)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
}
|
||||
|
||||
class Shape1 extends i0.VersionedTable {
|
||||
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get active =>
|
||||
columnsByName['active']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get providerName =>
|
||||
columnsByName['provider_name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get version =>
|
||||
columnsByName['version']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get lastCheck =>
|
||||
columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('provider_name', aliasedName, false,
|
||||
additionalChecks: i1.GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('version', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<DateTime> _column_15(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('last_check', aliasedName, true,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -121,6 +209,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from1To2(migrator, schema);
|
||||
return 2;
|
||||
case 2:
|
||||
final schema = _S3(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -129,8 +222,10 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
));
|
||||
|
||||
1
lib/data/local/schemas/drift_schema_v3.json
Normal file
1
lib/data/local/schemas/drift_schema_v3.json
Normal file
@@ -0,0 +1 @@
|
||||
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"profile_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<ProfileType>(ProfileType.values)","dart_type_name":"ProfileType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"url","getter_name":"url","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_update","getter_name":"lastUpdate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"update_interval","getter_name":"updateInterval","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"DurationTypeConverter()","dart_type_name":"Duration"}},{"name":"upload","getter_name":"upload","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download","getter_name":"download","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total","getter_name":"total","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expire","getter_name":"expire","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"web_page_url","getter_name":"webPageUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"support_url","getter_name":"supportUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"geo_asset_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<GeoAssetType>(GeoAssetType.values)","dart_type_name":"GeoAssetType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"provider_name","getter_name":"providerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"version","getter_name":"version","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_check","getter_name":"lastCheck","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"],"unique_keys":[["name","provider_name"]]}}]}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
|
||||
@DataClassName('ProfileEntry')
|
||||
class ProfileEntries extends Table {
|
||||
@@ -22,3 +23,22 @@ class ProfileEntries extends Table {
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DataClassName('GeoAssetEntry')
|
||||
class GeoAssetEntries extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get type => textEnum<GeoAssetType>()();
|
||||
BoolColumn get active => boolean()();
|
||||
TextColumn get name => text().withLength(min: 1)();
|
||||
TextColumn get providerName => text().withLength(min: 1)();
|
||||
TextColumn get version => text().nullable()();
|
||||
DateTimeColumn get lastCheck => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{name, providerName},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -147,19 +147,8 @@ ConfigOptions configPreferences(ConfigPreferencesRef ref) {
|
||||
urlTestInterval: ref.watch(urlTestIntervalStore),
|
||||
enableClashApi: ref.watch(enableClashApiStore),
|
||||
clashApiPort: ref.watch(clashApiPortStore),
|
||||
// enableTun: ref.watch(enableTunStore),
|
||||
// setSystemProxy: ref.watch(setSystemProxyStore),
|
||||
bypassLan: ref.watch(bypassLanStore),
|
||||
enableFakeDns: ref.watch(enableFakeDnsStore),
|
||||
rules: ref.watch(rulesProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
ConfigOptions configOptions(ConfigOptionsRef ref) {
|
||||
final serviceMode = ref.watch(serviceModeStoreProvider);
|
||||
return ref.watch(configPreferencesProvider).copyWith(
|
||||
enableTun: serviceMode == ServiceMode.tun,
|
||||
setSystemProxy: serviceMode == ServiceMode.systemProxy,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
||||
final PlatformServices platformServices;
|
||||
final ClashApi clash;
|
||||
final bool debug;
|
||||
final ConfigOptions Function() configOptions;
|
||||
final Future<ConfigOptions> Function() configOptions;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
@@ -95,9 +95,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
||||
String fileName,
|
||||
) {
|
||||
return exceptionHandler(
|
||||
() {
|
||||
() async {
|
||||
final configPath = filesEditor.configPath(fileName);
|
||||
final options = configOptions();
|
||||
final options = await configOptions();
|
||||
return setup()
|
||||
.andThen(() => changeConfigOptions(options))
|
||||
.andThen(
|
||||
@@ -119,7 +119,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final configPath = filesEditor.configPath(fileName);
|
||||
final options = configOptions();
|
||||
final options = await configOptions();
|
||||
loggy.info(
|
||||
"config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}",
|
||||
);
|
||||
@@ -159,9 +159,9 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
return exceptionHandler(
|
||||
() {
|
||||
() async {
|
||||
final configPath = filesEditor.configPath(fileName);
|
||||
return changeConfigOptions(configOptions())
|
||||
return changeConfigOptions(await configOptions())
|
||||
.andThen(
|
||||
() => singbox
|
||||
.restart(configPath, disableMemoryLimit)
|
||||
|
||||
140
lib/data/repository/geo_assets_repository.dart
Normal file
140
lib/data/repository/geo_assets_repository.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx_io.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
||||
import 'package:hiddify/domain/rules/geo_assets_repository.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
class GeoAssetsRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements GeoAssetsRepository {
|
||||
GeoAssetsRepositoryImpl({
|
||||
required this.geoAssetsDao,
|
||||
required this.dio,
|
||||
required this.filesEditor,
|
||||
});
|
||||
|
||||
final GeoAssetsDao geoAssetsDao;
|
||||
final Dio dio;
|
||||
final FilesEditorService filesEditor;
|
||||
|
||||
@override
|
||||
TaskEither<GeoAssetFailure, ({GeoAsset geoip, GeoAsset geosite})>
|
||||
getActivePair() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip);
|
||||
final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite);
|
||||
if (geoip == null || geosite == null) {
|
||||
return left(const GeoAssetFailure.activeAssetNotFound());
|
||||
}
|
||||
return right((geoip: geoip, geosite: geosite));
|
||||
},
|
||||
GeoAssetFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() {
|
||||
final persistedStream = geoAssetsDao.watchAll();
|
||||
final filesStream = _watchGeoFiles();
|
||||
|
||||
return Rx.combineLatest2(
|
||||
persistedStream,
|
||||
filesStream,
|
||||
(assets, files) => assets.map(
|
||||
(e) {
|
||||
final path = filesEditor.geoAssetPath(e.providerName, e.fileName);
|
||||
final file = files.firstOrNullWhere((e) => e.path == path);
|
||||
final stat = file?.statSync();
|
||||
return (e, stat?.size);
|
||||
},
|
||||
).toList(),
|
||||
).handleExceptions(GeoAssetUnexpectedFailure.new);
|
||||
}
|
||||
|
||||
Iterable<File> _geoFiles = [];
|
||||
Stream<Iterable<File>> _watchGeoFiles() async* {
|
||||
yield await _readGeoFiles();
|
||||
yield* Watcher(
|
||||
filesEditor.geoAssetsDir.path,
|
||||
pollingDelay: const Duration(seconds: 1),
|
||||
).events.asyncMap((event) async {
|
||||
if (event.type == ChangeType.MODIFY) {
|
||||
await _readGeoFiles();
|
||||
}
|
||||
return _geoFiles;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Iterable<File>> _readGeoFiles() async {
|
||||
return _geoFiles = Directory(filesEditor.geoAssetsDir.path)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((e) => e.extension == '.db');
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<GeoAssetFailure, Unit> update(GeoAsset geoAsset) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
loggy.debug(
|
||||
"checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
||||
);
|
||||
final response = await dio.get<Map>(geoAsset.repositoryUrl);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
return left(
|
||||
GeoAssetFailure.unexpected("invalid response", StackTrace.current),
|
||||
);
|
||||
}
|
||||
|
||||
final path =
|
||||
filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name);
|
||||
final tagName = response.data!['tag_name'] as String;
|
||||
loggy.debug("latest release of [${geoAsset.name}]: [$tagName]");
|
||||
if (tagName == geoAsset.version && await File(path).exists()) {
|
||||
await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now()));
|
||||
return left(const GeoAssetFailure.noUpdateAvailable());
|
||||
}
|
||||
|
||||
final assets = (response.data!['assets'] as List)
|
||||
.whereType<Map<String, dynamic>>();
|
||||
final asset =
|
||||
assets.firstOrNullWhere((e) => e["name"] == geoAsset.name);
|
||||
if (asset == null) {
|
||||
return left(
|
||||
GeoAssetFailure.unexpected(
|
||||
"couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
||||
StackTrace.current,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final downloadUrl = asset["browser_download_url"] as String;
|
||||
loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]");
|
||||
final tempPath = "$path.tmp";
|
||||
await File(path).parent.create(recursive: true);
|
||||
await dio.download(downloadUrl, tempPath);
|
||||
await File(tempPath).rename(path);
|
||||
|
||||
await geoAssetsDao.edit(
|
||||
geoAsset.copyWith(
|
||||
version: tagName,
|
||||
lastCheck: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
return right(unit);
|
||||
},
|
||||
GeoAssetFailure.unexpected,
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/domain/rules/geo_asset.dart
Normal file
51
lib/domain/rules/geo_asset.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'geo_asset.freezed.dart';
|
||||
part 'geo_asset.g.dart';
|
||||
|
||||
enum GeoAssetType { geoip, geosite }
|
||||
|
||||
typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size);
|
||||
|
||||
@freezed
|
||||
class GeoAsset with _$GeoAsset {
|
||||
const GeoAsset._();
|
||||
|
||||
const factory GeoAsset({
|
||||
required String id,
|
||||
required String name,
|
||||
required GeoAssetType type,
|
||||
required bool active,
|
||||
required String providerName,
|
||||
String? version,
|
||||
DateTime? lastCheck,
|
||||
}) = _GeoAsset;
|
||||
|
||||
factory GeoAsset.fromJson(Map<String, dynamic> json) =>
|
||||
_$GeoAssetFromJson(json);
|
||||
|
||||
String get fileName => name;
|
||||
|
||||
String get repositoryUrl =>
|
||||
"https://api.github.com/repos/$providerName/releases/latest";
|
||||
}
|
||||
|
||||
/// default geoip asset bundled with the app
|
||||
const defaultGeoip = GeoAsset(
|
||||
id: "sing-box-geoip",
|
||||
name: "geoip.db",
|
||||
type: GeoAssetType.geoip,
|
||||
active: true,
|
||||
providerName: "SagerNet/sing-geoip",
|
||||
);
|
||||
|
||||
/// default geosite asset bundled with the app
|
||||
const defaultGeosite = GeoAsset(
|
||||
id: "sing-box-geosite",
|
||||
name: "geosite.db",
|
||||
type: GeoAssetType.geosite,
|
||||
active: true,
|
||||
providerName: "SagerNet/sing-geosite",
|
||||
);
|
||||
|
||||
const defaultGeoAssets = [defaultGeoip, defaultGeosite];
|
||||
39
lib/domain/rules/geo_asset_failure.dart
Normal file
39
lib/domain/rules/geo_asset_failure.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
|
||||
part 'geo_asset_failure.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class GeoAssetFailure with _$GeoAssetFailure, Failure {
|
||||
const GeoAssetFailure._();
|
||||
|
||||
const factory GeoAssetFailure.unexpected([
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
]) = GeoAssetUnexpectedFailure;
|
||||
|
||||
@With<ExpectedFailure>()
|
||||
const factory GeoAssetFailure.noUpdateAvailable() = GeoAssetNoUpdateAvailable;
|
||||
|
||||
const factory GeoAssetFailure.activeAssetNotFound() =
|
||||
GeoAssetActiveAssetNotFound;
|
||||
|
||||
@override
|
||||
({String type, String? message}) present(TranslationsEn t) {
|
||||
return switch (this) {
|
||||
GeoAssetUnexpectedFailure() => (
|
||||
type: t.failure.geoAssets.unexpected,
|
||||
message: null,
|
||||
),
|
||||
GeoAssetNoUpdateAvailable() => (
|
||||
type: t.failure.geoAssets.notUpdate,
|
||||
message: null
|
||||
),
|
||||
GeoAssetActiveAssetNotFound() => (
|
||||
type: t.failure.geoAssets.activeNotFound,
|
||||
message: null,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
12
lib/domain/rules/geo_assets_repository.dart
Normal file
12
lib/domain/rules/geo_assets_repository.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
||||
|
||||
abstract interface class GeoAssetsRepository {
|
||||
TaskEither<GeoAssetFailure, ({GeoAsset geoip, GeoAsset geosite})>
|
||||
getActivePair();
|
||||
|
||||
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll();
|
||||
|
||||
TaskEither<GeoAssetFailure, Unit> update(GeoAsset geoAsset);
|
||||
}
|
||||
@@ -38,6 +38,8 @@ class ConfigOptions with _$ConfigOptions {
|
||||
@Default(false) bool bypassLan,
|
||||
@Default(false) bool enableFakeDns,
|
||||
@Default(true) bool independentDnsCache,
|
||||
@Default("geoip.db") String geoipPath,
|
||||
@Default("geosite.db") String geositePath,
|
||||
List<Rule>? rules,
|
||||
}) = _ConfigOptions;
|
||||
|
||||
|
||||
116
lib/features/settings/geo_assets/geo_asset_tile.dart
Normal file
116
lib/features/settings/geo_assets/geo_asset_tile.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart';
|
||||
import 'package:hiddify/utils/alerts.dart';
|
||||
import 'package:hiddify/utils/async_mutation.dart';
|
||||
import 'package:hiddify/utils/date_time_formatter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:humanizer/humanizer.dart';
|
||||
|
||||
class GeoAssetTile extends HookConsumerWidget {
|
||||
GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key})
|
||||
: geoAsset = geoAssetWithFileSize.$1,
|
||||
size = geoAssetWithFileSize.$2;
|
||||
|
||||
final GeoAsset geoAsset;
|
||||
final int? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final fileMissing = size == null;
|
||||
|
||||
final updateMutation = useMutation(
|
||||
initialOnFailure: (err) {
|
||||
if (err case GeoAssetNoUpdateAvailable()) {
|
||||
CustomToast(t.failure.geoAssets.notUpdate).show(context);
|
||||
} else {
|
||||
CustomAlertDialog.fromErr(
|
||||
t.presentError(err, action: t.settings.geoAssets.failureMsg),
|
||||
).show(context);
|
||||
}
|
||||
},
|
||||
initialOnSuccess: () =>
|
||||
CustomToast.success(t.settings.geoAssets.successMsg).show(context),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: geoAsset.name),
|
||||
if (geoAsset.providerName.isNotBlank)
|
||||
TextSpan(text: " (${geoAsset.providerName})"),
|
||||
],
|
||||
),
|
||||
),
|
||||
isThreeLine: true,
|
||||
subtitle: updateMutation.state.isInProgress
|
||||
? const LinearProgressIndicator()
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (geoAsset.version.isNotNullOrBlank)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
child: Text(
|
||||
t.settings.geoAssets.version(version: geoAsset.version!),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(),
|
||||
Flexible(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (fileMissing)
|
||||
TextSpan(
|
||||
text: t.settings.geoAssets.fileMissing,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
)
|
||||
else
|
||||
TextSpan(text: size?.bytes().toString()),
|
||||
if (geoAsset.lastCheck != null) ...[
|
||||
const TextSpan(text: " • "),
|
||||
TextSpan(text: geoAsset.lastCheck!.format()),
|
||||
],
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: geoAsset.active,
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: !updateMutation.state.isInProgress,
|
||||
onTap: () {
|
||||
if (updateMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
updateMutation.setFuture(
|
||||
ref
|
||||
.read(geoAssetsNotifierProvider.notifier)
|
||||
.updateGeoAsset(geoAsset),
|
||||
);
|
||||
},
|
||||
child: fileMissing
|
||||
? Text(t.settings.geoAssets.download)
|
||||
: Text(t.settings.geoAssets.update),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/features/settings/geo_assets/geo_assets_notifier.dart
Normal file
28
lib/features/settings/geo_assets/geo_assets_notifier.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'geo_assets_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger {
|
||||
@override
|
||||
Stream<List<GeoAssetWithFileSize>> build() {
|
||||
ref.disposeDelay(const Duration(seconds: 5));
|
||||
return ref
|
||||
.watch(geoAssetsRepositoryProvider)
|
||||
.watchAll()
|
||||
.map((event) => event.getOrElse((l) => throw l));
|
||||
}
|
||||
|
||||
Future<void> updateGeoAsset(GeoAsset geoAsset) async {
|
||||
await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse(
|
||||
(f) {
|
||||
loggy.warning("error updating profile", f);
|
||||
throw f;
|
||||
},
|
||||
).run();
|
||||
}
|
||||
}
|
||||
35
lib/features/settings/geo_assets/geo_assets_page.dart
Normal file
35
lib/features/settings/geo_assets/geo_assets_page.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart';
|
||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class GeoAssetsPage extends HookConsumerWidget {
|
||||
const GeoAssetsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final state = ref.watch(geoAssetsNotifierProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
title: Text(t.settings.geoAssets.pageTitle),
|
||||
),
|
||||
switch (state) {
|
||||
AsyncData(value: final geoAssets) => SliverList.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final geoAsset = geoAssets[index];
|
||||
return GeoAssetTile(geoAsset);
|
||||
},
|
||||
itemCount: geoAssets.length,
|
||||
),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,13 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
||||
await const ConfigOptionsRoute().push(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.geoAssets.pageTitle),
|
||||
leading: const Icon(Icons.folder),
|
||||
onTap: () async {
|
||||
await const GeoAssetsRoute().push(context);
|
||||
},
|
||||
),
|
||||
if (Platform.isAndroid) ...[
|
||||
ListTile(
|
||||
title: Text(t.settings.network.perAppProxyPageTitle),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
||||
import 'package:hiddify/gen/assets.gen.dart';
|
||||
import 'package:hiddify/services/platform_services.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -26,6 +28,9 @@ class FilesEditorService with InfraLogger {
|
||||
Directory(p.join(workingDir.path, Constants.configsFolderName));
|
||||
Directory get logsDir => dirs.workingDir;
|
||||
|
||||
Directory get geoAssetsDir =>
|
||||
Directory(p.join(workingDir.path, "geo-assets"));
|
||||
|
||||
File get appLogsFile => File(p.join(logsDir.path, "app.log"));
|
||||
File get coreLogsFile => File(p.join(logsDir.path, "box.log"));
|
||||
|
||||
@@ -48,6 +53,9 @@ class FilesEditorService with InfraLogger {
|
||||
if (!await configsDir.exists()) {
|
||||
await configsDir.create(recursive: true);
|
||||
}
|
||||
if (!await geoAssetsDir.exists()) {
|
||||
await geoAssetsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
if (await appLogsFile.exists()) {
|
||||
await appLogsFile.writeAsString("");
|
||||
@@ -77,6 +85,20 @@ class FilesEditorService with InfraLogger {
|
||||
return p.join(configsDir.path, "$fileName.json");
|
||||
}
|
||||
|
||||
String geoAssetPath(String providerName, String fileName) {
|
||||
final prefix = providerName.replaceAll("/", "-").toLowerCase();
|
||||
return p.join(
|
||||
geoAssetsDir.path,
|
||||
"$prefix${prefix.isBlank ? "" : "-"}$fileName",
|
||||
);
|
||||
}
|
||||
|
||||
/// geoasset's path relative to working directory
|
||||
String geoAssetRelativePath(String providerName, String fileName) {
|
||||
final fullPath = geoAssetPath(providerName, fileName);
|
||||
return p.relative(fullPath, from: workingDir.path);
|
||||
}
|
||||
|
||||
String tempConfigPath(String fileName) => configPath("temp_$fileName");
|
||||
|
||||
Future<void> deleteConfig(String fileName) {
|
||||
@@ -85,16 +107,18 @@ class FilesEditorService with InfraLogger {
|
||||
|
||||
Future<void> _populateGeoAssets() async {
|
||||
loggy.debug('populating geo assets');
|
||||
final geoipPath = p.join(workingDir.path, Constants.geoipFileName);
|
||||
final geoipPath =
|
||||
geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName);
|
||||
if (!await File(geoipPath).exists()) {
|
||||
final defaultGeoip = await rootBundle.load(Assets.core.geoip);
|
||||
await File(geoipPath).writeAsBytes(defaultGeoip.buffer.asInt8List());
|
||||
final bundledGeoip = await rootBundle.load(Assets.core.geoip);
|
||||
await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List());
|
||||
}
|
||||
|
||||
final geositePath = p.join(workingDir.path, Constants.geositeFileName);
|
||||
final geositePath =
|
||||
geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName);
|
||||
if (!await File(geositePath).exists()) {
|
||||
final defaultGeosite = await rootBundle.load(Assets.core.geosite);
|
||||
await File(geositePath).writeAsBytes(defaultGeosite.buffer.asInt8List());
|
||||
final bundledGeosite = await rootBundle.load(Assets.core.geosite);
|
||||
await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
libcore
2
libcore
Submodule libcore updated: 89ecc6bf12...2c2504f971
@@ -5,6 +5,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -14,8 +15,10 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, const {1, 2});
|
||||
throw MissingSchemaException(version, const {1, 2, 3});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
test/data/local/generated_migrations/schema_v3.dart
Normal file
168
test/data/local/generated_migrations/schema_v3.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@dart=2.12
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class ProfileEntries extends Table with TableInfo {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
ProfileEntries(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> type = GeneratedColumn<String>(
|
||||
'type', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<bool> active = GeneratedColumn<bool>(
|
||||
'active', aliasedName, false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))'));
|
||||
late final GeneratedColumn<String> name =
|
||||
GeneratedColumn<String>('name', aliasedName, false,
|
||||
additionalChecks: GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> url = GeneratedColumn<String>(
|
||||
'url', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<DateTime> lastUpdate = GeneratedColumn<DateTime>(
|
||||
'last_update', aliasedName, false,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<int> updateInterval = GeneratedColumn<int>(
|
||||
'update_interval', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<int> upload = GeneratedColumn<int>(
|
||||
'upload', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<int> download = GeneratedColumn<int>(
|
||||
'download', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<int> total = GeneratedColumn<int>(
|
||||
'total', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<DateTime> expire = GeneratedColumn<DateTime>(
|
||||
'expire', aliasedName, true,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<String> webPageUrl = GeneratedColumn<String>(
|
||||
'web_page_url', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<String> supportUrl = GeneratedColumn<String>(
|
||||
'support_url', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
type,
|
||||
active,
|
||||
name,
|
||||
url,
|
||||
lastUpdate,
|
||||
updateInterval,
|
||||
upload,
|
||||
download,
|
||||
total,
|
||||
expire,
|
||||
webPageUrl,
|
||||
supportUrl
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'profile_entries';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||
}
|
||||
|
||||
@override
|
||||
ProfileEntries createAlias(String alias) {
|
||||
return ProfileEntries(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoAssetEntries extends Table with TableInfo {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
GeoAssetEntries(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> type = GeneratedColumn<String>(
|
||||
'type', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<bool> active = GeneratedColumn<bool>(
|
||||
'active', aliasedName, false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))'));
|
||||
late final GeneratedColumn<String> name =
|
||||
GeneratedColumn<String>('name', aliasedName, false,
|
||||
additionalChecks: GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> providerName =
|
||||
GeneratedColumn<String>('provider_name', aliasedName, false,
|
||||
additionalChecks: GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> version = GeneratedColumn<String>(
|
||||
'version', aliasedName, true,
|
||||
type: DriftSqlType.string, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<DateTime> lastCheck = GeneratedColumn<DateTime>(
|
||||
'last_check', aliasedName, true,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, type, active, name, providerName, version, lastCheck];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'geo_asset_entries';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<GeneratedColumn>> get uniqueKeys => [
|
||||
{name, providerName},
|
||||
];
|
||||
@override
|
||||
Never map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
throw UnsupportedError('TableInfo.map in schema verification code');
|
||||
}
|
||||
|
||||
@override
|
||||
GeoAssetEntries createAlias(String alias) {
|
||||
return GeoAssetEntries(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV3 extends GeneratedDatabase {
|
||||
DatabaseAtV3(QueryExecutor e) : super(e);
|
||||
late final ProfileEntries profileEntries = ProfileEntries(this);
|
||||
late final GeoAssetEntries geoAssetEntries = GeoAssetEntries(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
||||
[profileEntries, geoAssetEntries];
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
@override
|
||||
DriftDatabaseOptions get options =>
|
||||
const DriftDatabaseOptions(storeDateTimeAsText: true);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import 'generated_migrations/schema.dart';
|
||||
|
||||
void main() {
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
@@ -16,5 +15,28 @@ void main() {
|
||||
final db = AppDatabase(connection: connection);
|
||||
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('upgrade from v2 to v3', () async {
|
||||
final connection = await verifier.startAt(2);
|
||||
final db = AppDatabase(connection: connection);
|
||||
|
||||
await verifier.migrateAndValidate(db, 3);
|
||||
|
||||
final prePopulated = await db.select(db.geoAssetEntries).get();
|
||||
await db.close();
|
||||
expect(prePopulated.length, equals(2));
|
||||
});
|
||||
|
||||
test('upgrade from v1 to v3 with pre-population', () async {
|
||||
final connection = await verifier.startAt(1);
|
||||
final db = AppDatabase(connection: connection);
|
||||
|
||||
await verifier.migrateAndValidate(db, 3);
|
||||
|
||||
final prePopulated = await db.select(db.geoAssetEntries).get();
|
||||
await db.close();
|
||||
expect(prePopulated.length, equals(2));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user