Add local profile
This commit is contained in:
@@ -239,6 +239,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"unexpected": "Unexpected Error",
|
"unexpected": "Unexpected Error",
|
||||||
"notFound": "Profile Not Found",
|
"notFound": "Profile Not Found",
|
||||||
|
"invalidUrl": "Invalid URL",
|
||||||
"invalidConfig": "Invalid Configs"
|
"invalidConfig": "Invalid Configs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"sort": "مرتبسازی",
|
"sort": "مرتبسازی",
|
||||||
"sortBy": "مرتبسازی براساس"
|
"sortBy": "مرتبسازی براساس"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید",
|
"termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید",
|
||||||
"start": "شروع"
|
"start": "شروع"
|
||||||
@@ -239,6 +239,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"unexpected": "خطای غیرمنتظره",
|
"unexpected": "خطای غیرمنتظره",
|
||||||
"notFound": "پروفایل یافت نشد",
|
"notFound": "پروفایل یافت نشد",
|
||||||
|
"invalidUrl": "لینک نامعتبر",
|
||||||
"invalidConfig": "کانفیگ غیر معتبر"
|
"invalidConfig": "کانفیگ غیر معتبر"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,31 @@ import 'package:hiddify/domain/profiles/profiles.dart';
|
|||||||
|
|
||||||
extension ProfileMapper on Profile {
|
extension ProfileMapper on Profile {
|
||||||
ProfileEntriesCompanion toCompanion() {
|
ProfileEntriesCompanion toCompanion() {
|
||||||
return ProfileEntriesCompanion.insert(
|
return switch (this) {
|
||||||
id: id,
|
RemoteProfile(:final url, :final options, :final subInfo) =>
|
||||||
active: active,
|
ProfileEntriesCompanion.insert(
|
||||||
name: name,
|
id: id,
|
||||||
url: url,
|
type: ProfileType.remote,
|
||||||
lastUpdate: lastUpdate,
|
active: active,
|
||||||
updateInterval: Value(options?.updateInterval),
|
name: name,
|
||||||
upload: Value(subInfo?.upload),
|
url: Value(url),
|
||||||
download: Value(subInfo?.download),
|
lastUpdate: lastUpdate,
|
||||||
total: Value(subInfo?.total),
|
updateInterval: Value(options?.updateInterval),
|
||||||
expire: Value(subInfo?.expire),
|
upload: Value(subInfo?.upload),
|
||||||
webPageUrl: Value(extra?.webPageUrl),
|
download: Value(subInfo?.download),
|
||||||
supportUrl: Value(extra?.supportUrl),
|
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) {
|
static Profile fromEntry(ProfileEntry e) {
|
||||||
@@ -36,26 +47,27 @@ extension ProfileMapper on Profile {
|
|||||||
download: e.download!,
|
download: e.download!,
|
||||||
total: e.total!,
|
total: e.total!,
|
||||||
expire: e.expire!,
|
expire: e.expire!,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProfileExtra? extra;
|
|
||||||
if (e.webPageUrl != null || e.supportUrl != null) {
|
|
||||||
extra = ProfileExtra(
|
|
||||||
webPageUrl: e.webPageUrl,
|
webPageUrl: e.webPageUrl,
|
||||||
supportUrl: e.supportUrl,
|
supportUrl: e.supportUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Profile(
|
return switch (e.type) {
|
||||||
id: e.id,
|
ProfileType.remote => RemoteProfile(
|
||||||
active: e.active,
|
id: e.id,
|
||||||
name: e.name,
|
active: e.active,
|
||||||
url: e.url,
|
name: e.name,
|
||||||
lastUpdate: e.lastUpdate,
|
url: e.url!,
|
||||||
options: options,
|
lastUpdate: e.lastUpdate,
|
||||||
subInfo: subInfo,
|
options: options,
|
||||||
extra: extra,
|
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/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:hiddify/data/local/dao/dao.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/tables.dart';
|
||||||
import 'package:hiddify/data/local/type_converters.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:hiddify/services/files_editor_service.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
@@ -17,7 +19,31 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
AppDatabase.connect() : super(_openConnection());
|
AppDatabase.connect() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@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() {
|
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:drift/drift.dart';
|
||||||
import 'package:hiddify/data/local/type_converters.dart';
|
import 'package:hiddify/data/local/type_converters.dart';
|
||||||
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
|
|
||||||
@DataClassName('ProfileEntry')
|
@DataClassName('ProfileEntry')
|
||||||
class ProfileEntries extends Table {
|
class ProfileEntries extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
|
TextColumn get type => textEnum<ProfileType>()();
|
||||||
BoolColumn get active => boolean()();
|
BoolColumn get active => boolean()();
|
||||||
TextColumn get name => text().withLength(min: 1)();
|
TextColumn get name => text().withLength(min: 1)();
|
||||||
TextColumn get url => text()();
|
TextColumn get url => text().nullable()();
|
||||||
DateTimeColumn get lastUpdate => dateTime()();
|
DateTimeColumn get lastUpdate => dateTime()();
|
||||||
IntColumn get updateInterval =>
|
IntColumn get updateInterval =>
|
||||||
integer().nullable().map(DurationTypeConverter())();
|
integer().nullable().map(DurationTypeConverter())();
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class ProfilesRepositoryImpl
|
|||||||
return exceptionHandler(
|
return exceptionHandler(
|
||||||
() async {
|
() async {
|
||||||
final existingProfile = await profilesDao.getProfileByUrl(url);
|
final existingProfile = await profilesDao.getProfileByUrl(url);
|
||||||
if (existingProfile != null) {
|
if (existingProfile case RemoteProfile()) {
|
||||||
loggy.info("profile with url[$url] already exists, updating");
|
loggy.info("profile with url[$url] already exists, updating");
|
||||||
final baseProfile = markAsActive
|
final baseProfile = markAsActive
|
||||||
? existingProfile.copyWith(active: true)
|
? existingProfile.copyWith(active: true)
|
||||||
@@ -105,7 +105,49 @@ class ProfilesRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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(
|
return exceptionHandler(
|
||||||
() async {
|
() async {
|
||||||
return fetch(baseProfile.url, baseProfile.id)
|
return fetch(baseProfile.url, baseProfile.id)
|
||||||
@@ -114,7 +156,6 @@ class ProfilesRepositoryImpl
|
|||||||
await profilesDao.create(
|
await profilesDao.create(
|
||||||
baseProfile.copyWith(
|
baseProfile.copyWith(
|
||||||
subInfo: remoteProfile.subInfo,
|
subInfo: remoteProfile.subInfo,
|
||||||
extra: remoteProfile.extra,
|
|
||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -131,7 +172,7 @@ class ProfilesRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TaskEither<ProfileFailure, Unit> update(Profile baseProfile) {
|
TaskEither<ProfileFailure, Unit> update(RemoteProfile baseProfile) {
|
||||||
return exceptionHandler(
|
return exceptionHandler(
|
||||||
() async {
|
() async {
|
||||||
loggy.debug(
|
loggy.debug(
|
||||||
@@ -143,7 +184,6 @@ class ProfilesRepositoryImpl
|
|||||||
await profilesDao.edit(
|
await profilesDao.edit(
|
||||||
baseProfile.copyWith(
|
baseProfile.copyWith(
|
||||||
subInfo: remoteProfile.subInfo,
|
subInfo: remoteProfile.subInfo,
|
||||||
extra: remoteProfile.extra,
|
|
||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -209,13 +249,13 @@ class ProfilesRepositoryImpl
|
|||||||
];
|
];
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
TaskEither<ProfileFailure, Profile> fetch(
|
TaskEither<ProfileFailure, RemoteProfile> fetch(
|
||||||
String url,
|
String url,
|
||||||
String fileName,
|
String fileName,
|
||||||
) {
|
) {
|
||||||
return TaskEither(
|
return TaskEither(
|
||||||
() async {
|
() async {
|
||||||
final tempPath = filesEditor.configPath("temp_$fileName");
|
final tempPath = filesEditor.tempConfigPath(fileName);
|
||||||
final path = filesEditor.configPath(fileName);
|
final path = filesEditor.configPath(fileName);
|
||||||
try {
|
try {
|
||||||
final response = await dio.download(url.trim(), tempPath);
|
final response = await dio.download(url.trim(), tempPath);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:dartx/dartx.dart';
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:loggy/loggy.dart';
|
import 'package:loggy/loggy.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -10,11 +11,13 @@ part 'profile.g.dart';
|
|||||||
|
|
||||||
final _loggy = Loggy('Profile');
|
final _loggy = Loggy('Profile');
|
||||||
|
|
||||||
|
enum ProfileType { remote, local }
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class Profile with _$Profile {
|
sealed class Profile with _$Profile {
|
||||||
const Profile._();
|
const Profile._();
|
||||||
|
|
||||||
const factory Profile({
|
const factory Profile.remote({
|
||||||
required String id,
|
required String id,
|
||||||
required bool active,
|
required bool active,
|
||||||
required String name,
|
required String name,
|
||||||
@@ -22,10 +25,19 @@ class Profile with _$Profile {
|
|||||||
required DateTime lastUpdate,
|
required DateTime lastUpdate,
|
||||||
ProfileOptions? options,
|
ProfileOptions? options,
|
||||||
SubscriptionInfo? subInfo,
|
SubscriptionInfo? subInfo,
|
||||||
ProfileExtra? extra,
|
}) = RemoteProfile;
|
||||||
}) = _Profile;
|
|
||||||
|
|
||||||
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");
|
_loggy.debug("Profile Headers: $headers");
|
||||||
|
|
||||||
final titleHeader = headers['profile-title']?.single;
|
final titleHeader = headers['profile-title']?.single;
|
||||||
@@ -59,7 +71,7 @@ class Profile with _$Profile {
|
|||||||
if (title.isEmpty) {
|
if (title.isEmpty) {
|
||||||
final part = url.split("/").lastOrNull;
|
final part = url.split("/").lastOrNull;
|
||||||
if (part != null) {
|
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, "");
|
title = part.replaceFirst(pattern, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,15 +91,14 @@ class Profile with _$Profile {
|
|||||||
|
|
||||||
final webPageUrlHeader = headers['profile-web-page-url']?.single;
|
final webPageUrlHeader = headers['profile-web-page-url']?.single;
|
||||||
final supportUrlHeader = headers['support-url']?.single;
|
final supportUrlHeader = headers['support-url']?.single;
|
||||||
ProfileExtra? extra;
|
if (subInfo != null) {
|
||||||
if (webPageUrlHeader != null || supportUrlHeader != null) {
|
subInfo = subInfo.copyWith(
|
||||||
extra = ProfileExtra(
|
webPageUrl: isUrl(webPageUrlHeader ?? "") ? webPageUrlHeader : null,
|
||||||
webPageUrl: webPageUrlHeader,
|
supportUrl: isUrl(supportUrlHeader ?? "") ? supportUrlHeader : null,
|
||||||
supportUrl: supportUrlHeader,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Profile(
|
return RemoteProfile(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
active: false,
|
active: false,
|
||||||
name: title.isBlank ? "Remote Profile" : title,
|
name: title.isBlank ? "Remote Profile" : title,
|
||||||
@@ -95,7 +106,6 @@ class Profile with _$Profile {
|
|||||||
lastUpdate: DateTime.now(),
|
lastUpdate: DateTime.now(),
|
||||||
options: options,
|
options: options,
|
||||||
subInfo: subInfo,
|
subInfo: subInfo,
|
||||||
extra: extra,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,17 +123,6 @@ class ProfileOptions with _$ProfileOptions {
|
|||||||
_$ProfileOptionsFromJson(json);
|
_$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
|
@freezed
|
||||||
class SubscriptionInfo with _$SubscriptionInfo {
|
class SubscriptionInfo with _$SubscriptionInfo {
|
||||||
const SubscriptionInfo._();
|
const SubscriptionInfo._();
|
||||||
@@ -134,6 +133,8 @@ class SubscriptionInfo with _$SubscriptionInfo {
|
|||||||
@JsonKey(fromJson: _fromJsonTotal, defaultValue: 9223372036854775807)
|
@JsonKey(fromJson: _fromJsonTotal, defaultValue: 9223372036854775807)
|
||||||
required int total,
|
required int total,
|
||||||
@JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) required DateTime expire,
|
@JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) required DateTime expire,
|
||||||
|
String? webPageUrl,
|
||||||
|
String? supportUrl,
|
||||||
}) = _SubscriptionInfo;
|
}) = _SubscriptionInfo;
|
||||||
|
|
||||||
bool get isExpired => expire <= DateTime.now();
|
bool get isExpired => expire <= DateTime.now();
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
|
|||||||
|
|
||||||
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
|
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
|
||||||
|
|
||||||
|
@With<ExpectedException>()
|
||||||
|
const factory ProfileFailure.invalidUrl() = ProfileInvalidUrlFailure;
|
||||||
|
|
||||||
const factory ProfileFailure.invalidConfig([String? message]) =
|
const factory ProfileFailure.invalidConfig([String? message]) =
|
||||||
ProfileInvalidConfigFailure;
|
ProfileInvalidConfigFailure;
|
||||||
|
|
||||||
@@ -29,6 +32,10 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
|
|||||||
type: t.failure.profiles.notFound,
|
type: t.failure.profiles.notFound,
|
||||||
message: null
|
message: null
|
||||||
),
|
),
|
||||||
|
ProfileInvalidUrlFailure() => (
|
||||||
|
type: t.failure.profiles.invalidUrl,
|
||||||
|
message: null,
|
||||||
|
),
|
||||||
ProfileInvalidConfigFailure(:final message) => (
|
ProfileInvalidConfigFailure(:final message) => (
|
||||||
type: t.failure.profiles.invalidConfig,
|
type: t.failure.profiles.invalidConfig,
|
||||||
message: message
|
message: message
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ abstract class ProfilesRepository {
|
|||||||
bool markAsActive = false,
|
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);
|
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/data/data_providers.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
@@ -16,16 +15,4 @@ class ActiveProfile extends _$ActiveProfile with AppLogger {
|
|||||||
.watchActiveProfile()
|
.watchActiveProfile()
|
||||||
.map((event) => event.getOrElse((l) => throw l));
|
.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
|
final effectiveMargin = isMain
|
||||||
? const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
? const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
||||||
@@ -60,17 +63,19 @@ class ProfileTile extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
if (profile is RemoteProfile || !isMain) ...[
|
||||||
width: 48,
|
SizedBox(
|
||||||
child: Semantics(
|
width: 48,
|
||||||
sortKey: const OrdinalSortKey(1),
|
child: Semantics(
|
||||||
child: ProfileActionButton(profile, !isMain),
|
sortKey: const OrdinalSortKey(1),
|
||||||
|
child: ProfileActionButton(profile, !isMain),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
VerticalDivider(
|
||||||
VerticalDivider(
|
width: 1,
|
||||||
width: 1,
|
color: effectiveOutlineColor,
|
||||||
color: effectiveOutlineColor,
|
),
|
||||||
),
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
@@ -177,7 +182,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
|||||||
CustomToast.success(t.profile.update.successMsg).show(context),
|
CustomToast.success(t.profile.update.successMsg).show(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!showAllActions) {
|
if (profile case RemoteProfile() when !showAllActions) {
|
||||||
return Semantics(
|
return Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
enabled: !updateProfileMutation.state.isInProgress,
|
enabled: !updateProfileMutation.state.isInProgress,
|
||||||
@@ -191,7 +196,7 @@ class ProfileActionButton extends HookConsumerWidget {
|
|||||||
updateProfileMutation.setFuture(
|
updateProfileMutation.setFuture(
|
||||||
ref
|
ref
|
||||||
.read(profilesNotifierProvider.notifier)
|
.read(profilesNotifierProvider.notifier)
|
||||||
.updateProfile(profile),
|
.updateProfile(profile as RemoteProfile),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.update),
|
child: const Icon(Icons.update),
|
||||||
@@ -250,20 +255,21 @@ class ProfileActionsMenu extends HookConsumerWidget {
|
|||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
builder: builder,
|
builder: builder,
|
||||||
menuChildren: [
|
menuChildren: [
|
||||||
MenuItemButton(
|
if (profile case RemoteProfile())
|
||||||
leadingIcon: const Icon(Icons.update),
|
MenuItemButton(
|
||||||
child: Text(t.profile.update.buttonTxt),
|
leadingIcon: const Icon(Icons.update),
|
||||||
onPressed: () {
|
child: Text(t.profile.update.buttonTxt),
|
||||||
if (updateProfileMutation.state.isInProgress) {
|
onPressed: () {
|
||||||
return;
|
if (updateProfileMutation.state.isInProgress) {
|
||||||
}
|
return;
|
||||||
updateProfileMutation.setFuture(
|
}
|
||||||
ref
|
updateProfileMutation.setFuture(
|
||||||
.read(profilesNotifierProvider.notifier)
|
ref
|
||||||
.updateProfile(profile),
|
.read(profilesNotifierProvider.notifier)
|
||||||
);
|
.updateProfile(profile as RemoteProfile),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(Icons.edit),
|
leadingIcon: const Icon(Icons.edit),
|
||||||
child: Text(t.profile.edit.buttonTxt),
|
child: Text(t.profile.edit.buttonTxt),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
}) async {
|
}) async {
|
||||||
if (id == 'new') {
|
if (id == 'new') {
|
||||||
return ProfileDetailState(
|
return ProfileDetailState(
|
||||||
profile: Profile(
|
profile: RemoteProfile(
|
||||||
id: const Uuid().v4(),
|
id: const Uuid().v4(),
|
||||||
active: true,
|
active: true,
|
||||||
name: profileName ?? "",
|
name: profileName ?? "",
|
||||||
@@ -52,15 +52,20 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
if (state case AsyncData(:final value)) {
|
if (state case AsyncData(:final value)) {
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
profile: value.profile.copyWith(
|
profile: value.profile.map(
|
||||||
name: name ?? value.profile.name,
|
remote: (rp) => rp.copyWith(
|
||||||
url: url ?? value.profile.url,
|
name: name ?? rp.name,
|
||||||
options: updateInterval == null
|
url: url ?? rp.url,
|
||||||
? value.profile.options
|
options: updateInterval == null
|
||||||
: updateInterval.fold(
|
? rp.options
|
||||||
() => null,
|
: updateInterval.fold(
|
||||||
(t) => ProfileOptions(updateInterval: Duration(hours: t)),
|
() => 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 (state case AsyncData(:final value)) {
|
||||||
if (value.save.isInProgress) return;
|
if (value.save.isInProgress) return;
|
||||||
final profile = value.profile;
|
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;
|
Either<ProfileFailure, Unit>? failureOrSuccess;
|
||||||
if (profile.name.isBlank || profile.url.isBlank) {
|
state = AsyncData(value.copyWith(save: const MutationInProgress()));
|
||||||
loggy.debug('profile save: invalid arguments');
|
switch (profile) {
|
||||||
} else if (value.isEditing) {
|
case RemoteProfile():
|
||||||
if (_originalProfile?.url == profile.url) {
|
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');
|
loggy.debug('editing profile');
|
||||||
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
failureOrSuccess = await _profilesRepo.edit(profile).run();
|
||||||
} else {
|
default:
|
||||||
loggy.debug('updating profile');
|
loggy.warning("local profile can't be added manually");
|
||||||
failureOrSuccess = await _profilesRepo.update(profile).run();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loggy.debug('adding profile, url: [${profile.url}]');
|
|
||||||
failureOrSuccess = await _profilesRepo.add(profile).run();
|
|
||||||
}
|
}
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
value.copyWith(
|
value.copyWith(
|
||||||
@@ -105,12 +119,17 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger {
|
|||||||
|
|
||||||
Future<void> updateProfile() async {
|
Future<void> updateProfile() async {
|
||||||
if (state case AsyncData(:final value)) {
|
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;
|
if (value.update.isInProgress || !value.isEditing) return;
|
||||||
final profile = value.profile;
|
final profile = value.profile;
|
||||||
loggy.debug('updating profile');
|
loggy.debug('updating profile');
|
||||||
state = AsyncData(value.copyWith(update: const MutationInProgress()));
|
state = AsyncData(value.copyWith(update: const MutationInProgress()));
|
||||||
final failureOrUpdatedProfile = await _profilesRepo
|
final failureOrUpdatedProfile = await _profilesRepo
|
||||||
.update(profile)
|
.update(profile as RemoteProfile)
|
||||||
.flatMap((_) => _profilesRepo.get(id))
|
.flatMap((_) => _profilesRepo.get(id))
|
||||||
.run();
|
.run();
|
||||||
state = AsyncData(
|
state = AsyncData(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:fpdart/fpdart.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/domain/failures.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/common/confirmation_dialogs.dart';
|
||||||
import 'package:hiddify/features/profile_detail/notifier/notifier.dart';
|
import 'package:hiddify/features/profile_detail/notifier/notifier.dart';
|
||||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||||
@@ -93,12 +94,13 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
if (state.profile case RemoteProfile())
|
||||||
child: Text(t.profile.update.buttonTxt),
|
PopupMenuItem(
|
||||||
onTap: () async {
|
child: Text(t.profile.update.buttonTxt),
|
||||||
await notifier.updateProfile();
|
onTap: () async {
|
||||||
},
|
await notifier.updateProfile();
|
||||||
),
|
},
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Text(t.profile.delete.buttonTxt),
|
child: Text(t.profile.delete.buttonTxt),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -140,52 +142,55 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
|
|||||||
hint: t.profile.detailsForm.nameHint,
|
hint: t.profile.detailsForm.nameHint,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
if (state.profile
|
||||||
padding: const EdgeInsets.symmetric(
|
case RemoteProfile(:final url, :final options)) ...[
|
||||||
horizontal: 16,
|
Padding(
|
||||||
vertical: 8,
|
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(
|
ListTile(
|
||||||
initialValue: state.profile.url,
|
title: Text(t.profile.detailsForm.updateInterval),
|
||||||
onChanged: (value) => notifier.setField(url: value),
|
subtitle: Text(
|
||||||
validator: (value) =>
|
options?.updateInterval.toApproximateTime(
|
||||||
(value != null && !isUrl(value))
|
isRelativeToNow: false,
|
||||||
? t.profile.detailsForm.invalidUrlMsg
|
) ??
|
||||||
: null,
|
t.general.toggle.disabled,
|
||||||
label: t.profile.detailsForm.urlLabel,
|
),
|
||||||
hint: t.profile.detailsForm.urlHint,
|
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)
|
if (state.isEditing)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(t.profile.detailsForm.lastUpdate),
|
title: Text(t.profile.detailsForm.lastUpdate),
|
||||||
|
|||||||
@@ -49,21 +49,39 @@ class ProfilesNotifier extends _$ProfilesNotifier with AppLogger {
|
|||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Unit> addProfile(String url) async {
|
Future<Unit> addProfile(String rawInput) async {
|
||||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||||
final markAsActive =
|
final markAsActive =
|
||||||
activeProfile == null || ref.read(markNewProfileActiveProvider);
|
activeProfile == null || ref.read(markNewProfileActiveProvider);
|
||||||
loggy.debug("adding profile, url: [$url]");
|
if (LinkParser.parse(rawInput) case (final link)?) {
|
||||||
return ref
|
loggy.debug("adding profile, url: [${link.url}]");
|
||||||
.read(profilesRepositoryProvider)
|
return ref
|
||||||
.addByUrl(url, markAsActive: markAsActive)
|
.read(profilesRepositoryProvider)
|
||||||
.getOrElse((l) {
|
.addByUrl(link.url, markAsActive: markAsActive)
|
||||||
loggy.warning("failed to add profile: $l");
|
.getOrElse((l) {
|
||||||
throw l;
|
loggy.warning("failed to add profile: $l");
|
||||||
}).run();
|
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");
|
loggy.debug("updating profile");
|
||||||
return ref
|
return ref
|
||||||
.read(profilesRepositoryProvider)
|
.read(profilesRepositoryProvider)
|
||||||
|
|||||||
@@ -50,20 +50,22 @@ class ProfilesUpdateNotifier extends _$ProfilesUpdateNotifier with AppLogger {
|
|||||||
await ref.read(profilesRepositoryProvider).watchAll().first;
|
await ref.read(profilesRepositoryProvider).watchAll().first;
|
||||||
if (failureOrProfiles case Right(value: final profiles)) {
|
if (failureOrProfiles case Right(value: final profiles)) {
|
||||||
for (final profile in profiles) {
|
for (final profile in profiles) {
|
||||||
loggy.debug("checking profile: [${profile.name}]");
|
if (profile case RemoteProfile()) {
|
||||||
final updateInterval = profile.options?.updateInterval;
|
loggy.debug("checking profile: [${profile.name}]");
|
||||||
if (updateInterval != null &&
|
final updateInterval = profile.options?.updateInterval;
|
||||||
updateInterval <=
|
if (updateInterval != null &&
|
||||||
DateTime.now().difference(profile.lastUpdate)) {
|
updateInterval <=
|
||||||
final failureOrSuccess = await ref
|
DateTime.now().difference(profile.lastUpdate)) {
|
||||||
.read(profilesRepositoryProvider)
|
final failureOrSuccess = await ref
|
||||||
.update(profile)
|
.read(profilesRepositoryProvider)
|
||||||
.run();
|
.update(profile)
|
||||||
state = AsyncData(
|
.run();
|
||||||
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
state = AsyncData(
|
||||||
);
|
(name: profile.name, failureOrSuccess: failureOrSuccess),
|
||||||
} else {
|
);
|
||||||
loggy.debug("skipping profile: [${profile.name}]");
|
} 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/core_providers.dart';
|
||||||
import 'package:hiddify/core/router/router.dart';
|
import 'package:hiddify/core/router/router.dart';
|
||||||
import 'package:hiddify/domain/failures.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/common/qr_code_scanner_screen.dart';
|
||||||
import 'package:hiddify/features/profiles/notifier/notifier.dart';
|
import 'package:hiddify/features/profiles/notifier/notifier.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
@@ -29,8 +30,13 @@ class AddProfileModal extends HookConsumerWidget {
|
|||||||
final addProfileMutation = useMutation(
|
final addProfileMutation = useMutation(
|
||||||
initialOnFailure: (err) {
|
initialOnFailure: (err) {
|
||||||
mutationTriggered.value = false;
|
mutationTriggered.value = false;
|
||||||
// CustomToast.error(t.presentError(err)).show(context);
|
if (err case ProfileInvalidUrlFailure()) {
|
||||||
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
CustomToast.error(
|
||||||
|
t.profile.add.invalidUrlMsg,
|
||||||
|
).show(context);
|
||||||
|
} else {
|
||||||
|
CustomAlertDialog.fromErr(t.presentError(err)).show(context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
initialOnSuccess: () {
|
initialOnSuccess: () {
|
||||||
CustomToast.success(t.profile.save.successMsg).show(context);
|
CustomToast.success(t.profile.save.successMsg).show(context);
|
||||||
@@ -102,24 +108,15 @@ class AddProfileModal extends HookConsumerWidget {
|
|||||||
size: buttonWidth,
|
size: buttonWidth,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final captureResult =
|
final captureResult =
|
||||||
await Clipboard.getData(Clipboard.kTextPlain);
|
await Clipboard.getData(Clipboard.kTextPlain)
|
||||||
final link =
|
.then((value) => value?.text ?? '');
|
||||||
LinkParser.parse(captureResult?.text ?? '');
|
if (addProfileMutation.state.isInProgress) return;
|
||||||
if (link != null && context.mounted) {
|
mutationTriggered.value = true;
|
||||||
if (addProfileMutation.state.isInProgress) return;
|
addProfileMutation.setFuture(
|
||||||
mutationTriggered.value = true;
|
ref
|
||||||
addProfileMutation.setFuture(
|
.read(profilesNotifierProvider.notifier)
|
||||||
ref
|
.addProfile(captureResult),
|
||||||
.read(profilesNotifierProvider.notifier)
|
);
|
||||||
.addProfile(link.url),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (context.mounted) {
|
|
||||||
CustomToast.error(
|
|
||||||
t.profile.add.invalidUrlMsg,
|
|
||||||
).show(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(buttonsGap),
|
const Gap(buttonsGap),
|
||||||
@@ -134,24 +131,15 @@ class AddProfileModal extends HookConsumerWidget {
|
|||||||
await const QRCodeScannerScreen()
|
await const QRCodeScannerScreen()
|
||||||
.open(context);
|
.open(context);
|
||||||
if (captureResult == null) return;
|
if (captureResult == null) return;
|
||||||
final link = LinkParser.simple(captureResult);
|
if (addProfileMutation.state.isInProgress) {
|
||||||
if (link != null && context.mounted) {
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
mutationTriggered.value = true;
|
||||||
|
addProfileMutation.setFuture(
|
||||||
|
ref
|
||||||
|
.read(profilesNotifierProvider.notifier)
|
||||||
|
.addProfile(captureResult),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ class FilesEditorService with InfraLogger {
|
|||||||
return p.join(_configsDir.path, "$fileName.json");
|
return p.join(_configsDir.path, "$fileName.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String tempConfigPath(String fileName) => configPath("temp_$fileName");
|
||||||
|
|
||||||
Future<void> deleteConfig(String fileName) {
|
Future<void> deleteConfig(String fileName) {
|
||||||
return File(configPath(fileName)).delete();
|
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';
|
import 'package:hiddify/utils/validators.dart';
|
||||||
|
|
||||||
typedef ProfileLink = ({String url, String name});
|
typedef ProfileLink = ({String url, String name});
|
||||||
|
|
||||||
// TODO: test and improve
|
// TODO: test and improve
|
||||||
abstract class LinkParser {
|
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) {
|
static ProfileLink? parse(String link) {
|
||||||
return simple(link) ?? deep(link);
|
return simple(link) ?? deep(link);
|
||||||
@@ -13,24 +18,39 @@ abstract class LinkParser {
|
|||||||
static ProfileLink? simple(String link) {
|
static ProfileLink? simple(String link) {
|
||||||
if (!isUrl(link)) return null;
|
if (!isUrl(link)) return null;
|
||||||
final uri = Uri.parse(link.trim());
|
final uri = Uri.parse(link.trim());
|
||||||
final params = uri.queryParameters;
|
|
||||||
return (
|
return (
|
||||||
url: uri.toString(),
|
url: uri.toString(),
|
||||||
// .replace(queryParameters: {})
|
name: uri.queryParameters['name'] ?? '',
|
||||||
// .toString()
|
|
||||||
// .removeSuffix('?')
|
|
||||||
// .split('&')
|
|
||||||
// .first,
|
|
||||||
name: params['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) {
|
static ProfileLink? deep(String link) {
|
||||||
final uri = Uri.tryParse(link.trim());
|
final uri = Uri.tryParse(link.trim());
|
||||||
if (uri == null || !uri.hasScheme || !uri.hasAuthority) return null;
|
if (uri == null || !uri.hasScheme || !uri.hasAuthority) return null;
|
||||||
final queryParams = uri.queryParameters;
|
final queryParams = uri.queryParameters;
|
||||||
switch (uri.scheme) {
|
switch (uri.scheme) {
|
||||||
case 'clash' || 'clashmeta':
|
case 'clash' || 'clashmeta' when uri.authority == 'install-config':
|
||||||
if (uri.authority != 'install-config' ||
|
if (uri.authority != 'install-config' ||
|
||||||
!queryParams.containsKey('url')) return null;
|
!queryParams.containsKey('url')) return null;
|
||||||
return (url: queryParams['url']!, name: queryParams['name'] ?? '');
|
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.url, equals(validExtendedUrl));
|
||||||
expect(profile.options, isNull);
|
expect(profile.options, isNull);
|
||||||
expect(profile.subInfo, isNull);
|
expect(profile.subInfo, isNull);
|
||||||
expect(profile.extra, isNull);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,13 +52,6 @@ void main() {
|
|||||||
download: 1024,
|
download: 1024,
|
||||||
total: 10240,
|
total: 10240,
|
||||||
expire: DateTime(2024),
|
expire: DateTime(2024),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
profile.extra,
|
|
||||||
equals(
|
|
||||||
const ProfileExtra(
|
|
||||||
webPageUrl: validBaseUrl,
|
webPageUrl: validBaseUrl,
|
||||||
supportUrl: validSupportUrl,
|
supportUrl: validSupportUrl,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user