diff --git a/Makefile b/Makefile index af60ee87..b6062d8f 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ DISTRIBUTOR_ARGS=--skip-clean --build-target $(TARGET) --build-dart-define sentr -get: +get: flutter pub get gen: diff --git a/lib/core/database/app_database.dart b/lib/core/database/app_database.dart index d9e86e37..4cf0f5be 100644 --- a/lib/core/database/app_database.dart +++ b/lib/core/database/app_database.dart @@ -21,7 +21,7 @@ class AppDatabase extends _$AppDatabase with InfraLogger { AppDatabase.connect() : super(openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration { @@ -48,6 +48,9 @@ class AppDatabase extends _$AppDatabase with InfraLogger { await m.createTable(schema.geoAssetEntries); await _prePopulateGeoAssets(); }, + from3To4: (m, schema) async { + await m.addColumn(profileEntries, profileEntries.testUrl); + }, ), beforeOpen: (details) async { if (kDebugMode) { diff --git a/lib/core/database/schema_versions.dart b/lib/core/database/schema_versions.dart index da7a4d2b..384bb0f5 100644 --- a/lib/core/database/schema_versions.dart +++ b/lib/core/database/schema_versions.dart @@ -39,78 +39,38 @@ final class _S2 extends i0.VersionedSchema { class Shape0 extends i0.VersionedTable { Shape0({required super.source, required super.alias}) : super.aliased(); - i1.GeneratedColumn get id => - columnsByName['id']! as i1.GeneratedColumn; - i1.GeneratedColumn get type => - columnsByName['type']! as i1.GeneratedColumn; - i1.GeneratedColumn get active => - columnsByName['active']! as i1.GeneratedColumn; - i1.GeneratedColumn get name => - columnsByName['name']! as i1.GeneratedColumn; - i1.GeneratedColumn get url => - columnsByName['url']! as i1.GeneratedColumn; - i1.GeneratedColumn get lastUpdate => - columnsByName['last_update']! as i1.GeneratedColumn; - i1.GeneratedColumn get updateInterval => - columnsByName['update_interval']! as i1.GeneratedColumn; - i1.GeneratedColumn get upload => - columnsByName['upload']! as i1.GeneratedColumn; - i1.GeneratedColumn get download => - columnsByName['download']! as i1.GeneratedColumn; - i1.GeneratedColumn get total => - columnsByName['total']! as i1.GeneratedColumn; - i1.GeneratedColumn get expire => - columnsByName['expire']! as i1.GeneratedColumn; - i1.GeneratedColumn get webPageUrl => - columnsByName['web_page_url']! as i1.GeneratedColumn; - i1.GeneratedColumn get supportUrl => - columnsByName['support_url']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get active => columnsByName['active']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get url => columnsByName['url']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastUpdate => columnsByName['last_update']! as i1.GeneratedColumn; + i1.GeneratedColumn get updateInterval => columnsByName['update_interval']! as i1.GeneratedColumn; + i1.GeneratedColumn get upload => columnsByName['upload']! as i1.GeneratedColumn; + i1.GeneratedColumn get download => columnsByName['download']! as i1.GeneratedColumn; + i1.GeneratedColumn get total => columnsByName['total']! as i1.GeneratedColumn; + i1.GeneratedColumn get expire => columnsByName['expire']! as i1.GeneratedColumn; + i1.GeneratedColumn get webPageUrl => columnsByName['web_page_url']! as i1.GeneratedColumn; + i1.GeneratedColumn get supportUrl => columnsByName['support_url']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_0(String aliasedName) => - i1.GeneratedColumn('id', aliasedName, false, - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_1(String aliasedName) => - i1.GeneratedColumn('type', aliasedName, false, - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_2(String aliasedName) => - i1.GeneratedColumn('active', aliasedName, false, - type: i1.DriftSqlType.bool, - defaultConstraints: i1.GeneratedColumn.constraintIsAlways( - 'CHECK ("active" IN (0, 1))')); -i1.GeneratedColumn _column_3(String aliasedName) => - i1.GeneratedColumn('name', aliasedName, false, - additionalChecks: i1.GeneratedColumn.checkTextLength( - minTextLength: 1, - ), - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_4(String aliasedName) => - i1.GeneratedColumn('url', aliasedName, true, - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_5(String aliasedName) => - i1.GeneratedColumn('last_update', aliasedName, false, - type: i1.DriftSqlType.dateTime); -i1.GeneratedColumn _column_6(String aliasedName) => - i1.GeneratedColumn('update_interval', aliasedName, true, - type: i1.DriftSqlType.int); -i1.GeneratedColumn _column_7(String aliasedName) => - i1.GeneratedColumn('upload', aliasedName, true, - type: i1.DriftSqlType.int); -i1.GeneratedColumn _column_8(String aliasedName) => - i1.GeneratedColumn('download', aliasedName, true, - type: i1.DriftSqlType.int); -i1.GeneratedColumn _column_9(String aliasedName) => - i1.GeneratedColumn('total', aliasedName, true, - type: i1.DriftSqlType.int); -i1.GeneratedColumn _column_10(String aliasedName) => - i1.GeneratedColumn('expire', aliasedName, true, - type: i1.DriftSqlType.dateTime); -i1.GeneratedColumn _column_11(String aliasedName) => - i1.GeneratedColumn('web_page_url', aliasedName, true, - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_12(String aliasedName) => - i1.GeneratedColumn('support_url', aliasedName, true, - type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_0(String aliasedName) => i1.GeneratedColumn('id', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_1(String aliasedName) => i1.GeneratedColumn('type', aliasedName, false, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_2(String aliasedName) => i1.GeneratedColumn('active', aliasedName, false, type: i1.DriftSqlType.bool, defaultConstraints: i1.GeneratedColumn.constraintIsAlways('CHECK ("active" IN (0, 1))')); +i1.GeneratedColumn _column_3(String aliasedName) => i1.GeneratedColumn('name', aliasedName, false, + additionalChecks: i1.GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_4(String aliasedName) => i1.GeneratedColumn('url', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_5(String aliasedName) => i1.GeneratedColumn('last_update', aliasedName, false, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_6(String aliasedName) => i1.GeneratedColumn('update_interval', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_7(String aliasedName) => i1.GeneratedColumn('upload', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_8(String aliasedName) => i1.GeneratedColumn('download', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_9(String aliasedName) => i1.GeneratedColumn('total', aliasedName, true, type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_10(String aliasedName) => i1.GeneratedColumn('expire', aliasedName, true, type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_11(String aliasedName) => i1.GeneratedColumn('web_page_url', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_12(String aliasedName) => i1.GeneratedColumn('support_url', aliasedName, true, type: i1.DriftSqlType.string); final class _S3 extends i0.VersionedSchema { _S3({required super.database}) : super(version: 3); @@ -170,37 +130,26 @@ final class _S3 extends i0.VersionedSchema { class Shape1 extends i0.VersionedTable { Shape1({required super.source, required super.alias}) : super.aliased(); - i1.GeneratedColumn get id => - columnsByName['id']! as i1.GeneratedColumn; - i1.GeneratedColumn get type => - columnsByName['type']! as i1.GeneratedColumn; - i1.GeneratedColumn get active => - columnsByName['active']! as i1.GeneratedColumn; - i1.GeneratedColumn get name => - columnsByName['name']! as i1.GeneratedColumn; - i1.GeneratedColumn get providerName => - columnsByName['provider_name']! as i1.GeneratedColumn; - i1.GeneratedColumn get version => - columnsByName['version']! as i1.GeneratedColumn; - i1.GeneratedColumn get lastCheck => - columnsByName['last_check']! as i1.GeneratedColumn; + i1.GeneratedColumn get id => columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get active => columnsByName['active']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get providerName => columnsByName['provider_name']! as i1.GeneratedColumn; + i1.GeneratedColumn get version => columnsByName['version']! as i1.GeneratedColumn; + i1.GeneratedColumn get lastCheck => columnsByName['last_check']! as i1.GeneratedColumn; } -i1.GeneratedColumn _column_13(String aliasedName) => - i1.GeneratedColumn('provider_name', aliasedName, false, - additionalChecks: i1.GeneratedColumn.checkTextLength( - minTextLength: 1, - ), - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_14(String aliasedName) => - i1.GeneratedColumn('version', aliasedName, true, - type: i1.DriftSqlType.string); -i1.GeneratedColumn _column_15(String aliasedName) => - i1.GeneratedColumn('last_check', aliasedName, true, - type: i1.DriftSqlType.dateTime); +i1.GeneratedColumn _column_13(String aliasedName) => i1.GeneratedColumn('provider_name', aliasedName, false, + additionalChecks: i1.GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_14(String aliasedName) => i1.GeneratedColumn('version', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_15(String aliasedName) => i1.GeneratedColumn('last_check', aliasedName, true, type: i1.DriftSqlType.dateTime); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, _S2 schema) from1To2, required Future Function(i1.Migrator m, _S3 schema) from2To3, + required Future Function(i1.Migrator m, _S3 schema) from3To4, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -214,6 +163,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from2To3(migrator, schema); return 3; + case 3: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from3To4(migrator, schema); + return 4; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -223,9 +177,11 @@ i0.MigrationStepWithVersion migrationSteps({ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, _S2 schema) from1To2, required Future Function(i1.Migrator m, _S3 schema) from2To3, + required Future Function(i1.Migrator m, _S3 schema) from3To4, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, from2To3: from2To3, + from3To4: from3To4, )); diff --git a/lib/core/database/schemas/drift_schema_v4.json b/lib/core/database/schemas/drift_schema_v4.json new file mode 100644 index 00000000..b884ae0d --- /dev/null +++ b/lib/core/database/schemas/drift_schema_v4.json @@ -0,0 +1,296 @@ +{ + "_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.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": [] + }, + { + "name": "test_url", + "getter_name": "testUrl", + "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.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" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/lib/core/database/tables/database_tables.dart b/lib/core/database/tables/database_tables.dart index 469f33de..84795efa 100644 --- a/lib/core/database/tables/database_tables.dart +++ b/lib/core/database/tables/database_tables.dart @@ -11,14 +11,14 @@ class ProfileEntries extends Table { TextColumn get name => text().withLength(min: 1)(); TextColumn get url => text().nullable()(); DateTimeColumn get lastUpdate => dateTime()(); - IntColumn get updateInterval => - integer().nullable().map(DurationTypeConverter())(); + IntColumn get updateInterval => integer().nullable().map(DurationTypeConverter())(); IntColumn get upload => integer().nullable()(); IntColumn get download => integer().nullable()(); IntColumn get total => integer().nullable()(); DateTimeColumn get expire => dateTime().nullable()(); TextColumn get webPageUrl => text().nullable()(); TextColumn get supportUrl => text().nullable()(); + TextColumn get testUrl => text().nullable()(); @override Set get primaryKey => {id}; diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart index 71267019..28740177 100644 --- a/lib/features/connection/data/connection_repository.dart +++ b/lib/features/connection/data/connection_repository.dart @@ -22,12 +22,14 @@ abstract interface class ConnectionRepository { String fileName, String profileName, bool disableMemoryLimit, + String? testUrl, ); TaskEither disconnect(); TaskEither reconnect( String fileName, String profileName, bool disableMemoryLimit, + String? testUrl, ); } @@ -101,11 +103,16 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con @visibleForTesting TaskEither applyConfigOption( SingboxConfigOption options, + String? testUrl, ) { return exceptionHandler( () { _configOptionsSnapshot = options; - return singbox.changeOptions(options).mapLeft(InvalidConfigOption.new).run(); + var newOptions = options; + if (testUrl != null) { + newOptions = options.copyWith(connectionTestUrl: testUrl); + } + return singbox.changeOptions(newOptions).mapLeft(InvalidConfigOption.new).run(); }, UnexpectedConnectionFailure.new, ); @@ -138,10 +145,11 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con String fileName, String profileName, bool disableMemoryLimit, + String? testUrl, ) { return TaskEither.Do( ($) async { - final options = await $(getConfigOption()); + var options = await $(getConfigOption()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", ); @@ -159,7 +167,7 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con }), ); await $(setup()); - await $(applyConfigOption(options)); + await $(applyConfigOption(options, testUrl)); return await $( singbox .start( @@ -203,23 +211,26 @@ class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements Con String fileName, String profileName, bool disableMemoryLimit, + String? testUrl, ) { - return exceptionHandler( - () async { - return getConfigOption() - .flatMap((options) => applyConfigOption(options)) - .andThen( - () => singbox - .restart( - profilePathResolver.file(fileName).path, - profileName, - disableMemoryLimit, - ) - .mapLeft(UnexpectedConnectionFailure.new), - ) - .run(); + return TaskEither.Do( + ($) async { + var options = await $(getConfigOption()); + loggy.info( + "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", + ); + + await $(applyConfigOption(options, testUrl)); + return await $( + singbox + .restart( + profilePathResolver.file(fileName).path, + profileName, + disableMemoryLimit, + ) + .mapLeft(UnexpectedConnectionFailure.new), + ); }, - UnexpectedConnectionFailure.new, - ); + ).handleExceptions(UnexpectedConnectionFailure.new); } } diff --git a/lib/features/connection/notifier/connection_notifier.dart b/lib/features/connection/notifier/connection_notifier.dart index e09cebfd..7d0df1ad 100644 --- a/lib/features/connection/notifier/connection_notifier.dart +++ b/lib/features/connection/notifier/connection_notifier.dart @@ -103,6 +103,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { profile.id, profile.name, ref.read(Preferences.disableMemoryLimit), + profile.testUrl, ) .mapLeft((err) { loggy.warning("error reconnecting", err); @@ -133,6 +134,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { activeProfile.id, activeProfile.name, ref.read(Preferences.disableMemoryLimit), + activeProfile.testUrl, ) .mapLeft((err) async { loggy.warning("error connecting", err); diff --git a/lib/features/profile/data/profile_data_mapper.dart b/lib/features/profile/data/profile_data_mapper.dart index 6abe7bc3..8c80ec99 100644 --- a/lib/features/profile/data/profile_data_mapper.dart +++ b/lib/features/profile/data/profile_data_mapper.dart @@ -5,8 +5,7 @@ import 'package:hiddify/features/profile/model/profile_entity.dart'; extension ProfileEntityMapper on ProfileEntity { ProfileEntriesCompanion toEntry() { return switch (this) { - RemoteProfileEntity(:final url, :final options, :final subInfo) => - ProfileEntriesCompanion.insert( + RemoteProfileEntity(:final url, :final options, :final subInfo) => ProfileEntriesCompanion.insert( id: id, type: ProfileType.remote, active: active, @@ -20,6 +19,7 @@ extension ProfileEntityMapper on ProfileEntity { expire: Value(subInfo?.expire), webPageUrl: Value(subInfo?.webPageUrl), supportUrl: Value(subInfo?.supportUrl), + testUrl: Value(testUrl), ), LocalProfileEntity() => ProfileEntriesCompanion.insert( id: id, @@ -41,6 +41,7 @@ extension RemoteProfileEntityMapper on RemoteProfileEntity { expire: Value(subInfo?.expire), webPageUrl: Value(subInfo?.webPageUrl), supportUrl: Value(subInfo?.supportUrl), + testUrl: Value(testUrl), ); } } @@ -73,12 +74,14 @@ extension ProfileEntryMapper on ProfileEntry { lastUpdate: lastUpdate, options: options, subInfo: subInfo, + testUrl: testUrl, ), ProfileType.local => LocalProfileEntity( id: id, active: active, name: name, lastUpdate: lastUpdate, + testUrl: testUrl, ), }; } diff --git a/lib/features/profile/data/profile_parser.dart b/lib/features/profile/data/profile_parser.dart index e3c18140..c42d44ac 100644 --- a/lib/features/profile/data/profile_parser.dart +++ b/lib/features/profile/data/profile_parser.dart @@ -24,14 +24,12 @@ abstract class ProfileParser { var name = ''; if (headers['profile-title'] case [final titleHeader]) { if (titleHeader.startsWith("base64:")) { - name = - utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); + name = utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); } else { name = titleHeader.trim(); } } - if (headers['content-disposition'] case [final contentDispositionHeader] - when name.isEmpty) { + if (headers['content-disposition'] case [final contentDispositionHeader] when name.isEmpty) { final regExp = RegExp('filename="([^"]*)"'); final match = regExp.firstMatch(contentDispositionHeader); if (match != null && match.groupCount >= 1) { @@ -52,19 +50,20 @@ abstract class ProfileParser { final updateInterval = Duration(hours: int.parse(updateIntervalStr)); options = ProfileOptions(updateInterval: updateInterval); } - + String? testUrl; + if (headers['test-url'] case [final testUrl_] when isUrl(testUrl_)) { + testUrl = testUrl_; + } SubscriptionInfo? subInfo; if (headers['subscription-userinfo'] case [final subInfoStr]) { subInfo = parseSubscriptionInfo(subInfoStr); } if (subInfo != null) { - if (headers['profile-web-page-url'] case [final profileWebPageUrl] - when isUrl(profileWebPageUrl)) { + if (headers['profile-web-page-url'] case [final profileWebPageUrl] when isUrl(profileWebPageUrl)) { subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl); } - if (headers['support-url'] case [final profileSupportUrl] - when isUrl(profileSupportUrl)) { + if (headers['support-url'] case [final profileSupportUrl] when isUrl(profileSupportUrl)) { subInfo = subInfo.copyWith(supportUrl: profileSupportUrl); } } @@ -77,23 +76,16 @@ abstract class ProfileParser { lastUpdate: DateTime.now(), options: options, subInfo: subInfo, + testUrl: testUrl, ); } static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) { final values = subInfoStr.split(';'); final map = { - for (final v in values) - v.split('=').first.trim(): - num.tryParse(v.split('=').second.trim())?.toInt(), + for (final v in values) v.split('=').first.trim(): num.tryParse(v.split('=').second.trim())?.toInt(), }; - if (map - case { - "upload": final upload?, - "download": final download?, - "total": var total, - "expire": var expire - }) { + if (map case {"upload": final upload?, "download": final download?, "total": var total, "expire": var expire}) { total = (total == null || total == 0) ? infiniteTrafficThreshold : total; expire = (expire == null || expire == 0) ? infiniteTimeThreshold : expire; return SubscriptionInfo( diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart index 36b4ad54..f6c2c12e 100644 --- a/lib/features/profile/data/profile_repository.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -282,6 +282,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil ? profilePatch.copyWith( name: Value(baseProfile.name), url: Value(baseProfile.url), + testUrl: Value(baseProfile.testUrl), updateInterval: Value(baseProfile.options?.updateInterval), ) : profilePatch, @@ -349,6 +350,7 @@ class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements Profil 'profile-update-interval', 'support-url', 'profile-web-page-url', + 'test-url', ]; @visibleForTesting diff --git a/lib/features/profile/model/profile_entity.dart b/lib/features/profile/model/profile_entity.dart index 144546be..8d84ba64 100644 --- a/lib/features/profile/model/profile_entity.dart +++ b/lib/features/profile/model/profile_entity.dart @@ -15,6 +15,7 @@ sealed class ProfileEntity with _$ProfileEntity { required String name, required String url, required DateTime lastUpdate, + String? testUrl, ProfileOptions? options, SubscriptionInfo? subInfo, }) = RemoteProfileEntity; @@ -24,6 +25,7 @@ sealed class ProfileEntity with _$ProfileEntity { required bool active, required String name, required DateTime lastUpdate, + String? testUrl, }) = LocalProfileEntity; } diff --git a/lib/singbox/service/platform_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart index e8376584..005ae4ed 100644 --- a/lib/singbox/service/platform_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -17,15 +17,11 @@ class PlatformSingboxService with InfraLogger implements SingboxService { static const channelPrefix = "com.hiddify.app"; static const methodChannel = MethodChannel("$channelPrefix/method"); - static const statusChannel = - EventChannel("$channelPrefix/service.status", JSONMethodCodec()); - static const alertsChannel = - EventChannel("$channelPrefix/service.alerts", JSONMethodCodec()); - static const statsChannel = - EventChannel("$channelPrefix/stats", JSONMethodCodec()); + static const statusChannel = EventChannel("$channelPrefix/service.status", JSONMethodCodec()); + static const alertsChannel = EventChannel("$channelPrefix/service.alerts", JSONMethodCodec()); + static const statsChannel = EventChannel("$channelPrefix/stats", JSONMethodCodec()); static const groupsChannel = EventChannel("$channelPrefix/groups"); - static const activeGroupsChannel = - EventChannel("$channelPrefix/active-groups"); + static const activeGroupsChannel = EventChannel("$channelPrefix/active-groups"); static const logsChannel = EventChannel("$channelPrefix/service.logs"); late final ValueStream _status; @@ -33,10 +29,8 @@ class PlatformSingboxService with InfraLogger implements SingboxService { @override Future init() async { loggy.debug("initializing"); - final status = - statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); - final alerts = - alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); + final status = statusChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); + final alerts = alertsChannel.receiveBroadcastStream().map(SingboxStatus.fromEvent); _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); await _status.first; @@ -250,9 +244,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService { @override Stream> watchLogs(String path) async* { - yield* logsChannel - .receiveBroadcastStream() - .map((event) => (event as List).map((e) => e as String).toList()); + yield* logsChannel.receiveBroadcastStream().map((event) => (event as List).map((e) => e as String).toList()); } @override diff --git a/lib/utils/validators.dart b/lib/utils/validators.dart index 7be7e631..0ffbcc5f 100644 --- a/lib/utils/validators.dart +++ b/lib/utils/validators.dart @@ -1,6 +1,7 @@ /// https://gist.github.com/dperini/729294 final _urlRegex = RegExp( - r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$", + // r"^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$", + r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', ); /// https://stackoverflow.com/a/12968117 diff --git a/libcore b/libcore index e6db5f23..77fe588e 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit e6db5f23487b93fa7e78f9f8912c767deca31067 +Subproject commit 77fe588eae4d49966cccfe57e9bb495b37933642