Backup before removing hiddify references
This commit is contained in:
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
30
lib/core/model/secrets.dart
Normal file
30
lib/core/model/secrets.dart
Normal 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;
|
||||
}
|
||||
@@ -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 не инициализирован
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ final tabLocations = [
|
||||
const PerAppProxyRoute().location,
|
||||
const ConfigOptionsRoute().location,
|
||||
const SettingsRoute().location,
|
||||
const LogsOverviewRoute().location,
|
||||
const AboutRoute().location,
|
||||
];
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
116
lib/core/telegram/telegram_logger.dart
Normal file
116
lib/core/telegram/telegram_logger.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
20
lib/core/telegram_config.dart
Normal file
20
lib/core/telegram_config.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
39
lib/core/telegram_config.dart.example
Normal file
39
lib/core/telegram_config.dart.example
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -20,7 +20,6 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
Future<void> show(BuildContext context) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => this,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user