Add local profile
This commit is contained in:
@@ -239,6 +239,7 @@
|
||||
"profiles": {
|
||||
"unexpected": "Unexpected Error",
|
||||
"notFound": "Profile Not Found",
|
||||
"invalidUrl": "Invalid URL",
|
||||
"invalidConfig": "Invalid Configs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"sort": "مرتبسازی",
|
||||
"sortBy": "مرتبسازی براساس"
|
||||
},
|
||||
},
|
||||
"intro": {
|
||||
"termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید",
|
||||
"start": "شروع"
|
||||
@@ -239,6 +239,7 @@
|
||||
"profiles": {
|
||||
"unexpected": "خطای غیرمنتظره",
|
||||
"notFound": "پروفایل یافت نشد",
|
||||
"invalidUrl": "لینک نامعتبر",
|
||||
"invalidConfig": "کانفیگ غیر معتبر"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,31 @@ import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
|
||||
extension ProfileMapper on Profile {
|
||||
ProfileEntriesCompanion toCompanion() {
|
||||
return ProfileEntriesCompanion.insert(
|
||||
id: id,
|
||||
active: active,
|
||||
name: name,
|
||||
url: url,
|
||||
lastUpdate: lastUpdate,
|
||||
updateInterval: Value(options?.updateInterval),
|
||||
upload: Value(subInfo?.upload),
|
||||
download: Value(subInfo?.download),
|
||||
total: Value(subInfo?.total),
|
||||
expire: Value(subInfo?.expire),
|
||||
webPageUrl: Value(extra?.webPageUrl),
|
||||
supportUrl: Value(extra?.supportUrl),
|
||||
);
|
||||
return switch (this) {
|
||||
RemoteProfile(:final url, :final options, :final subInfo) =>
|
||||
ProfileEntriesCompanion.insert(
|
||||
id: id,
|
||||
type: ProfileType.remote,
|
||||
active: active,
|
||||
name: name,
|
||||
url: Value(url),
|
||||
lastUpdate: lastUpdate,
|
||||
updateInterval: Value(options?.updateInterval),
|
||||
upload: Value(subInfo?.upload),
|
||||
download: Value(subInfo?.download),
|
||||
total: Value(subInfo?.total),
|
||||
expire: Value(subInfo?.expire),
|
||||
webPageUrl: Value(subInfo?.webPageUrl),
|
||||
supportUrl: Value(subInfo?.supportUrl),
|
||||
),
|
||||
LocalProfile() => ProfileEntriesCompanion.insert(
|
||||
id: id,
|
||||
type: ProfileType.local,
|
||||
active: active,
|
||||
name: name,
|
||||
lastUpdate: lastUpdate,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static Profile fromEntry(ProfileEntry e) {
|
||||
@@ -36,26 +47,27 @@ extension ProfileMapper on Profile {
|
||||
download: e.download!,
|
||||
total: e.total!,
|
||||
expire: e.expire!,
|
||||
);
|
||||
}
|
||||
|
||||
ProfileExtra? extra;
|
||||
if (e.webPageUrl != null || e.supportUrl != null) {
|
||||
extra = ProfileExtra(
|
||||
webPageUrl: e.webPageUrl,
|
||||
supportUrl: e.supportUrl,
|
||||
);
|
||||
}
|
||||
|
||||
return Profile(
|
||||
id: e.id,
|
||||
active: e.active,
|
||||
name: e.name,
|
||||
url: e.url,
|
||||
lastUpdate: e.lastUpdate,
|
||||
options: options,
|
||||
subInfo: subInfo,
|
||||
extra: extra,
|
||||
);
|
||||
return switch (e.type) {
|
||||
ProfileType.remote => RemoteProfile(
|
||||
id: e.id,
|
||||
active: e.active,
|
||||
name: e.name,
|
||||
url: e.url!,
|
||||
lastUpdate: e.lastUpdate,
|
||||
options: options,
|
||||
subInfo: subInfo,
|
||||
),
|
||||
ProfileType.local => LocalProfile(
|
||||
id: e.id,
|
||||
active: e.active,
|
||||
name: e.name,
|
||||
lastUpdate: e.lastUpdate,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ 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/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/services/files_editor_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
@@ -17,7 +19,31 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase.connect() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
// add type column to profile entries table
|
||||
// make url column nullable
|
||||
from1To2: (m, schema) async {
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.profileEntries,
|
||||
columnTransformer: {
|
||||
schema.profileEntries.type: const Constant<String>("remote"),
|
||||
},
|
||||
newColumns: [schema.profileEntries.type],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
|
||||
136
lib/data/local/schema_versions.dart
Normal file
136
lib/data/local/schema_versions.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class _S2 extends i0.VersionedSchema {
|
||||
_S2({required super.database}) : super(version: 2);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
profileEntries,
|
||||
];
|
||||
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);
|
||||
}
|
||||
|
||||
class Shape0 extends i0.VersionedTable {
|
||||
Shape0({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 url =>
|
||||
columnsByName['url']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get lastUpdate =>
|
||||
columnsByName['last_update']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get updateInterval =>
|
||||
columnsByName['update_interval']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get upload =>
|
||||
columnsByName['upload']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get download =>
|
||||
columnsByName['download']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get total =>
|
||||
columnsByName['total']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get expire =>
|
||||
columnsByName['expire']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get webPageUrl =>
|
||||
columnsByName['web_page_url']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get supportUrl =>
|
||||
columnsByName['support_url']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('id', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('type', aliasedName, false,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<bool> _column_2(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>('active', aliasedName, false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("active" IN (0, 1))'));
|
||||
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||
additionalChecks: i1.GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('url', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('last_update', aliasedName, false,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('update_interval', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('upload', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('download', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
i1.GeneratedColumn<int> _column_9(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>('total', aliasedName, true,
|
||||
type: i1.DriftSqlType.int);
|
||||
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('expire', aliasedName, true,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
i1.GeneratedColumn<String> _column_11(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('web_page_url', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('support_url', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
case 1:
|
||||
final schema = _S2(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from1To2(migrator, schema);
|
||||
return 2;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
));
|
||||
160
lib/data/local/schemas/drift_schema_v1.json
Normal file
160
lib/data/local/schemas/drift_schema_v1.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"_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": "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": false,
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
174
lib/data/local/schemas/drift_schema_v2.json
Normal file
174
lib/data/local/schemas/drift_schema_v2.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"_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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
|
||||
@DataClassName('ProfileEntry')
|
||||
class ProfileEntries extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get type => textEnum<ProfileType>()();
|
||||
BoolColumn get active => boolean()();
|
||||
TextColumn get name => text().withLength(min: 1)();
|
||||
TextColumn get url => text()();
|
||||
TextColumn get url => text().nullable()();
|
||||
DateTimeColumn get lastUpdate => dateTime()();
|
||||
IntColumn get updateInterval =>
|
||||
integer().nullable().map(DurationTypeConverter())();
|
||||
|
||||
@@ -72,7 +72,7 @@ class ProfilesRepositoryImpl
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final existingProfile = await profilesDao.getProfileByUrl(url);
|
||||
if (existingProfile != null) {
|
||||
if (existingProfile case RemoteProfile()) {
|
||||
loggy.info("profile with url[$url] already exists, updating");
|
||||
final baseProfile = markAsActive
|
||||
? existingProfile.copyWith(active: true)
|
||||
@@ -105,7 +105,49 @@ class ProfilesRepositoryImpl
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> add(Profile baseProfile) {
|
||||
TaskEither<ProfileFailure, Unit> addByContent(
|
||||
String content, {
|
||||
required String name,
|
||||
bool markAsActive = false,
|
||||
}) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final profileId = const Uuid().v4();
|
||||
final tempPath = filesEditor.tempConfigPath(profileId);
|
||||
final path = filesEditor.configPath(profileId);
|
||||
try {
|
||||
await File(tempPath).writeAsString(content);
|
||||
final parseResult =
|
||||
await singbox.parseConfig(path, tempPath, false).run();
|
||||
return parseResult.fold(
|
||||
(l) async {
|
||||
loggy.warning("error parsing config: $l");
|
||||
return left(ProfileFailure.invalidConfig(l.msg));
|
||||
},
|
||||
(_) async {
|
||||
final profile = LocalProfile(
|
||||
id: profileId,
|
||||
active: markAsActive,
|
||||
name: name,
|
||||
lastUpdate: DateTime.now(),
|
||||
);
|
||||
await profilesDao.create(profile);
|
||||
return right(unit);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (await File(tempPath).exists()) await File(tempPath).delete();
|
||||
}
|
||||
},
|
||||
(error, stackTrace) {
|
||||
loggy.warning("error adding profile by content", error, stackTrace);
|
||||
return ProfileUnexpectedFailure(error, stackTrace);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> add(RemoteProfile baseProfile) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
return fetch(baseProfile.url, baseProfile.id)
|
||||
@@ -114,7 +156,6 @@ class ProfilesRepositoryImpl
|
||||
await profilesDao.create(
|
||||
baseProfile.copyWith(
|
||||
subInfo: remoteProfile.subInfo,
|
||||
extra: remoteProfile.extra,
|
||||
lastUpdate: DateTime.now(),
|
||||
),
|
||||
);
|
||||
@@ -131,7 +172,7 @@ class ProfilesRepositoryImpl
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> update(Profile baseProfile) {
|
||||
TaskEither<ProfileFailure, Unit> update(RemoteProfile baseProfile) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
loggy.debug(
|
||||
@@ -143,7 +184,6 @@ class ProfilesRepositoryImpl
|
||||
await profilesDao.edit(
|
||||
baseProfile.copyWith(
|
||||
subInfo: remoteProfile.subInfo,
|
||||
extra: remoteProfile.extra,
|
||||
lastUpdate: DateTime.now(),
|
||||
),
|
||||
);
|
||||
@@ -209,13 +249,13 @@ class ProfilesRepositoryImpl
|
||||
];
|
||||
|
||||
@visibleForTesting
|
||||
TaskEither<ProfileFailure, Profile> fetch(
|
||||
TaskEither<ProfileFailure, RemoteProfile> fetch(
|
||||
String url,
|
||||
String fileName,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final tempPath = filesEditor.configPath("temp_$fileName");
|
||||
final tempPath = filesEditor.tempConfigPath(fileName);
|
||||
final path = filesEditor.configPath(fileName);
|
||||
try {
|
||||
final response = await dio.download(url.trim(), tempPath);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -10,11 +11,13 @@ part 'profile.g.dart';
|
||||
|
||||
final _loggy = Loggy('Profile');
|
||||
|
||||
enum ProfileType { remote, local }
|
||||
|
||||
@freezed
|
||||
class Profile with _$Profile {
|
||||
sealed class Profile with _$Profile {
|
||||
const Profile._();
|
||||
|
||||
const factory Profile({
|
||||
const factory Profile.remote({
|
||||
required String id,
|
||||
required bool active,
|
||||
required String name,
|
||||
@@ -22,10 +25,19 @@ class Profile with _$Profile {
|
||||
required DateTime lastUpdate,
|
||||
ProfileOptions? options,
|
||||
SubscriptionInfo? subInfo,
|
||||
ProfileExtra? extra,
|
||||
}) = _Profile;
|
||||
}) = RemoteProfile;
|
||||
|
||||
factory Profile.fromResponse(String url, Map<String, List<String>> headers) {
|
||||
const factory Profile.local({
|
||||
required String id,
|
||||
required bool active,
|
||||
required String name,
|
||||
required DateTime lastUpdate,
|
||||
}) = LocalProfile;
|
||||
|
||||
static RemoteProfile fromResponse(
|
||||
String url,
|
||||
Map<String, List<String>> headers,
|
||||
) {
|
||||
_loggy.debug("Profile Headers: $headers");
|
||||
|
||||
final titleHeader = headers['profile-title']?.single;
|
||||
@@ -59,7 +71,7 @@ class Profile with _$Profile {
|
||||
if (title.isEmpty) {
|
||||
final part = url.split("/").lastOrNull;
|
||||
if (part != null) {
|
||||
final pattern = RegExp(r"\.(yaml|yml|txt)[\s\S]*");
|
||||
final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*");
|
||||
title = part.replaceFirst(pattern, "");
|
||||
}
|
||||
}
|
||||
@@ -79,15 +91,14 @@ class Profile with _$Profile {
|
||||
|
||||
final webPageUrlHeader = headers['profile-web-page-url']?.single;
|
||||
final supportUrlHeader = headers['support-url']?.single;
|
||||
ProfileExtra? extra;
|
||||
if (webPageUrlHeader != null || supportUrlHeader != null) {
|
||||
extra = ProfileExtra(
|
||||
webPageUrl: webPageUrlHeader,
|
||||
supportUrl: supportUrlHeader,
|
||||
if (subInfo != null) {
|
||||
subInfo = subInfo.copyWith(
|
||||
webPageUrl: isUrl(webPageUrlHeader ?? "") ? webPageUrlHeader : null,
|
||||
supportUrl: isUrl(supportUrlHeader ?? "") ? supportUrlHeader : null,
|
||||
);
|
||||
}
|
||||
|
||||
return Profile(
|
||||
return RemoteProfile(
|
||||
id: const Uuid().v4(),
|
||||
active: false,
|
||||
name: title.isBlank ? "Remote Profile" : title,
|
||||
@@ -95,7 +106,6 @@ class Profile with _$Profile {
|
||||
lastUpdate: DateTime.now(),
|
||||
options: options,
|
||||
subInfo: subInfo,
|
||||
extra: extra,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,17 +123,6 @@ class ProfileOptions with _$ProfileOptions {
|
||||
_$ProfileOptionsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfileExtra with _$ProfileExtra {
|
||||
const factory ProfileExtra({
|
||||
String? webPageUrl,
|
||||
String? supportUrl,
|
||||
}) = _ProfileExtra;
|
||||
|
||||
factory ProfileExtra.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfileExtraFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SubscriptionInfo with _$SubscriptionInfo {
|
||||
const SubscriptionInfo._();
|
||||
@@ -134,6 +133,8 @@ class SubscriptionInfo with _$SubscriptionInfo {
|
||||
@JsonKey(fromJson: _fromJsonTotal, defaultValue: 9223372036854775807)
|
||||
required int total,
|
||||
@JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) required DateTime expire,
|
||||
String? webPageUrl,
|
||||
String? supportUrl,
|
||||
}) = _SubscriptionInfo;
|
||||
|
||||
bool get isExpired => expire <= DateTime.now();
|
||||
|
||||
@@ -15,6 +15,9 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
|
||||
|
||||
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
|
||||
|
||||
@With<ExpectedException>()
|
||||
const factory ProfileFailure.invalidUrl() = ProfileInvalidUrlFailure;
|
||||
|
||||
const factory ProfileFailure.invalidConfig([String? message]) =
|
||||
ProfileInvalidConfigFailure;
|
||||
|
||||
@@ -29,6 +32,10 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
|
||||
type: t.failure.profiles.notFound,
|
||||
message: null
|
||||
),
|
||||
ProfileInvalidUrlFailure() => (
|
||||
type: t.failure.profiles.invalidUrl,
|
||||
message: null,
|
||||
),
|
||||
ProfileInvalidConfigFailure(:final message) => (
|
||||
type: t.failure.profiles.invalidConfig,
|
||||
message: message
|
||||
|
||||
@@ -19,9 +19,15 @@ abstract class ProfilesRepository {
|
||||
bool markAsActive = false,
|
||||
});
|
||||
|
||||
TaskEither<ProfileFailure, Unit> add(Profile baseProfile);
|
||||
TaskEither<ProfileFailure, Unit> addByContent(
|
||||
String content, {
|
||||
required String name,
|
||||
bool markAsActive = false,
|
||||
});
|
||||
|
||||
TaskEither<ProfileFailure, Unit> update(Profile baseProfile);
|
||||
TaskEither<ProfileFailure, Unit> add(RemoteProfile baseProfile);
|
||||
|
||||
TaskEither<ProfileFailure, Unit> update(RemoteProfile baseProfile);
|
||||
|
||||
TaskEither<ProfileFailure, Unit> edit(Profile profile);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -16,16 +15,4 @@ class ActiveProfile extends _$ActiveProfile with AppLogger {
|
||||
.watchActiveProfile()
|
||||
.map((event) => event.getOrElse((l) => throw l));
|
||||
}
|
||||
|
||||
Future<Unit?> updateProfile() async {
|
||||
if (state case AsyncData(value: final profile?)) {
|
||||
loggy.debug("updating active profile");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,10 @@ class ProfileTile extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
|
||||
final subInfo = profile.subInfo;
|
||||
final subInfo = switch (profile) {
|
||||
RemoteProfile(:final subInfo) => subInfo,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
final effectiveMargin = isMain
|
||||
? const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
||||
@@ -60,17 +63,19 @@ class ProfileTile extends HookConsumerWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
if (profile is RemoteProfile || !isMain) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: ProfileActionButton(profile, !isMain),
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
color: effectiveOutlineColor,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
@@ -177,7 +182,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
||||
CustomToast.success(t.profile.update.successMsg).show(context),
|
||||
);
|
||||
|
||||
if (!showAllActions) {
|
||||
if (profile case RemoteProfile() when !showAllActions) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: !updateProfileMutation.state.isInProgress,
|
||||
@@ -191,7 +196,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
||||
updateProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.updateProfile(profile),
|
||||
.updateProfile(profile as RemoteProfile),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.update),
|
||||
@@ -250,20 +255,21 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
||||
return MenuAnchor(
|
||||
builder: builder,
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.update),
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onPressed: () {
|
||||
if (updateProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
updateProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.updateProfile(profile),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (profile case RemoteProfile())
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.update),
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onPressed: () {
|
||||
if (updateProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
updateProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.updateProfile(profile as RemoteProfile),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.edit),
|
||||
child: Text(t.profile.edit.buttonTxt),
|
||||
|
||||
@@ -19,7 +19,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
}) async {
|
||||
if (id == 'new') {
|
||||
return ProfileDetailState(
|
||||
profile: Profile(
|
||||
profile: RemoteProfile(
|
||||
id: const Uuid().v4(),
|
||||
active: true,
|
||||
name: profileName ?? "",
|
||||
@@ -52,15 +52,20 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
if (state case AsyncData(:final value)) {
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
profile: value.profile.copyWith(
|
||||
name: name ?? value.profile.name,
|
||||
url: url ?? value.profile.url,
|
||||
options: updateInterval == null
|
||||
? value.profile.options
|
||||
: updateInterval.fold(
|
||||
() => null,
|
||||
(t) => ProfileOptions(updateInterval: Duration(hours: t)),
|
||||
),
|
||||
profile: value.profile.map(
|
||||
remote: (rp) => rp.copyWith(
|
||||
name: name ?? rp.name,
|
||||
url: url ?? rp.url,
|
||||
options: updateInterval == null
|
||||
? rp.options
|
||||
: updateInterval.fold(
|
||||
() => null,
|
||||
(t) => ProfileOptions(
|
||||
updateInterval: Duration(hours: t),
|
||||
),
|
||||
),
|
||||
),
|
||||
local: (lp) => lp.copyWith(name: name ?? lp.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -71,24 +76,33 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value.save.isInProgress) return;
|
||||
final profile = value.profile;
|
||||
loggy.debug(
|
||||
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
||||
);
|
||||
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||
Either<ProfileFailure, Unit>? failureOrSuccess;
|
||||
if (profile.name.isBlank || profile.url.isBlank) {
|
||||
loggy.debug('profile save: invalid arguments');
|
||||
} else if (value.isEditing) {
|
||||
if (_originalProfile?.url == profile.url) {
|
||||
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||
switch (profile) {
|
||||
case RemoteProfile():
|
||||
loggy.debug(
|
||||
'saving profile, url: [${profile.url}], name: [${profile.name}]',
|
||||
);
|
||||
if (profile.name.isBlank || profile.url.isBlank) {
|
||||
loggy.debug('profile save: invalid arguments');
|
||||
} else if (value.isEditing) {
|
||||
if (_originalProfile case RemoteProfile(:final url)
|
||||
when url == profile.url) {
|
||||
loggy.debug('editing profile');
|
||||
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||
} else {
|
||||
loggy.debug('updating profile');
|
||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
||||
}
|
||||
} else {
|
||||
loggy.debug('adding profile, url: [${profile.url}]');
|
||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
||||
}
|
||||
case LocalProfile() when value.isEditing:
|
||||
loggy.debug('editing profile');
|
||||
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||
} else {
|
||||
loggy.debug('updating profile');
|
||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
||||
}
|
||||
} else {
|
||||
loggy.debug('adding profile, url: [${profile.url}]');
|
||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
||||
default:
|
||||
loggy.warning("local profile can't be added manually");
|
||||
}
|
||||
state = AsyncData(
|
||||
value.copyWith(
|
||||
@@ -105,12 +119,17 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
||||
|
||||
Future<void> updateProfile() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
loggy.debug('updating profile');
|
||||
if (value.profile case LocalProfile()) {
|
||||
loggy.warning("local profile can't be updated");
|
||||
return;
|
||||
}
|
||||
if (value.update.isInProgress || !value.isEditing) return;
|
||||
final profile = value.profile;
|
||||
loggy.debug('updating profile');
|
||||
state = AsyncData(value.copyWith(update: const MutationInProgress()));
|
||||
final failureOrUpdatedProfile = await _profilesRepo
|
||||
.update(profile)
|
||||
.update(profile as RemoteProfile)
|
||||
.flatMap((_) => _profilesRepo.get(id))
|
||||
.run();
|
||||
state = AsyncData(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:fpdart/fpdart.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/features/common/confirmation_dialogs.dart';
|
||||
import 'package:hiddify/features/profile_detail/notifier/notifier.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
@@ -93,12 +94,13 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onTap: () async {
|
||||
await notifier.updateProfile();
|
||||
},
|
||||
),
|
||||
if (state.profile case RemoteProfile())
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.update.buttonTxt),
|
||||
onTap: () async {
|
||||
await notifier.updateProfile();
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.profile.delete.buttonTxt),
|
||||
onTap: () async {
|
||||
@@ -140,52 +142,55 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
||||
hint: t.profile.detailsForm.nameHint,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
if (state.profile
|
||||
case RemoteProfile(:final url, :final options)) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: CustomTextFormField(
|
||||
initialValue: url,
|
||||
onChanged: (value) =>
|
||||
notifier.setField(url: value),
|
||||
validator: (value) =>
|
||||
(value != null && !isUrl(value))
|
||||
? t.profile.detailsForm.invalidUrlMsg
|
||||
: null,
|
||||
label: t.profile.detailsForm.urlLabel,
|
||||
hint: t.profile.detailsForm.urlHint,
|
||||
),
|
||||
),
|
||||
child: CustomTextFormField(
|
||||
initialValue: state.profile.url,
|
||||
onChanged: (value) => notifier.setField(url: value),
|
||||
validator: (value) =>
|
||||
(value != null && !isUrl(value))
|
||||
? t.profile.detailsForm.invalidUrlMsg
|
||||
: null,
|
||||
label: t.profile.detailsForm.urlLabel,
|
||||
hint: t.profile.detailsForm.urlHint,
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.updateInterval),
|
||||
subtitle: Text(
|
||||
options?.updateInterval.toApproximateTime(
|
||||
isRelativeToNow: false,
|
||||
) ??
|
||||
t.general.toggle.disabled,
|
||||
),
|
||||
leading: const Icon(Icons.update),
|
||||
onTap: () async {
|
||||
final intervalInHours = await SettingsInputDialog(
|
||||
title: t.profile.detailsForm
|
||||
.updateIntervalDialogTitle,
|
||||
initialValue: options?.updateInterval.inHours,
|
||||
optionalAction: (
|
||||
t.general.state.disable,
|
||||
() =>
|
||||
notifier.setField(updateInterval: none()),
|
||||
),
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (intervalInHours == null) return;
|
||||
notifier.setField(
|
||||
updateInterval: optionOf(intervalInHours),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.updateInterval),
|
||||
subtitle: Text(
|
||||
state.profile.options?.updateInterval
|
||||
.toApproximateTime(
|
||||
isRelativeToNow: false,
|
||||
) ??
|
||||
t.general.toggle.disabled,
|
||||
),
|
||||
leading: const Icon(Icons.update),
|
||||
onTap: () async {
|
||||
final intervalInHours = await SettingsInputDialog(
|
||||
title: t.profile.detailsForm
|
||||
.updateIntervalDialogTitle,
|
||||
initialValue:
|
||||
state.profile.options?.updateInterval.inHours,
|
||||
optionalAction: (
|
||||
t.general.state.disable,
|
||||
() => notifier.setField(updateInterval: none()),
|
||||
),
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (intervalInHours == null) return;
|
||||
notifier.setField(
|
||||
updateInterval: optionOf(intervalInHours),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (state.isEditing)
|
||||
ListTile(
|
||||
title: Text(t.profile.detailsForm.lastUpdate),
|
||||
|
||||
@@ -49,21 +49,39 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger {
|
||||
}).run();
|
||||
}
|
||||
|
||||
Future<Unit> addProfile(String url) async {
|
||||
Future<Unit> addProfile(String rawInput) async {
|
||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||
final markAsActive =
|
||||
activeProfile == null || ref.read(markNewProfileActiveProvider);
|
||||
loggy.debug("adding profile, url: [$url]");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByUrl(url, markAsActive: markAsActive)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
if (LinkParser.parse(rawInput) case (final link)?) {
|
||||
loggy.debug("adding profile, url: [${link.url}]");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByUrl(link.url, markAsActive: markAsActive)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
} else if (LinkParser.protocol(rawInput) case (final parsed)?) {
|
||||
loggy.debug("adding profile, content");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.addByContent(
|
||||
parsed.content,
|
||||
name: parsed.name,
|
||||
markAsActive: markAsActive,
|
||||
)
|
||||
.getOrElse((l) {
|
||||
loggy.warning("failed to add profile: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
} else {
|
||||
loggy.debug("invalid content");
|
||||
throw const ProfileInvalidUrlFailure();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Unit?> updateProfile(Profile profile) async {
|
||||
Future<Unit?> updateProfile(RemoteProfile profile) async {
|
||||
loggy.debug("updating profile");
|
||||
return ref
|
||||
.read(profilesRepositoryProvider)
|
||||
|
||||
@@ -50,20 +50,22 @@ class ProfilesUpdateNotifier extends _$ProfilesUpdateNotifier with AppLogger {
|
||||
await ref.read(profilesRepositoryProvider).watchAll().first;
|
||||
if (failureOrProfiles case Right(value: final profiles)) {
|
||||
for (final profile in profiles) {
|
||||
loggy.debug("checking profile: [${profile.name}]");
|
||||
final updateInterval = profile.options?.updateInterval;
|
||||
if (updateInterval != null &&
|
||||
updateInterval <=
|
||||
DateTime.now().difference(profile.lastUpdate)) {
|
||||
final failureOrSuccess = await ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.run();
|
||||
state = AsyncData(
|
||||
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
||||
);
|
||||
} else {
|
||||
loggy.debug("skipping profile: [${profile.name}]");
|
||||
if (profile case RemoteProfile()) {
|
||||
loggy.debug("checking profile: [${profile.name}]");
|
||||
final updateInterval = profile.options?.updateInterval;
|
||||
if (updateInterval != null &&
|
||||
updateInterval <=
|
||||
DateTime.now().difference(profile.lastUpdate)) {
|
||||
final failureOrSuccess = await ref
|
||||
.read(profilesRepositoryProvider)
|
||||
.update(profile)
|
||||
.run();
|
||||
state = AsyncData(
|
||||
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
||||
);
|
||||
} else {
|
||||
loggy.debug("skipping profile: [${profile.name}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/features/common/qr_code_scanner_screen.dart';
|
||||
import 'package:hiddify/features/profiles/notifier/notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -29,8 +30,13 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
final addProfileMutation = useMutation(
|
||||
initialOnFailure: (err) {
|
||||
mutationTriggered.value = false;
|
||||
// CustomToast.error(t.presentError(err)).show(context);
|
||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
||||
if (err case ProfileInvalidUrlFailure()) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
} else {
|
||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
||||
}
|
||||
},
|
||||
initialOnSuccess: () {
|
||||
CustomToast.success(t.profile.save.successMsg).show(context);
|
||||
@@ -102,24 +108,15 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
size: buttonWidth,
|
||||
onTap: () async {
|
||||
final captureResult =
|
||||
await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final link =
|
||||
LinkParser.parse(captureResult?.text ?? '');
|
||||
if (link != null && context.mounted) {
|
||||
if (addProfileMutation.state.isInProgress) return;
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(link.url),
|
||||
);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
await Clipboard.getData(Clipboard.kTextPlain)
|
||||
.then((value) => value?.text ?? '');
|
||||
if (addProfileMutation.state.isInProgress) return;
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(captureResult),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(buttonsGap),
|
||||
@@ -134,24 +131,15 @@ class AddProfileModal extends HookConsumerWidget {
|
||||
await const QRCodeScannerScreen()
|
||||
.open(context);
|
||||
if (captureResult == null) return;
|
||||
final link = LinkParser.simple(captureResult);
|
||||
if (link != null && context.mounted) {
|
||||
if (addProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(link.url),
|
||||
);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
CustomToast.error(
|
||||
t.profile.add.invalidUrlMsg,
|
||||
).show(context);
|
||||
}
|
||||
if (addProfileMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
mutationTriggered.value = true;
|
||||
addProfileMutation.setFuture(
|
||||
ref
|
||||
.read(profilesNotifierProvider.notifier)
|
||||
.addProfile(captureResult),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
|
||||
@@ -77,6 +77,8 @@ class FilesEditorService with InfraLogger {
|
||||
return p.join(_configsDir.path, "$fileName.json");
|
||||
}
|
||||
|
||||
String tempConfigPath(String fileName) => configPath("temp_$fileName");
|
||||
|
||||
Future<void> deleteConfig(String fileName) {
|
||||
return File(configPath(fileName)).delete();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/utils/validators.dart';
|
||||
|
||||
typedef ProfileLink = ({String url, String name});
|
||||
|
||||
// TODO: test and improve
|
||||
abstract class LinkParser {
|
||||
static const protocols = ['clash', 'clashmeta', 'sing-box', 'hiddify'];
|
||||
// protocols schemas
|
||||
static const protocols = {'clash', 'clashmeta', 'sing-box', 'hiddify'};
|
||||
static const rawProtocols = {'vmess', 'vless', 'trojan', 'ss', 'tuic'};
|
||||
|
||||
static ProfileLink? parse(String link) {
|
||||
return simple(link) ?? deep(link);
|
||||
@@ -13,24 +18,39 @@ abstract class LinkParser {
|
||||
static ProfileLink? simple(String link) {
|
||||
if (!isUrl(link)) return null;
|
||||
final uri = Uri.parse(link.trim());
|
||||
final params = uri.queryParameters;
|
||||
return (
|
||||
url: uri.toString(),
|
||||
// .replace(queryParameters: {})
|
||||
// .toString()
|
||||
// .removeSuffix('?')
|
||||
// .split('&')
|
||||
// .first,
|
||||
name: params['name'] ?? '',
|
||||
name: uri.queryParameters['name'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
static ({String content, String name})? protocol(String content) {
|
||||
final lines = safeDecodeBase64(content).split('\n');
|
||||
for (final line in lines) {
|
||||
final uri = Uri.tryParse(line);
|
||||
if (uri == null) continue;
|
||||
final fragment =
|
||||
uri.hasFragment ? Uri.decodeComponent(uri.fragment) : null;
|
||||
final name = switch (uri.scheme) {
|
||||
'ss' => fragment ?? ProxyType.shadowSocks.label,
|
||||
'vless' => fragment ?? ProxyType.vless.label,
|
||||
'tuic' => fragment ?? ProxyType.tuic.label,
|
||||
'vmess' => ProxyType.vmess.label,
|
||||
_ => null,
|
||||
};
|
||||
if (name != null) {
|
||||
return (content: content, name: name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static ProfileLink? deep(String link) {
|
||||
final uri = Uri.tryParse(link.trim());
|
||||
if (uri == null || !uri.hasScheme || !uri.hasAuthority) return null;
|
||||
final queryParams = uri.queryParameters;
|
||||
switch (uri.scheme) {
|
||||
case 'clash' || 'clashmeta':
|
||||
case 'clash' || 'clashmeta' when uri.authority == 'install-config':
|
||||
if (uri.authority != 'install-config' ||
|
||||
!queryParams.containsKey('url')) return null;
|
||||
return (url: queryParams['url']!, name: queryParams['name'] ?? '');
|
||||
@@ -48,3 +68,11 @@ abstract class LinkParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String safeDecodeBase64(String str) {
|
||||
try {
|
||||
return utf8.decode(base64Decode(str));
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
21
test/data/local/generated_migrations/schema.dart
Normal file
21
test/data/local/generated_migrations/schema.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@dart=2.12
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, const {1, 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
100
test/data/local/generated_migrations/schema_v1.dart
Normal file
100
test/data/local/generated_migrations/schema_v1.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
// 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<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, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
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,
|
||||
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 DatabaseAtV1 extends GeneratedDatabase {
|
||||
DatabaseAtV1(QueryExecutor e) : super(e);
|
||||
late final ProfileEntries profileEntries = ProfileEntries(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [profileEntries];
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
@override
|
||||
DriftDatabaseOptions get options =>
|
||||
const DriftDatabaseOptions(storeDateTimeAsText: true);
|
||||
}
|
||||
104
test/data/local/generated_migrations/schema_v2.dart
Normal file
104
test/data/local/generated_migrations/schema_v2.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 DatabaseAtV2 extends GeneratedDatabase {
|
||||
DatabaseAtV2(QueryExecutor e) : super(e);
|
||||
late final ProfileEntries profileEntries = ProfileEntries(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [profileEntries];
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
@override
|
||||
DriftDatabaseOptions get options =>
|
||||
const DriftDatabaseOptions(storeDateTimeAsText: true);
|
||||
}
|
||||
20
test/data/local/migrations_test.dart
Normal file
20
test/data/local/migrations_test.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:drift_dev/api/migrations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
|
||||
import 'generated_migrations/schema.dart';
|
||||
|
||||
void main() {
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('upgrade from v1 to v2', () async {
|
||||
final connection = await verifier.startAt(1);
|
||||
final db = AppDatabase(connection: connection);
|
||||
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,6 @@ void main() {
|
||||
expect(profile.url, equals(validExtendedUrl));
|
||||
expect(profile.options, isNull);
|
||||
expect(profile.subInfo, isNull);
|
||||
expect(profile.extra, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -53,13 +52,6 @@ void main() {
|
||||
download: 1024,
|
||||
total: 10240,
|
||||
expire: DateTime(2024),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
profile.extra,
|
||||
equals(
|
||||
const ProfileExtra(
|
||||
webPageUrl: validBaseUrl,
|
||||
supportUrl: validSupportUrl,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user