This commit is contained in:
problematicconsumer
2023-12-01 12:56:24 +03:30
parent 9c165e178b
commit ed614988a2
181 changed files with 3092 additions and 2341 deletions

View File

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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/";
}

View File

@@ -0,0 +1,7 @@
import 'dart:io';
typedef Directories = ({
Directory baseDir,
Directory workingDir,
Directory tempDir
});

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
export 'app_theme.dart';
export 'general_prefs.dart';
export 'locale_prefs.dart';
export 'theme_prefs.dart';

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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