Refactor
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
import 'package:accessibility_tools/accessibility_tools.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/features/common/app_update_notifier.dart';
|
||||
import 'package:hiddify/features/common/common_controllers.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:upgrader/upgrader.dart';
|
||||
|
||||
bool _debugAccessibility = false;
|
||||
|
||||
class AppView extends HookConsumerWidget with PresLogger {
|
||||
const AppView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(routerProvider);
|
||||
final locale = ref.watch(localeNotifierProvider).flutterLocale;
|
||||
final theme = ref.watch(themeProvider);
|
||||
|
||||
ref.watch(commonControllersProvider);
|
||||
|
||||
final upgrader = ref.watch(upgraderProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: router,
|
||||
locale: locale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: theme.mode.flutterThemeMode,
|
||||
theme: theme.light(),
|
||||
darkTheme: theme.dark(),
|
||||
title: Constants.appName,
|
||||
builder: (context, child) {
|
||||
child = UpgradeAlert(
|
||||
upgrader: upgrader,
|
||||
navigatorKey: router.routerDelegate.navigatorKey,
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
if (kDebugMode && _debugAccessibility) {
|
||||
return AccessibilityTools(
|
||||
checkFontOverflows: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/core/app_info/app_info_provider.dart
Normal file
30
lib/core/app_info/app_info_provider.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hiddify/core/model/app_info_entity.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_info_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Environment environment(EnvironmentRef ref) =>
|
||||
throw Exception("override environmentProvider");
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppInfo extends _$AppInfo {
|
||||
@override
|
||||
Future<AppInfoEntity> build() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final environment = ref.watch(environmentProvider);
|
||||
return AppInfoEntity(
|
||||
name: packageInfo.appName,
|
||||
version: packageInfo.version,
|
||||
buildNumber: packageInfo.buildNumber,
|
||||
release: Release.read(),
|
||||
operatingSystem: Platform.operatingSystem,
|
||||
operatingSystemVersion: Platform.operatingSystemVersion,
|
||||
environment: environment,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/domain/app/app.dart';
|
||||
import 'package:hiddify/domain/environment.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'core_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppInfo appInfo(AppInfoRef ref) =>
|
||||
throw UnimplementedError('AppInfo must be overridden');
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Environment env(EnvRef ref) => ref.watch(appInfoProvider).environment;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TranslationsEn translations(TranslationsRef ref) =>
|
||||
ref.watch(localeNotifierProvider).build();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppTheme theme(ThemeRef ref) => AppTheme(
|
||||
ref.watch(themeModeNotifierProvider),
|
||||
ref.watch(localeNotifierProvider).preferredFontFamily,
|
||||
);
|
||||
59
lib/core/database/app_database.dart
Normal file
59
lib/core/database/app_database.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/core/database/connection/database_connection.dart';
|
||||
import 'package:hiddify/core/database/converters/duration_converter.dart';
|
||||
import 'package:hiddify/core/database/schema_versions.dart';
|
||||
import 'package:hiddify/core/database/tables/database_tables.dart';
|
||||
import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart';
|
||||
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
|
||||
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||
import 'package:hiddify/features/profile/model/profile_entity.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase({required QueryExecutor connection}) : super(connection);
|
||||
|
||||
AppDatabase.connect() : super(openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
await _prePopulateGeoAssets();
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
// add type column to profile entries table
|
||||
// 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],
|
||||
),
|
||||
);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.createTable(schema.geoAssetEntries);
|
||||
await _prePopulateGeoAssets();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _prePopulateGeoAssets() async {
|
||||
await transaction(() async {
|
||||
final geoAssets = defaultGeoAssets.map((e) => e.toEntry());
|
||||
for (final geoAsset in geoAssets) {
|
||||
await into(geoAssetEntries).insert(geoAsset);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
14
lib/core/database/connection/database_connection.dart
Normal file
14
lib/core/database/connection/database_connection.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
LazyDatabase openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbDir = await FilesEditorService.getDatabaseDirectory();
|
||||
final file = File(p.join(dbDir.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
13
lib/core/database/converters/duration_converter.dart
Normal file
13
lib/core/database/converters/duration_converter.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class DurationTypeConverter extends TypeConverter<Duration, int> {
|
||||
@override
|
||||
Duration fromSql(int fromDb) {
|
||||
return Duration(seconds: fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
int toSql(Duration value) {
|
||||
return value.inSeconds;
|
||||
}
|
||||
}
|
||||
7
lib/core/database/database_provider.dart
Normal file
7
lib/core/database/database_provider.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:hiddify/core/database/app_database.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'database_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect();
|
||||
231
lib/core/database/schema_versions.dart
Normal file
231
lib/core/database/schema_versions.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
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);
|
||||
|
||||
final class _S3 extends i0.VersionedSchema {
|
||||
_S3({required super.database}) : super(version: 3);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
profileEntries,
|
||||
geoAssetEntries,
|
||||
];
|
||||
late final Shape0 profileEntries = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'profile_entries',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape1 geoAssetEntries = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'geo_asset_entries',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
'UNIQUE(name, provider_name)',
|
||||
],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
}
|
||||
|
||||
class Shape1 extends i0.VersionedTable {
|
||||
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get active =>
|
||||
columnsByName['active']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get providerName =>
|
||||
columnsByName['provider_name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get version =>
|
||||
columnsByName['version']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get lastCheck =>
|
||||
columnsByName['last_check']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('provider_name', aliasedName, false,
|
||||
additionalChecks: i1.GeneratedColumn.checkTextLength(
|
||||
minTextLength: 1,
|
||||
),
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('version', aliasedName, true,
|
||||
type: i1.DriftSqlType.string);
|
||||
i1.GeneratedColumn<DateTime> _column_15(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>('last_check', aliasedName, true,
|
||||
type: i1.DriftSqlType.dateTime);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
case 1:
|
||||
final schema = _S2(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from1To2(migrator, schema);
|
||||
return 2;
|
||||
case 2:
|
||||
final schema = _S3(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, _S3 schema) from2To3,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
));
|
||||
160
lib/core/database/schemas/drift_schema_v1.json
Normal file
160
lib/core/database/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/core/database/schemas/drift_schema_v2.json
Normal file
174
lib/core/database/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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
286
lib/core/database/schemas/drift_schema_v3.json
Normal file
286
lib/core/database/schemas/drift_schema_v3.json
Normal file
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"_meta": {
|
||||
"description": "This file contains a serialized version of schema entities for drift.",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"options": {
|
||||
"store_date_time_values_as_text": true
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"id": 0,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "profile_entries",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"getter_name": "type",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumNameConverter<ProfileType>(ProfileType.values)",
|
||||
"dart_type_name": "ProfileType"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"getter_name": "active",
|
||||
"moor_type": "bool",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"getter_name": "name",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
{
|
||||
"allowed-lengths": {
|
||||
"min": 1,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"getter_name": "url",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "last_update",
|
||||
"getter_name": "lastUpdate",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "update_interval",
|
||||
"getter_name": "updateInterval",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "DurationTypeConverter()",
|
||||
"dart_type_name": "Duration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload",
|
||||
"getter_name": "upload",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "download",
|
||||
"getter_name": "download",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "total",
|
||||
"getter_name": "total",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "expire",
|
||||
"getter_name": "expire",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "web_page_url",
|
||||
"getter_name": "webPageUrl",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "support_url",
|
||||
"getter_name": "supportUrl",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": [],
|
||||
"explicit_pk": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "geo_asset_entries",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"getter_name": "type",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumNameConverter<GeoAssetType>(GeoAssetType.values)",
|
||||
"dart_type_name": "GeoAssetType"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"getter_name": "active",
|
||||
"moor_type": "bool",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"active\" IN (0, 1))",
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"getter_name": "name",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
{
|
||||
"allowed-lengths": {
|
||||
"min": 1,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "provider_name",
|
||||
"getter_name": "providerName",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [
|
||||
{
|
||||
"allowed-lengths": {
|
||||
"min": 1,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"getter_name": "version",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "last_check",
|
||||
"getter_name": "lastCheck",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": false,
|
||||
"constraints": [],
|
||||
"explicit_pk": [
|
||||
"id"
|
||||
],
|
||||
"unique_keys": [
|
||||
[
|
||||
"name",
|
||||
"provider_name"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
44
lib/core/database/tables/database_tables.dart
Normal file
44
lib/core/database/tables/database_tables.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/core/database/converters/duration_converter.dart';
|
||||
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||
import 'package:hiddify/features/profile/model/profile_entity.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().nullable()();
|
||||
DateTimeColumn get lastUpdate => dateTime()();
|
||||
IntColumn get updateInterval =>
|
||||
integer().nullable().map(DurationTypeConverter())();
|
||||
IntColumn get upload => integer().nullable()();
|
||||
IntColumn get download => integer().nullable()();
|
||||
IntColumn get total => integer().nullable()();
|
||||
DateTimeColumn get expire => dateTime().nullable()();
|
||||
TextColumn get webPageUrl => text().nullable()();
|
||||
TextColumn get supportUrl => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
@DataClassName('GeoAssetEntry')
|
||||
class GeoAssetEntries extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get type => textEnum<GeoAssetType>()();
|
||||
BoolColumn get active => boolean()();
|
||||
TextColumn get name => text().withLength(min: 1)();
|
||||
TextColumn get providerName => text().withLength(min: 1)();
|
||||
TextColumn get version => text().nullable()();
|
||||
DateTimeColumn get lastCheck => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{name, providerName},
|
||||
];
|
||||
}
|
||||
28
lib/core/http_client/http_client_provider.dart
Normal file
28
lib/core/http_client/http_client_provider.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'http_client_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Dio httpClient(HttpClientRef ref) {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
sendTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: {
|
||||
"User-Agent": ref.watch(appInfoProvider).requireValue.userAgent,
|
||||
},
|
||||
),
|
||||
);
|
||||
final debug = ref.read(debugModeNotifierProvider);
|
||||
if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
||||
dio.httpClientAdapter = NativeAdapter();
|
||||
}
|
||||
return dio;
|
||||
}
|
||||
13
lib/core/localization/locale_extensions.dart
Normal file
13
lib/core/localization/locale_extensions.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:hiddify/gen/fonts.gen.dart';
|
||||
import 'package:hiddify/gen/translations.g.dart';
|
||||
|
||||
extension AppLocaleX on AppLocale {
|
||||
String get preferredFontFamily =>
|
||||
this == AppLocale.fa ? FontFamily.shabnam : "";
|
||||
|
||||
String get localeName =>
|
||||
LocaleNamesLocalizationsDelegate
|
||||
.nativeLocaleNames[flutterLocale.toString()] ??
|
||||
name;
|
||||
}
|
||||
28
lib/core/localization/locale_preferences.dart
Normal file
28
lib/core/localization/locale_preferences.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/gen/translations.g.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'locale_preferences.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LocalePreferences extends _$LocalePreferences {
|
||||
@override
|
||||
AppLocale build() {
|
||||
final persisted =
|
||||
ref.watch(sharedPreferencesProvider).requireValue.getString("locale");
|
||||
if (persisted == null) return AppLocaleUtils.findDeviceLocale();
|
||||
// keep backward compatibility with chinese after changing zh to zh_CN
|
||||
if (persisted == "zh") {
|
||||
return AppLocale.zhCn;
|
||||
}
|
||||
return AppLocale.values.byName(persisted);
|
||||
}
|
||||
|
||||
Future<void> changeLocale(AppLocale value) async {
|
||||
state = value;
|
||||
await ref
|
||||
.read(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.setString("locale", value.name);
|
||||
}
|
||||
}
|
||||
11
lib/core/localization/translations.dart
Normal file
11
lib/core/localization/translations.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:hiddify/core/localization/locale_preferences.dart';
|
||||
import 'package:hiddify/gen/translations.g.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
export 'package:hiddify/gen/translations.g.dart';
|
||||
|
||||
part 'translations.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TranslationsEn translations(TranslationsRef ref) =>
|
||||
ref.watch(localePreferencesProvider).build();
|
||||
32
lib/core/model/app_info_entity.dart
Normal file
32
lib/core/model/app_info_entity.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
|
||||
part 'app_info_entity.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class AppInfoEntity with _$AppInfoEntity {
|
||||
const AppInfoEntity._();
|
||||
|
||||
const factory AppInfoEntity({
|
||||
required String name,
|
||||
required String version,
|
||||
required String buildNumber,
|
||||
required Release release,
|
||||
required String operatingSystem,
|
||||
required String operatingSystemVersion,
|
||||
required Environment environment,
|
||||
}) = _AppInfoEntity;
|
||||
|
||||
String get userAgent =>
|
||||
"HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box";
|
||||
|
||||
String get presentVersion => environment == Environment.prod
|
||||
? version
|
||||
: "$version ${environment.name}";
|
||||
|
||||
/// formats app info for sharing
|
||||
String format() => '''
|
||||
$name v$version ($buildNumber) [${environment.name}]
|
||||
${release.name} release
|
||||
$operatingSystem [$operatingSystemVersion]''';
|
||||
}
|
||||
13
lib/core/model/constants.dart
Normal file
13
lib/core/model/constants.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
abstract class Constants {
|
||||
static const appName = "Hiddify Next";
|
||||
static const githubUrl = "https://github.com/hiddify/hiddify-next";
|
||||
static const githubReleasesApiUrl =
|
||||
"https://api.github.com/repos/hiddify/hiddify-next/releases";
|
||||
static const githubLatestReleaseUrl =
|
||||
"https://github.com/hiddify/hiddify-next/releases/latest";
|
||||
static const appCastUrl =
|
||||
"https://raw.githubusercontent.com/hiddify/hiddify-next/main/appcast.xml";
|
||||
static const telegramChannelUrl = "https://t.me/hiddify";
|
||||
static const privacyPolicyUrl = "https://hiddify.com/en/privacy-policy/";
|
||||
static const termsAndConditionsUrl = "https://hiddify.com/terms/";
|
||||
}
|
||||
7
lib/core/model/directories.dart
Normal file
7
lib/core/model/directories.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
typedef Directories = ({
|
||||
Directory baseDir,
|
||||
Directory workingDir,
|
||||
Directory tempDir
|
||||
});
|
||||
25
lib/core/model/environment.dart
Normal file
25
lib/core/model/environment.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
|
||||
enum Environment {
|
||||
prod,
|
||||
dev;
|
||||
|
||||
static const sentryDSN = String.fromEnvironment("sentry_dsn");
|
||||
}
|
||||
|
||||
enum Release {
|
||||
general("general"),
|
||||
googlePlay("google-play");
|
||||
|
||||
const Release(this.key);
|
||||
|
||||
final String key;
|
||||
|
||||
bool get allowCustomUpdateChecker => this == general;
|
||||
|
||||
static Release read() =>
|
||||
Release.values.firstOrNullWhere(
|
||||
(e) => e.key == const String.fromEnvironment("release"),
|
||||
) ??
|
||||
Release.general;
|
||||
}
|
||||
73
lib/core/model/failures.dart
Normal file
73
lib/core/model/failures.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
|
||||
typedef PresentableError = ({String type, String? message});
|
||||
|
||||
mixin Failure {
|
||||
({String type, String? message}) present(TranslationsEn t);
|
||||
}
|
||||
|
||||
/// failures that are not expected to happen but depending on [error] type might not be relevant (eg network errors)
|
||||
mixin UnexpectedFailure {
|
||||
Object? get error;
|
||||
StackTrace? get stackTrace;
|
||||
}
|
||||
|
||||
/// failures that are expected to happen and should be handled by the app
|
||||
/// and should be logged, eg missing permissions
|
||||
mixin ExpectedMeasuredFailure {}
|
||||
|
||||
/// failures ignored by analytics service etc.
|
||||
mixin ExpectedFailure {}
|
||||
|
||||
extension ErrorPresenter on TranslationsEn {
|
||||
PresentableError errorToPair(Object error) => switch (error) {
|
||||
UnexpectedFailure(error: final nestedErr?) => errorToPair(nestedErr),
|
||||
Failure() => error.present(this),
|
||||
DioException() => error.present(this),
|
||||
_ => (type: failure.unexpected, message: null),
|
||||
};
|
||||
|
||||
PresentableError presentError(
|
||||
Object error, {
|
||||
String? action,
|
||||
}) {
|
||||
final pair = errorToPair(error);
|
||||
if (action == null) return pair;
|
||||
return (
|
||||
type: action,
|
||||
message: pair.type + (pair.message == null ? "" : "\n${pair.message!}"),
|
||||
);
|
||||
}
|
||||
|
||||
String presentShortError(
|
||||
Object error, {
|
||||
String? action,
|
||||
}) {
|
||||
final pair = errorToPair(error);
|
||||
if (action == null) return pair.type;
|
||||
return "$action: ${pair.type}";
|
||||
}
|
||||
}
|
||||
|
||||
extension DioExceptionPresenter on DioException {
|
||||
PresentableError present(TranslationsEn t) => switch (type) {
|
||||
DioExceptionType.connectionTimeout ||
|
||||
DioExceptionType.sendTimeout ||
|
||||
DioExceptionType.receiveTimeout =>
|
||||
(type: t.failure.connection.timeout, message: null),
|
||||
DioExceptionType.badCertificate => (
|
||||
type: t.failure.connection.badCertificate,
|
||||
message: message,
|
||||
),
|
||||
DioExceptionType.badResponse => (
|
||||
type: t.failure.connection.badResponse,
|
||||
message: message,
|
||||
),
|
||||
DioExceptionType.connectionError => (
|
||||
type: t.failure.connection.connectionError,
|
||||
message: message,
|
||||
),
|
||||
_ => (type: t.failure.connection.unexpected, message: message),
|
||||
};
|
||||
}
|
||||
15
lib/core/model/region.dart
Normal file
15
lib/core/model/region.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
|
||||
enum Region {
|
||||
ir,
|
||||
cn,
|
||||
ru,
|
||||
other;
|
||||
|
||||
String present(TranslationsEn t) => switch (this) {
|
||||
ir => t.settings.general.regions.ir,
|
||||
cn => t.settings.general.regions.cn,
|
||||
ru => t.settings.general.regions.ru,
|
||||
other => t.settings.general.regions.other,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/environment.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
import 'package:hiddify/core/model/region.dart';
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||
import 'package:hiddify/utils/pref_notifier.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'general_prefs.g.dart';
|
||||
part 'general_preferences.g.dart';
|
||||
|
||||
// TODO refactor
|
||||
|
||||
bool _debugIntroPage = false;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class IntroCompleted extends _$IntroCompleted {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"intro_completed",
|
||||
false,
|
||||
);
|
||||
@@ -33,7 +36,7 @@ class IntroCompleted extends _$IntroCompleted {
|
||||
@Riverpod(keepAlive: true)
|
||||
class RegionNotifier extends _$RegionNotifier {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"region",
|
||||
Region.other,
|
||||
mapFrom: Region.values.byName,
|
||||
@@ -51,8 +54,11 @@ class RegionNotifier extends _$RegionNotifier {
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SilentStartNotifier extends _$SilentStartNotifier {
|
||||
late final _pref =
|
||||
Pref(ref.watch(sharedPreferencesProvider), "silent_start", false);
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"silent_start",
|
||||
false,
|
||||
);
|
||||
|
||||
@override
|
||||
bool build() => _pref.getValue();
|
||||
@@ -66,7 +72,7 @@ class SilentStartNotifier extends _$SilentStartNotifier {
|
||||
@Riverpod(keepAlive: true)
|
||||
class EnableAnalytics extends _$EnableAnalytics {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"enable_analytics",
|
||||
true,
|
||||
);
|
||||
@@ -83,7 +89,7 @@ class EnableAnalytics extends _$EnableAnalytics {
|
||||
@Riverpod(keepAlive: true)
|
||||
class DisableMemoryLimit extends _$DisableMemoryLimit {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"disable_memory_limit",
|
||||
false,
|
||||
);
|
||||
@@ -100,9 +106,9 @@ class DisableMemoryLimit extends _$DisableMemoryLimit {
|
||||
@Riverpod(keepAlive: true)
|
||||
class DebugModeNotifier extends _$DebugModeNotifier {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"debug_mode",
|
||||
ref.read(envProvider) == Environment.dev,
|
||||
ref.read(environmentProvider) == Environment.dev,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -117,7 +123,7 @@ class DebugModeNotifier extends _$DebugModeNotifier {
|
||||
@Riverpod(keepAlive: true)
|
||||
class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"per_app_proxy_mode",
|
||||
PerAppProxyMode.off,
|
||||
mapFrom: PerAppProxyMode.values.byName,
|
||||
@@ -136,13 +142,13 @@ class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier {
|
||||
@Riverpod(keepAlive: true)
|
||||
class PerAppProxyList extends _$PerAppProxyList {
|
||||
late final _include = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"per_app_proxy_include_list",
|
||||
<String>[],
|
||||
);
|
||||
|
||||
late final _exclude = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"per_app_proxy_exclude_list",
|
||||
<String>[],
|
||||
);
|
||||
@@ -165,7 +171,7 @@ class PerAppProxyList extends _$PerAppProxyList {
|
||||
@riverpod
|
||||
class MarkNewProfileActive extends _$MarkNewProfileActive {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"mark_new_profile_active",
|
||||
true,
|
||||
);
|
||||
8
lib/core/preferences/preferences_provider.dart
Normal file
8
lib/core/preferences/preferences_provider.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'preferences_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<SharedPreferences> sharedPreferences(SharedPreferencesRef ref) async =>
|
||||
SharedPreferences.getInstance();
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/utils/pref_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'service_prefs.g.dart';
|
||||
part 'service_preferences.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class StartedByUser extends _$StartedByUser with AppLogger {
|
||||
late final _pref =
|
||||
Pref(ref.watch(sharedPreferencesProvider), "started_by_user", false);
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"started_by_user",
|
||||
false,
|
||||
);
|
||||
|
||||
@override
|
||||
bool build() => _pref.getValue();
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/gen/fonts.gen.dart';
|
||||
import 'package:hiddify/gen/translations.g.dart';
|
||||
import 'package:hiddify/utils/pref_notifier.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
export 'package:hiddify/gen/translations.g.dart';
|
||||
|
||||
part 'locale_prefs.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LocaleNotifier extends _$LocaleNotifier {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
"locale",
|
||||
AppLocaleUtils.findDeviceLocale(),
|
||||
mapFrom: (String value) {
|
||||
// keep backward compatibility with chinese after changing zh to zh_CN
|
||||
if (value == "zh") {
|
||||
return AppLocale.zhCn;
|
||||
}
|
||||
return AppLocale.values.byName(value);
|
||||
},
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
@override
|
||||
AppLocale build() => _pref.getValue();
|
||||
|
||||
Future<void> update(AppLocale value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
extension AppLocaleX on AppLocale {
|
||||
String get preferredFontFamily =>
|
||||
this == AppLocale.fa ? FontFamily.shabnam : "";
|
||||
|
||||
String get localeName =>
|
||||
LocaleNamesLocalizationsDelegate
|
||||
.nativeLocaleNames[flutterLocale.toString()] ??
|
||||
name;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export 'app_theme.dart';
|
||||
export 'general_prefs.dart';
|
||||
export 'locale_prefs.dart';
|
||||
export 'theme_prefs.dart';
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:hiddify/core/prefs/app_theme.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/utils/pref_notifier.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'theme_prefs.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ThemeModeNotifier extends _$ThemeModeNotifier {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider),
|
||||
"theme_mode",
|
||||
AppThemeMode.system,
|
||||
mapFrom: AppThemeMode.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
@override
|
||||
AppThemeMode build() => _pref.getValue();
|
||||
|
||||
Future<void> update(AppThemeMode value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/core/preferences/general_preferences.dart';
|
||||
import 'package:hiddify/core/router/routes.dart';
|
||||
import 'package:hiddify/services/deep_link_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -92,6 +92,7 @@ class RouterListenable extends _$RouterListenable
|
||||
});
|
||||
}
|
||||
|
||||
// ignore: avoid_build_context_in_providers
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
// if (this.state.isLoading || this.state.hasError) return null;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/router/app_router.dart';
|
||||
import 'package:hiddify/features/about/view/about_page.dart';
|
||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||
import 'package:hiddify/features/config_option/overview/config_options_page.dart';
|
||||
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
|
||||
import 'package:hiddify/features/home/view/view.dart';
|
||||
import 'package:hiddify/features/intro/intro_page.dart';
|
||||
import 'package:hiddify/features/home/widget/home_page.dart';
|
||||
import 'package:hiddify/features/intro/widget/intro_page.dart';
|
||||
import 'package:hiddify/features/log/overview/logs_overview_page.dart';
|
||||
import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_page.dart';
|
||||
import 'package:hiddify/features/profile/add/add_profile_modal.dart';
|
||||
import 'package:hiddify/features/profile/details/profile_details_page.dart';
|
||||
import 'package:hiddify/features/profile/overview/profiles_overview_page.dart';
|
||||
import 'package:hiddify/features/proxies/view/view.dart';
|
||||
import 'package:hiddify/features/settings/view/config_options_page.dart';
|
||||
import 'package:hiddify/features/settings/view/per_app_proxy_page.dart';
|
||||
import 'package:hiddify/features/settings/view/settings_page.dart';
|
||||
import 'package:hiddify/features/proxy/overview/proxies_overview_page.dart';
|
||||
import 'package:hiddify/features/settings/about/about_page.dart';
|
||||
import 'package:hiddify/features/settings/overview/settings_overview_page.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
part 'routes.g.dart';
|
||||
@@ -184,7 +184,7 @@ class ProxiesRoute extends GoRouteData {
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return const NoTransitionPage(
|
||||
name: name,
|
||||
child: ProxiesPage(),
|
||||
child: ProxiesOverviewPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -291,10 +291,10 @@ class SettingsRoute extends GoRouteData {
|
||||
return const MaterialPage(
|
||||
fullscreenDialog: true,
|
||||
name: name,
|
||||
child: SettingsPage(),
|
||||
child: SettingsOverviewPage(),
|
||||
);
|
||||
}
|
||||
return const NoTransitionPage(name: name, child: SettingsPage());
|
||||
return const NoTransitionPage(name: name, child: SettingsOverviewPage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
|
||||
import 'package:flex_color_scheme/flex_color_scheme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/prefs/locale_prefs.dart';
|
||||
import 'package:hiddify/core/theme/app_theme_mode.dart';
|
||||
import 'package:hiddify/core/theme/theme_extensions.dart';
|
||||
|
||||
enum AppThemeMode {
|
||||
system,
|
||||
light,
|
||||
dark,
|
||||
black;
|
||||
|
||||
String present(TranslationsEn t) => switch (this) {
|
||||
system => t.settings.general.themeModes.system,
|
||||
light => t.settings.general.themeModes.light,
|
||||
dark => t.settings.general.themeModes.dark,
|
||||
black => t.settings.general.themeModes.black,
|
||||
};
|
||||
|
||||
ThemeMode get flutterThemeMode => switch (this) {
|
||||
system => ThemeMode.system,
|
||||
light => ThemeMode.light,
|
||||
dark => ThemeMode.dark,
|
||||
black => ThemeMode.dark,
|
||||
};
|
||||
|
||||
bool get trueBlack => this == black;
|
||||
}
|
||||
|
||||
// mostly exact copy of flex color scheme 7.1's fabulous 12 theme
|
||||
class AppTheme {
|
||||
AppTheme(
|
||||
this.mode,
|
||||
@@ -160,42 +138,3 @@ class AppTheme {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionButtonTheme extends ThemeExtension<ConnectionButtonTheme> {
|
||||
const ConnectionButtonTheme({
|
||||
this.idleColor,
|
||||
this.connectedColor,
|
||||
});
|
||||
|
||||
final Color? idleColor;
|
||||
final Color? connectedColor;
|
||||
|
||||
static const ConnectionButtonTheme light = ConnectionButtonTheme(
|
||||
idleColor: Color(0xFF4a4d8b),
|
||||
connectedColor: Color(0xFF44a334),
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ConnectionButtonTheme> copyWith({
|
||||
Color? idleColor,
|
||||
Color? connectedColor,
|
||||
}) =>
|
||||
ConnectionButtonTheme(
|
||||
idleColor: idleColor ?? this.idleColor,
|
||||
connectedColor: connectedColor ?? this.connectedColor,
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ConnectionButtonTheme> lerp(
|
||||
covariant ThemeExtension<ConnectionButtonTheme>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! ConnectionButtonTheme) {
|
||||
return this;
|
||||
}
|
||||
return ConnectionButtonTheme(
|
||||
idleColor: Color.lerp(idleColor, other.idleColor, t),
|
||||
connectedColor: Color.lerp(connectedColor, other.connectedColor, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
lib/core/theme/app_theme_mode.dart
Normal file
25
lib/core/theme/app_theme_mode.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
|
||||
enum AppThemeMode {
|
||||
system,
|
||||
light,
|
||||
dark,
|
||||
black;
|
||||
|
||||
String present(TranslationsEn t) => switch (this) {
|
||||
system => t.settings.general.themeModes.system,
|
||||
light => t.settings.general.themeModes.light,
|
||||
dark => t.settings.general.themeModes.dark,
|
||||
black => t.settings.general.themeModes.black,
|
||||
};
|
||||
|
||||
ThemeMode get flutterThemeMode => switch (this) {
|
||||
system => ThemeMode.system,
|
||||
light => ThemeMode.light,
|
||||
dark => ThemeMode.dark,
|
||||
black => ThemeMode.dark,
|
||||
};
|
||||
|
||||
bool get trueBlack => this == black;
|
||||
}
|
||||
40
lib/core/theme/theme_extensions.dart
Normal file
40
lib/core/theme/theme_extensions.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConnectionButtonTheme extends ThemeExtension<ConnectionButtonTheme> {
|
||||
const ConnectionButtonTheme({
|
||||
this.idleColor,
|
||||
this.connectedColor,
|
||||
});
|
||||
|
||||
final Color? idleColor;
|
||||
final Color? connectedColor;
|
||||
|
||||
static const ConnectionButtonTheme light = ConnectionButtonTheme(
|
||||
idleColor: Color(0xFF4a4d8b),
|
||||
connectedColor: Color(0xFF44a334),
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ConnectionButtonTheme> copyWith({
|
||||
Color? idleColor,
|
||||
Color? connectedColor,
|
||||
}) =>
|
||||
ConnectionButtonTheme(
|
||||
idleColor: idleColor ?? this.idleColor,
|
||||
connectedColor: connectedColor ?? this.connectedColor,
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ConnectionButtonTheme> lerp(
|
||||
covariant ThemeExtension<ConnectionButtonTheme>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! ConnectionButtonTheme) {
|
||||
return this;
|
||||
}
|
||||
return ConnectionButtonTheme(
|
||||
idleColor: Color.lerp(idleColor, other.idleColor, t),
|
||||
connectedColor: Color.lerp(connectedColor, other.connectedColor, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/core/theme/theme_preferences.dart
Normal file
26
lib/core/theme/theme_preferences.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/core/theme/app_theme_mode.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'theme_preferences.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ThemePreferences extends _$ThemePreferences {
|
||||
@override
|
||||
AppThemeMode build() {
|
||||
final persisted = ref
|
||||
.watch(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.getString("theme_mode");
|
||||
if (persisted == null) return AppThemeMode.system;
|
||||
return AppThemeMode.values.byName(persisted);
|
||||
}
|
||||
|
||||
Future<void> changeThemeMode(AppThemeMode value) async {
|
||||
state = value;
|
||||
await ref
|
||||
.read(sharedPreferencesProvider)
|
||||
.requireValue
|
||||
.setString("theme_mode", value.name);
|
||||
}
|
||||
}
|
||||
48
lib/core/utils/exception_handler.dart
Normal file
48
lib/core/utils/exception_handler.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
mixin ExceptionHandler implements LoggerMixin {
|
||||
TaskEither<F, R> exceptionHandler<F, R>(
|
||||
Future<Either<F, R>> Function() run,
|
||||
F Function(Object error, StackTrace stackTrace) onError,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
try {
|
||||
return await run();
|
||||
} catch (error, stackTrace) {
|
||||
return Left(onError(error, stackTrace));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamExceptionHandler<R extends Object?> on Stream<R> {
|
||||
Stream<Either<F, R>> handleExceptions<F>(
|
||||
F Function(Object error, StackTrace stackTrace) onError,
|
||||
) {
|
||||
return map(right<F, R>).onErrorReturnWith(
|
||||
(error, stackTrace) {
|
||||
return Left(onError(error, stackTrace));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension TaskEitherExceptionHandler<F, R> on TaskEither<F, R> {
|
||||
TaskEither<F, R> handleExceptions(
|
||||
F Function(Object error, StackTrace stackTrace) onError,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
try {
|
||||
return await run();
|
||||
} catch (error, stackTrace) {
|
||||
return Left(onError(error, stackTrace));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
15
lib/core/utils/ffi_utils.dart
Normal file
15
lib/core/utils/ffi_utils.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
R withMemory<R, T extends NativeType>(
|
||||
int size,
|
||||
R Function(Pointer<T> memory) action,
|
||||
) {
|
||||
final memory = calloc<Int8>(size);
|
||||
try {
|
||||
return action(memory.cast());
|
||||
} finally {
|
||||
calloc.free(memory);
|
||||
}
|
||||
}
|
||||
11
lib/core/utils/json_converters.dart
Normal file
11
lib/core/utils/json_converters.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
class IntervalInSecondsConverter implements JsonConverter<Duration, int> {
|
||||
const IntervalInSecondsConverter();
|
||||
|
||||
@override
|
||||
Duration fromJson(int json) => Duration(seconds: json);
|
||||
|
||||
@override
|
||||
int toJson(Duration object) => object.inSeconds;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
|
||||
class CustomAlertDialog extends StatelessWidget {
|
||||
const CustomAlertDialog({
|
||||
|
||||
Reference in New Issue
Block a user