Add geo assets settings

This commit is contained in:
problematicconsumer
2023-11-17 21:30:09 +03:30
parent c33d555041
commit 82b8e1b6f0
30 changed files with 1003 additions and 29 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),
);

View File

@@ -1 +1,2 @@
export 'geo_assets_dao.dart';
export 'profiles_dao.dart';

View 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());
},
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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() {

View File

@@ -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,
));

View 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"]]}}]}

View File

@@ -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},
];
}

View File

@@ -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,
);
}

View File

@@ -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)

View 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,
);
}
}

View 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];

View 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,
),
};
}
}

View 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);
}

View File

@@ -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;

View 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),
),
];
},
),
);
}
}

View 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();
}
}

View 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(),
},
],
),
);
}
}

View File

@@ -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),

View File

@@ -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());
}
}
}

Submodule libcore updated: 89ecc6bf12...2c2504f971

View File

@@ -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});
}
}
}

View 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);
}

View File

@@ -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));
});
}