Backup before removing hiddify references

This commit is contained in:
Hiddify User
2026-01-15 12:28:40 +03:00
parent f54603d129
commit 36d9e31236
231 changed files with 6648 additions and 1832 deletions

View File

@@ -20,7 +20,8 @@ bool _testCrashReport = false;
class AnalyticsController extends _$AnalyticsController with AppLogger {
@override
Future<bool> build() async {
return _preferences.getBool(enableAnalyticsPrefKey) ?? true;
// UMBRIX: По умолчанию ВЫКЛЮЧЕНО для приватности
return _preferences.getBool(enableAnalyticsPrefKey) ?? false;
}
SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue;
@@ -45,13 +46,21 @@ class AnalyticsController extends _$AnalyticsController with AppLogger {
options.environment = env.name;
options.dist = appInfo.release.name;
options.debug = kDebugMode;
// UMBRIX: Только fatal crashes, никакой аналитики
options.enableNativeCrashHandling = true;
options.enableNdkScopeSync = true;
// options.attachScreenshot = true;
// UMBRIX: Отключаем всю трассировку и слежку
options.tracesSampleRate = 0.0; // Было 0.20
options.enableUserInteractionTracing = false; // Было true
options.enableAutoPerformanceTracing = false;
options.attachScreenshot = false;
options.attachViewHierarchy = false;
// UMBRIX: Максимальная анонимизация
options.serverName = "";
options.attachThreads = true;
options.tracesSampleRate = 0.20;
options.enableUserInteractionTracing = true;
options.attachThreads = false; // Было true
options.addIntegration(sentryLogger);
options.beforeSend = sentryBeforeSend;
},

View File

@@ -13,8 +13,7 @@ class HapticService extends _$HapticService {
}
static const String hapticFeedbackPrefKey = "haptic_feedback";
SharedPreferences get _preferences =>
ref.read(sharedPreferencesProvider).requireValue;
SharedPreferences get _preferences => ref.read(sharedPreferencesProvider).requireValue;
Future<void> updatePreference(bool value) async {
state = value;

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter_loggy_dio/flutter_loggy_dio.dart';
import 'package:hiddify/utils/custom_loggers.dart';
class DioHttpClient with InfraLogger {
@@ -14,7 +13,7 @@ class DioHttpClient with InfraLogger {
required String userAgent,
required bool debug,
}) {
for (var mode in ["proxy", "direct", "both"]) {
for (final mode in ["proxy", "direct", "both"]) {
_dio[mode] = Dio(
BaseOptions(
connectTimeout: timeout,
@@ -31,7 +30,7 @@ class DioHttpClient with InfraLogger {
if (mode != "proxy") ...[
const Duration(seconds: 2),
const Duration(seconds: 3),
]
],
],
),
);

View File

@@ -1,19 +1,14 @@
abstract class Constants {
static const appName = "Hiddify";
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/privacy-policy/";
static const termsAndConditionsUrl = "https://hiddify.com/terms/";
static const cfWarpPrivacyPolicy =
"https://www.cloudflare.com/application/privacypolicy/";
static const cfWarpTermsOfService =
"https://www.cloudflare.com/application/terms/";
static const appName = "Umbrix";
static const githubUrl = "https://github.com/umbrix-app/umbrix";
static const githubReleasesApiUrl = "https://api.github.com/repos/umbrix-app/umbrix/releases";
static const githubLatestReleaseUrl = "https://github.com/umbrix-app/umbrix/releases/latest";
static const appCastUrl = "https://raw.githubusercontent.com/umbrix-app/umbrix/main/appcast.xml";
static const telegramChannelUrl = "https://t.me/umbrix_app";
static const privacyPolicyUrl = "https://umbrix.net/privacy.html";
static const termsAndConditionsUrl = "https://umbrix.net/terms.html";
static const cfWarpPrivacyPolicy = "https://www.cloudflare.com/application/privacypolicy/";
static const cfWarpTermsOfService = "https://www.cloudflare.com/application/terms/";
}
const kAnimationDuration = Duration(milliseconds: 250);

View File

@@ -8,6 +8,7 @@ enum Region {
id,
tr,
br,
in_, // India
other;
String present(TranslationsEn t) => switch (this) {
@@ -18,6 +19,7 @@ enum Region {
af => t.settings.general.regions.af,
id => t.settings.general.regions.id,
br => t.settings.general.regions.br,
in_ => "India", // Добавим в переводы позже
other => t.settings.general.regions.other,
};
}

View File

@@ -0,0 +1,30 @@
/// UMBRIX: Секретные ключи для Telegram Bot (отправка логов)
///
/// ⚠️ ВАЖНО: НЕ КОММИТИТЬ ЭТОТ ФАЙЛ В GIT!
/// Добавьте в .gitignore: lib/core/model/secrets.dart
///
/// Инструкция по получению токена:
/// 1. Откройте Telegram, найдите @BotFather
/// 2. Отправьте команду /newbot
/// 3. Следуйте инструкциям, придумайте имя (например, UmbrixLogsBot)
/// 4. Скопируйте токен и вставьте ниже
/// 5. Отправьте команду /mybots → выберите бота → Bot Settings → Group Privacy → Turn OFF
/// 6. Добавьте бота в свою группу/канал
/// 7. Отправьте любое сообщение в группу
/// 8. Откройте: https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
/// 9. Найдите "chat":{"id":-100XXXXXXXXX} и скопируйте этот ID
library;
abstract class Secrets {
/// Токен Telegram бота для отправки логов
/// Получить: @BotFather в Telegram → /newbot
static const String telegramBotToken = ""; // ← ВСТАВЬТЕ СЮДА ВАШ ТОКЕН
/// ID чата/канала куда отправлять логи
/// Формат: -100XXXXXXXXX для каналов/групп
/// Или просто число для личных сообщений
static const String telegramChatId = ""; // ← ВСТАВЬТЕ СЮДА ID ЧАТА
/// Проверка что токены настроены
static bool get isConfigured => telegramBotToken.isNotEmpty && telegramChatId.isNotEmpty;
}

View File

@@ -5,7 +5,10 @@ import 'package:hiddify/core/preferences/actions_at_closing.dart';
// import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/utils/preferences_utils.dart';
import 'package:hiddify/features/connection/model/connection_status.dart';
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -33,7 +36,7 @@ abstract class Preferences {
static final perAppProxyMode = PreferencesNotifier.create<PerAppProxyMode, String>(
"per_app_proxy_mode",
PerAppProxyMode.off,
PerAppProxyMode.exclude,
mapFrom: PerAppProxyMode.values.byName,
mapTo: (value) => value.name,
);
@@ -103,14 +106,45 @@ class PerAppProxyList extends _$PerAppProxyList {
);
@override
List<String> build() => ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include ? _include.read() : _exclude.read();
List<String> build() {
// Слушаем изменения режима и перестраиваем список
final mode = ref.watch(Preferences.perAppProxyMode);
return mode == PerAppProxyMode.include ? _include.read() : _exclude.read();
}
Future<void> update(List<String> value) {
state = value;
if (ref.read(Preferences.perAppProxyMode) == PerAppProxyMode.include) {
return _include.write(value);
Future<void> update(List<String> value) async {
print('[PerAppProxyList] update() вызван с ${value.length} приложениями');
final mode = ref.read(Preferences.perAppProxyMode);
print('[PerAppProxyList] Текущий режим: $mode');
// Сначала сохраняем в SharedPreferences
if (mode == PerAppProxyMode.include) {
await _include.write(value);
print('[PerAppProxyList] Записан include список: $value');
} else {
await _exclude.write(value);
print('[PerAppProxyList] Записан exclude список: $value');
}
// Затем обновляем локальное состояние
state = value;
print('[PerAppProxyList] State обновлён');
// Автоматически перезапускаем VPN если он активен
print('[PerAppProxyList] Вызываю _reconnectVpnIfActive()');
await _reconnectVpnIfActive();
}
Future<void> _reconnectVpnIfActive() async {
try {
final connectionNotifier = await ref.read(connectionNotifierProvider.future);
if (connectionNotifier is Connected) {
final profile = await ref.read(activeProfileProvider.future);
await ref.read(connectionNotifierProvider.notifier).reconnect(profile);
}
} catch (_) {
// Игнорируем ошибки если connection provider не инициализирован
}
return _exclude.write(value);
}
}
@@ -125,8 +159,22 @@ class ExcludedDomainsList extends _$ExcludedDomainsList {
@override
List<String> build() => _pref.read();
Future<void> update(List<String> value) {
Future<void> update(List<String> value) async {
state = value;
return _pref.write(value);
await _pref.write(value);
// Автоматически перезапускаем VPN если он активен
await _reconnectVpnIfActive();
}
Future<void> _reconnectVpnIfActive() async {
try {
final connectionNotifier = await ref.read(connectionNotifierProvider.future);
if (connectionNotifier is Connected) {
final profile = await ref.read(activeProfileProvider.future);
await ref.read(connectionNotifierProvider.notifier).reconnect(profile);
}
} catch (_) {
// Игнорируем ошибки если connection provider не инициализирован
}
}
}

View File

@@ -55,7 +55,6 @@ final tabLocations = [
const PerAppProxyRoute().location,
const ConfigOptionsRoute().location,
const SettingsRoute().location,
const LogsOverviewRoute().location,
const AboutRoute().location,
];

View File

@@ -319,14 +319,14 @@ class ConfigOptionsRoute extends GoRouteData {
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
if (useMobileRouter) {
return MaterialPage(
return const MaterialPage(
name: name,
child: ConfigOptionsPage(section: section),
child: ConfigOptionsPage(),
);
}
return NoTransitionPage(
return const NoTransitionPage(
name: name,
child: ConfigOptionsPage(section: section),
child: ConfigOptionsPage(),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:hiddify/core/telegram_config.dart';
import 'package:hiddify/utils/utils.dart';
/// Сервис для анонимной отправки логов в Telegram
class TelegramLogger with InfraLogger {
static const int maxLogSize = 4096; // Telegram ограничение на текст сообщения
static const int maxFileSize = 50 * 1024 * 1024; // 50MB максимум для файла
final Dio _dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
/// Отправить логи как текстовое сообщение (для коротких логов)
Future<bool> sendLogsAsText(String logs, {String? deviceInfo}) async {
if (!TelegramConfig.isConfigured) {
loggy.warning('Telegram not configured');
return false;
}
try {
final message = _formatMessage(logs, deviceInfo);
final response = await _dio.post(
'https://api.telegram.org/bot${TelegramConfig.botToken}/sendMessage',
data: {
'chat_id': TelegramConfig.chatId,
'text': message,
'parse_mode': 'HTML',
},
);
return response.statusCode == 200;
} catch (e) {
loggy.error('Failed to send logs to Telegram', e);
return false;
}
}
/// Отправить файл логов (для больших логов)
Future<bool> sendLogsAsFile(File logFile, {String? deviceInfo}) async {
if (!TelegramConfig.isConfigured) {
loggy.warning('Telegram not configured');
return false;
}
try {
// Проверка размера файла
final fileSize = await logFile.length();
if (fileSize > maxFileSize) {
loggy.warning('Log file too large: $fileSize bytes');
return false;
}
final fileName = logFile.path.split('/').last;
final caption = deviceInfo != null ? '📱 Device: $deviceInfo\n📅 ${DateTime.now().toIso8601String()}' : '📅 ${DateTime.now().toIso8601String()}';
final formData = FormData.fromMap({
'chat_id': TelegramConfig.chatId,
'document': await MultipartFile.fromFile(
logFile.path,
filename: fileName,
),
'caption': caption,
});
final response = await _dio.post(
'https://api.telegram.org/bot${TelegramConfig.botToken}/sendDocument',
data: formData,
);
return response.statusCode == 200;
} catch (e) {
loggy.error('Failed to send log file to Telegram', e);
return false;
}
}
String _formatMessage(String logs, String? deviceInfo) {
final header = deviceInfo != null ? '📱 <b>Umbrix Logs</b>\nDevice: $deviceInfo\n' : '📱 <b>Umbrix Logs</b>\n';
final timestamp = '📅 ${DateTime.now().toIso8601String()}\n';
const separator = '━━━━━━━━━━━━━━━━\n';
// Обрезаем логи если они слишком длинные
var logContent = logs;
final maxContentSize = maxLogSize - header.length - timestamp.length - separator.length - 50;
if (logContent.length > maxContentSize) {
logContent = '${logContent.substring(0, maxContentSize)}\n\n... (truncated)';
}
return '$header$timestamp$separator$logContent';
}
/// Получить информацию об устройстве для логов (анонимно)
static String getAnonymousDeviceInfo() {
// Только общая информация без идентификаторов
if (Platform.isAndroid) {
return 'Android ${Platform.operatingSystemVersion}';
} else if (Platform.isIOS) {
return 'iOS ${Platform.operatingSystemVersion}';
} else if (Platform.isWindows) {
return 'Windows';
} else if (Platform.isMacOS) {
return 'macOS';
} else if (Platform.isLinux) {
return 'Linux';
}
return 'Unknown OS';
}
}

View File

@@ -0,0 +1,20 @@
/// ⚠️ НЕ КОММИТЬТЕ ЭТОТ ФАЙЛ В GIT!
/// Замените значения ниже на реальные данные вашего Telegram бота
///
/// Инструкция по настройке: см. TELEGRAM_SETUP.md
library;
class TelegramConfig {
/// Токен вашего Telegram бота от @BotFather
/// Пример: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz123456789'
static const String botToken = 'YOUR_BOT_TOKEN_HERE';
/// Chat ID группы/канала куда отправлять логи
/// Пример: '-1001234567890'
static const String chatId = 'YOUR_CHAT_ID_HERE';
/// Проверка что конфиг настроен
static bool get isConfigured {
return botToken != 'YOUR_BOT_TOKEN_HERE' && chatId != 'YOUR_CHAT_ID_HERE';
}
}

View File

@@ -0,0 +1,39 @@
// 🔐 ИНСТРУКЦИЯ ПО НАСТРОЙКЕ TELEGRAM БОТА
//
// 1. Создайте Telegram бота через @BotFather:
// - Откройте Telegram и найдите @BotFather
// - Отправьте команду: /newbot
// - Введите имя бота: Umbrix Log Bot
// - Введите username: @umbrix_logs_bot (или любой свободный)
// - Скопируйте TOKEN который выдаст BotFather
//
// 2. Получите CHAT_ID:
// - Создайте приватную группу/канал для логов
// - Добавьте туда бота
// - Отправьте любое сообщение в группу
// - Откройте: https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
// - Найдите "chat":{"id":-1001234567890} - это ваш CHAT_ID
//
// 3. Скопируйте этот файл в telegram_config.dart:
// cp lib/core/telegram_config.dart.example lib/core/telegram_config.dart
//
// 4. Вставьте свои данные ниже
//
// 5. Добавьте в .gitignore (чтобы не попало в репозиторий):
// lib/core/telegram_config.dart
//
/// ⚠️ НЕ КОММИТЬТЕ ЭТОТ ФАЙЛ В GIT!
class TelegramConfig {
/// Токен вашего Telegram бота от @BotFather
static const String botToken = 'YOUR_BOT_TOKEN_HERE';
/// ID чата/канала куда отправлять логи (с минусом для групп)
static const String chatId = 'YOUR_CHAT_ID_HERE';
/// URL Telegram Bot API
static const String apiUrl = 'https://api.telegram.org';
/// Максимальный размер одного сообщения (4096 символов - лимит Telegram)
static const int maxMessageLength = 4000;
}

View File

@@ -20,15 +20,92 @@ class AppTheme {
}
ThemeData darkTheme(ColorScheme? darkColorScheme) {
final ColorScheme scheme = darkColorScheme ??
ColorScheme.fromSeed(
seedColor: const Color(0xFF293CA0),
brightness: Brightness.dark,
);
// Кастомная темная тема с хорошей контрастностью для блоков
const ColorScheme scheme = ColorScheme(
brightness: Brightness.dark,
// Основной цвет Outline - бирюзовый
primary: Color(0xFF2fbea5), // hsl(170, 60%, 46%)
onPrimary: Color(0xFFFFFFFF), // Белый текст на кнопках
primaryContainer: Color(0xFF005048),
onPrimaryContainer: Color(0xFF7df8dd),
// Фон карточек/блоков - заметно светлее фона приложения
surface: Color(0xFF263238), // Светло-серый для карточек
onSurface: Color(0xFFE1E2E6), // Светлый текст на карточках
surfaceContainerHighest: Color(0xFF37474F), // Еще светлее для выделенных элементов
// Дополнительные цвета
secondary: Color(0xFF2fbea5),
onSecondary: Color(0xFFFFFFFF), // Белый текст
secondaryContainer: Color(0xFF005048),
onSecondaryContainer: Color(0xFF7df8dd),
// Ошибки
error: Color(0xFFf44336),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFF93000a),
onErrorContainer: Color(0xFFffdad6),
// Контуры и границы - видимые
outline: Color(0xFF4CAF50), // Зеленоватый контур для видимости
outlineVariant: Color(0xFF546E7A),
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: mode.trueBlack ? Colors.black : scheme.background,
scaffoldBackgroundColor: const Color(0xFF191f23), // Очень темный фон
cardTheme: CardTheme(
elevation: 4,
shadowColor: Colors.black45,
surfaceTintColor: const Color(0xFF2fbea5), // Легкий бирюзовый оттенок
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFF37474F), // Видимая граница карточки
),
),
),
// Настройка текста кнопок
textTheme: const TextTheme(
labelLarge: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStateProperty.all(const Color(0xFFFFFFFF)), // Белый текст
backgroundColor: WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.disabled)) {
return const Color(0xFF263238).withOpacity(0.5);
}
return const Color(0xFF263238); // Темно-серый фон
}),
elevation: WidgetStateProperty.all(4),
shadowColor: WidgetStateProperty.all(Colors.black45),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
textStyle: WidgetStateProperty.all(
const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2fbea5), // Бирюзовый текст
side: const BorderSide(color: Color(0xFF2fbea5), width: 1.5),
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
fontFamily: fontFamily,
extensions: const <ThemeExtension<dynamic>>{
ConnectionButtonTheme.light,

View File

@@ -4,22 +4,17 @@ import 'package:hiddify/core/localization/translations.dart';
enum AppThemeMode {
system,
light,
dark,
black;
dark;
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

@@ -9,7 +9,8 @@ class ThemePreferences extends _$ThemePreferences {
@override
AppThemeMode build() {
final persisted = ref.watch(sharedPreferencesProvider).requireValue.getString("theme_mode");
if (persisted == null) return AppThemeMode.system;
// UMBRIX: Темная тема по умолчанию
if (persisted == null) return AppThemeMode.dark;
return AppThemeMode.values.byName(persisted);
}

View File

@@ -106,7 +106,7 @@ class PreferencesNotifier<T, P> extends StateNotifier<T> {
final List<T>? possibleValues;
static StateNotifierProvider<PreferencesNotifier<T, P>, T> create<T, P>(String key, T defaultValue,
{T Function(Ref ref)? defaultValueFunction, T Function(P value)? mapFrom, P Function(T value)? mapTo, bool Function(T value)? validator, T? overrideValue, List<T>? possibleValues}) =>
{T Function(Ref ref)? defaultValueFunction, T Function(P value)? mapFrom, P Function(T value)? mapTo, bool Function(T value)? validator, T? overrideValue, List<T>? possibleValues,}) =>
StateNotifierProvider(
(ref) => PreferencesNotifier._(
ref: ref,
@@ -119,7 +119,7 @@ class PreferencesNotifier<T, P> extends StateNotifier<T> {
validator: validator,
),
overrideValue: overrideValue,
possibleValues: possibleValues),
possibleValues: possibleValues,),
);
static AutoDisposeStateNotifierProvider<PreferencesNotifier<T, P>, T> createAutoDispose<T, P>(

View File

@@ -20,7 +20,6 @@ class CustomAlertDialog extends StatelessWidget {
Future<void> show(BuildContext context) async {
await showDialog(
context: context,
useRootNavigator: true,
builder: (context) => this,
);
}