From 2058aba4833135f79318bc08e81258747c8261cf Mon Sep 17 00:00:00 2001 From: Umbrix Developer Date: Thu, 22 Jan 2026 07:07:04 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20v1.7.8:=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC=D0=B0=20=D0=B1=D0=B0=D0=B3=D1=80=D0=B5=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D0=B2=20=D0=B2=20Telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Автоматический сбор диагностики (статус, пинг, протокол, логи) - Кнопка 'Сообщить о проблеме' в настройках - Анонимная отправка багрепортов в Telegram - Telegram бот настроен и готов к работе --- .env.example | 10 + .gitignore | 6 +- BUG_REPORT_INTEGRATION.md | 359 ++++++++++++++++++ BUG_REPORT_SYSTEM.md | 225 +++++++++++ TELEGRAM_BOT_SETUP_RU.md | 254 +++++++++++++ build_with_sentry.sh | 53 +++ lib/bootstrap.dart | 9 - lib/core/telegram_config.dart | 4 +- lib/features/app/widget/app.dart | 28 -- .../app_update/widget/new_version_dialog.dart | 48 ++- .../bug_report/data/bug_report_service.dart | 285 ++++++++++++++ .../bug_report/widget/bug_report_dialog.dart | 162 ++++++++ .../settings/widgets/logs_setting_tiles.dart | 15 + pubspec.yaml | 2 +- 14 files changed, 1404 insertions(+), 56 deletions(-) create mode 100644 .env.example create mode 100644 BUG_REPORT_INTEGRATION.md create mode 100644 BUG_REPORT_SYSTEM.md create mode 100644 TELEGRAM_BOT_SETUP_RU.md create mode 100755 build_with_sentry.sh create mode 100644 lib/features/bug_report/data/bug_report_service.dart create mode 100644 lib/features/bug_report/widget/bug_report_dialog.dart diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..d63dd05b --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Конфигурация для разработки (пример) +# Скопируй в .env.local и заполни реальными значениями + +# Sentry DSN для сбора crash reports +# Получи из: https://umbrix-dj.sentry.io/ → Projects → umbrix-app → Settings → Client Keys +# Формат: https://публичный_ключ@o123.ingest.sentry.io/456 +SENTRY_DSN= + +# Пример (ЗАМЕНИ на свой): +# SENTRY_DSN=https://abc123def456@o4510744763170816.ingest.sentry.io/789456 diff --git a/.gitignore b/.gitignore index 1bb1a25c..5910c502 100644 --- a/.gitignore +++ b/.gitignore @@ -59,5 +59,9 @@ app.*.map.json /data # FVM Version Cache -.fvm/lib/core/telegram_config.dart +.fvm/ + +# Secrets +lib/core/telegram_config.dart android/key.properties +.env.local diff --git a/BUG_REPORT_INTEGRATION.md b/BUG_REPORT_INTEGRATION.md new file mode 100644 index 00000000..396d03dd --- /dev/null +++ b/BUG_REPORT_INTEGRATION.md @@ -0,0 +1,359 @@ +# 🔗 Интеграция системы багрепортов с существующими логами + +## Архитектура логирования в Umbrix + +### Файлы логов + +``` +~/.local/share/umbrix/logs/ (или другая директория на основе платформы) +├── box.log # Логи ядра (singbox core) +└── app.log # Логи приложения (Flutter) +``` + +### Ротация логов + +Файлы автоматически ротируются при достижении 5MB: +- `box.log` → `box.log.1` → `box.log.2` +- Максимум 2 backup файла + +## Как подключиться к логам + +### 1. LogRepository (Основной источник) + +```dart +// Получить репозиторий логов +final logRepository = await ref.read(logRepositoryProvider.future); + +// Подписаться на поток логов в реальном времени +logRepository.watchLogs().listen((either) { + either.fold( + (failure) => print('Error: $failure'), + (logs) { + for (final log in logs) { + print('${log.time} [${log.level}] ${log.message}'); + } + }, + ); +}); +``` + +### 2. LogPathResolver (Пути к файлам) + +```dart +// Получить пути к файлам логов +final logPathResolver = ref.read(logPathResolverProvider); + +// Core logs +final coreLogFile = logPathResolver.coreFile(); +print('Core logs: ${coreLogFile.path}'); + +// App logs +final appLogFile = logPathResolver.appFile(); +print('App logs: ${appLogFile.path}'); + +// Директория логов +final logsDir = logPathResolver.directory; +print('Logs directory: ${logsDir.path}'); +``` + +### 3. Чтение файлов напрямую + +```dart +import 'dart:io'; + +// Прочитать все логи +final coreLogFile = logPathResolver.coreFile(); +if (await coreLogFile.exists()) { + final content = await coreLogFile.readAsString(); + print(content); +} + +// Прочитать последние N строк +Future readLastLines(File file, int maxLines) async { + final lines = await file.readAsLines(); + final lastLines = lines.length > maxLines + ? lines.sublist(lines.length - maxLines) + : lines; + return lastLines.join('\n'); +} + +final last100Lines = await readLastLines(coreLogFile, 100); +``` + +## Что собирает BugReportService + +### Диагностическая информация + +```dart +class DiagnosticInfo { + // 1. Информация об устройстве (анонимно) + final String deviceInfo; // "Android 12" / "iOS 15" / "Windows" + + // 2. Статус подключения + final String connectionStatus; // "CONNECTED" / "DISCONNECTED" / etc + final String? connectionError; // Текст ошибки если есть + + // 3. Информация о прокси + final String? activeProxyInfo; // "Тип: vmess, Тег: server-1, Пинг: 120ms" + final int? pingDelay; // Задержка в мс (120, 450, etc) + + // 4. Логи (последние 100 строк) + final String coreLogsPreview; // Из box.log + final String appLogsPreview; // Из app.log + + // 5. Метаданные + final DateTime timestamp; // Время создания отчёта +} +``` + +### Источники данных + +| Данные | Источник | Provider/Service | +|--------|----------|------------------| +| **Device Info** | `Platform.operatingSystem` | `TelegramLogger.getAnonymousDeviceInfo()` | +| **Connection Status** | `ConnectionNotifier` | `connectionNotifierProvider` | +| **Active Proxy** | `ActiveProxyNotifier` | `activeProxyNotifierProvider` | +| **Ping Delay** | `ProxyItemEntity.urlTestDelay` | Из активного прокси | +| **Core Logs** | `box.log` файл | `logPathResolver.coreFile()` | +| **App Logs** | `app.log` файл | `logPathResolver.appFile()` | + +## Как добавить дополнительную информацию + +### 1. Расширить DiagnosticInfo + +```dart +// В bug_report_service.dart +class DiagnosticInfo { + // Добавьте новое поле + final String? vpnStatus; + final int? memoryUsage; + final String? networkType; + + // ... остальное +} +``` + +### 2. Обновить метод collectDiagnostics() + +```dart +Future collectDiagnostics() async { + // ... существующий код + + // Добавьте новый сбор данных + String? vpnStatus; + try { + // Ваша логика получения VPN статуса + vpnStatus = await getVpnStatus(); + } catch (e) { + vpnStatus = 'Ошибка: $e'; + } + + return DiagnosticInfo( + // ... существующие поля + vpnStatus: vpnStatus, + ); +} +``` + +### 3. Обновить форматирование + +```dart +String _formatBugReport(...) { + // ... существующий код + + // Добавьте в отчёт + if (diagnostics.vpnStatus != null) { + buffer.writeln('📡 VPN:'); + buffer.writeln(diagnostics.vpnStatus); + buffer.writeln(); + } + + // ... остальное +} +``` + +## Подключение к метрикам Singbox + +### Получить статистику трафика + +```dart +// Singbox предоставляет метрики +final singbox = ref.read(singboxServiceProvider); + +// В будущем можно добавить: +// - Общий трафик (upload/download) +// - Количество подключений +// - Ошибки DNS +// - etc +``` + +### Пример расширенной диагностики + +```dart +Future collectDiagnostics() async { + // ... существующий код + + // Дополнительные данные из singbox + String? trafficInfo; + try { + // Если singbox предоставляет API для статистики + final stats = await singbox.getStats(); + trafficInfo = 'Upload: ${stats.upload}, Download: ${stats.download}'; + } catch (e) { + trafficInfo = 'Недоступно'; + } + + return DiagnosticInfo( + // ... остальные поля + trafficInfo: trafficInfo, + ); +} +``` + +## Логирование в приложении + +### Использование логгера + +```dart +import 'package:umbrix/utils/custom_loggers.dart'; + +class MyService with InfraLogger { // или AppLogger, CoreLogger + void someMethod() { + loggy.debug('Debug message'); + loggy.info('Info message'); + loggy.warning('Warning message'); + loggy.error('Error message', error, stackTrace); + } +} +``` + +### Типы логгеров + +- `InfraLogger` — инфраструктурные логи (network, IO, etc) +- `AppLogger` — логи приложения (UI, бизнес-логика) +- `CoreLogger` — логи ядра (singbox) + +Все они пишутся в соответствующие файлы через `LogRepository`. + +## Расширенная интеграция с Telegram + +### Форматирование для Telegram + +```dart +// Telegram поддерживает HTML разметку +final message = ''' +🐛 БАГРЕПОРТ +Устройство: $deviceInfo +Статус: $connectionStatus + +
+Логи:
+$logs
+
+'''; + +await telegramLogger.sendLogsAsText(message); +``` + +### Отправка нескольких файлов + +```dart +// Можно отправить Core и App логи отдельно +await telegramLogger.sendLogsAsFile( + logPathResolver.coreFile(), + deviceInfo: 'Core Logs - $deviceInfo', +); + +await telegramLogger.sendLogsAsFile( + logPathResolver.appFile(), + deviceInfo: 'App Logs - $deviceInfo', +); +``` + +## Дебаг и тестирование + +### Посмотреть текущие логи + +```dart +// В debug режиме можно вывести текущие логи +void debugPrintLogs() async { + final logPathResolver = ref.read(logPathResolverProvider); + + print('=== CORE LOGS ==='); + final coreFile = logPathResolver.coreFile(); + if (await coreFile.exists()) { + final lines = await coreFile.readAsLines(); + lines.take(10).forEach(print); // Первые 10 строк + } + + print('=== APP LOGS ==='); + final appFile = logPathResolver.appFile(); + if (await appFile.exists()) { + final lines = await appFile.readAsLines(); + lines.take(10).forEach(print); // Первые 10 строк + } +} +``` + +### Тест диагностики + +```dart +void testDiagnostics() async { + final service = ref.read(bugReportServiceProvider); + final diagnostics = await service.collectDiagnostics(); + + print('Device: ${diagnostics.deviceInfo}'); + print('Connection: ${diagnostics.connectionStatus}'); + print('Proxy: ${diagnostics.activeProxyInfo}'); + print('Ping: ${diagnostics.pingDelay}ms'); + print('Core logs (first 5 lines):'); + print(diagnostics.coreLogsPreview.split('\n').take(5).join('\n')); +} +``` + +## FAQ + +### Q: Где физически хранятся логи? +**A:** Зависит от платформы: +- Android: `/data/data/com.hiddify.umbrix/files/logs/` +- iOS: `Application Support/logs/` +- Desktop: `~/.local/share/umbrix/logs/` (Linux), `~/Library/Application Support/umbrix/logs/` (macOS) + +### Q: Как очистить логи? +**A:** +```dart +final logRepository = await ref.read(logRepositoryProvider.future); +await logRepository.clearLogs(); +``` + +### Q: Можно ли собирать логи без отправки? +**A:** Да: +```dart +final service = ref.read(bugReportServiceProvider); +final diagnostics = await service.collectDiagnostics(); +// diagnostics теперь содержит всю информацию +``` + +### Q: Как добавить скриншот к багрепорту? +**A:** Нужно расширить `TelegramLogger`: +```dart +Future sendPhotoWithCaption(File photo, String caption) async { + // Используйте Telegram Bot API endpoint sendPhoto + final formData = FormData.fromMap({ + 'chat_id': TelegramConfig.chatId, + 'photo': await MultipartFile.fromFile(photo.path), + 'caption': caption, + }); + + final response = await _dio.post( + 'https://api.telegram.org/bot${TelegramConfig.botToken}/sendPhoto', + data: formData, + ); + + return response.statusCode == 200; +} +``` + +--- + +**Система готова и полностью интегрирована!** 🎉 diff --git a/BUG_REPORT_SYSTEM.md b/BUG_REPORT_SYSTEM.md new file mode 100644 index 00000000..6a67c299 --- /dev/null +++ b/BUG_REPORT_SYSTEM.md @@ -0,0 +1,225 @@ +# 🐛 Система отправки багрепортов в техподдержку + +## 📋 Обзор + +Реализована полноценная система для сбора и отправки багрепортов в Telegram с автоматическим сбором диагностической информации. + +## ✨ Что реализовано + +### 1. **Автоматический сбор диагностики** (`BugReportService`) + +Система автоматически собирает: +- ✅ **Информация об устройстве** (анонимно): OS, версия +- ✅ **Статус подключения**: connected/disconnected/connecting +- ✅ **Ошибки подключения**: если есть +- ✅ **Активный прокси**: тип протокола, тег сервера +- ✅ **Пинг/задержка**: в миллисекундах + оценка качества +- ✅ **Логи**: последние 100 строк из `core.log` и `app.log` + +### 2. **UI для отправки багрепортов** (`BugReportDialog`) + +Диалоговое окно с: +- Поле для описания проблемы пользователем +- Чекбокс "Включить логи" (по умолчанию включён) +- Информация о конфиденциальности +- Кнопка "Отправить" с индикатором загрузки + +### 3. **Интеграция с Telegram** + +Использует существующий `TelegramLogger`: +- Отправка как текст (если помещается в 4KB) +- Или как файл (если логи большие) +- Красиво форматированный отчёт с эмодзи + +## 📍 Где находится + +### Код: +``` +lib/features/bug_report/ +├── data/ +│ └── bug_report_service.dart # Сервис сбора и отправки +└── widget/ + └── bug_report_dialog.dart # UI диалога +``` + +### Кнопки в UI: +1. **Настройки → Логи и отладка → "Сообщить о проблеме"** + - Всегда доступна + - Основной способ отправки багрепорта + +## 🔧 Как использовать + +### Для пользователя: + +1. Откройте **Настройки** +2. Раздел **"Логи и отладка"** +3. Нажмите **"Сообщить о проблеме"** 🐛 +4. Опишите проблему +5. Нажмите **"Отправить"** + +### Для разработчика: + +```dart +// Показать диалог программно +await BugReportDialog.show(context); + +// Или получить сервис напрямую +final service = ref.read(bugReportServiceProvider); +final result = await service.sendBugReport( + userDescription: 'Описание проблемы', + includeLogs: true, +); +``` + +## 📊 Формат отчёта + +Отчёт выглядит так: + +``` +🐛 БАГРЕПОРТ UMBRIX +═══════════════════════════════ + +📝 ОПИСАНИЕ ПРОБЛЕМЫ: +Не могу подключиться к серверу, таймаут + +═══════════════════════════════ +💻 УСТРОЙСТВО: +Android 12 + +🔌 СТАТУС ПОДКЛЮЧЕНИЯ: +DISCONNECTED +Ошибка: Connection timeout + +🌐 ПРОКСИ: +Тип: vmess, Тег: server-1, Пинг: 450ms +Задержка: 450ms (🟠 Медленно) + +🕐 ВРЕМЯ: +2026-01-22T15:30:00.000Z + +═══════════════════════════════ +📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК): + +Core: +[INFO] Starting connection... +[ERROR] Connection failed: timeout +... + +App: +[DEBUG] User clicked connect +[ERROR] Failed to establish connection +... +``` + +## 🔐 Безопасность и конфиденциальность + +✅ **Мы НЕ собираем:** +- Личные данные +- IP адреса +- Конфигурацию серверов (URL, пароли) +- Историю посещений + +✅ **Мы собираем ТОЛЬКО:** +- Тип ОС (Android/iOS/Windows...) +- Статус подключения +- Задержку пинга +- Технические логи (без личных данных) + +## 🎯 Как это предотвращает негативные отзывы в Play Store + +### Проблема: +Пользователи оставляют плохие отзывы когда: +- Не могут подключиться +- Приложение "не работает" +- Не понимают, что делать + +### Решение: +1. **Перехватываем недовольство** — кнопка "Сообщить о проблеме" даёт альтернативу отзыву +2. **Собираем контекст** — автоматически видим ЧТО именно не работает +3. **Быстро реагируем** — получаем отчёт в Telegram → можем помочь +4. **Показываем заботу** — пользователь видит, что есть поддержка + +## 📱 Дополнительные места размещения (опционально) + +Можно добавить кнопку в: + +### 1. Экран ошибки подключения +```dart +// В connection_failure_screen.dart +FilledButton.icon( + onPressed: () => BugReportDialog.show(context), + icon: const Icon(FluentIcons.bug_20_regular), + label: const Text('Сообщить о проблеме'), +) +``` + +### 2. Меню приложения (три точки) +```dart +// В app_bar_actions.dart +PopupMenuItem( + child: const Text('Сообщить о проблеме'), + onTap: () => BugReportDialog.show(context), +) +``` + +### 3. Диалог обновления (при ошибке) +```dart +// Если обновление провалилось +if (updateFailed) { + TextButton( + child: const Text('Сообщить о проблеме'), + onPressed: () => BugReportDialog.show(context), + ) +} +``` + +## 🚀 Настройка Telegram + +1. Создайте бота через @BotFather +2. Получите токен +3. Создайте приватный канал/группу +4. Получите Chat ID +5. Настройте в `lib/core/telegram_config.dart`: + +```dart +class TelegramConfig { + static const String botToken = 'YOUR_BOT_TOKEN'; + static const String chatId = 'YOUR_CHAT_ID'; +} +``` + +## 🧪 Тестирование + +```dart +// Протестировать сбор диагностики +final service = ref.read(bugReportServiceProvider); +final diagnostics = await service.collectDiagnostics(); +print(diagnostics.connectionStatus); +print(diagnostics.pingDelay); + +// Протестировать отправку +final result = await service.sendBugReport( + userDescription: 'Test report', + includeLogs: true, +); +print(result.isSuccess); +``` + +## 📈 Метрики для отслеживания + +- Количество отправленных багрепортов +- Среднее время ответа поддержки +- Процент решённых проблем +- Соотношение багрепортов к негативным отзывам + +## 🎉 Результат + +Вместо: +> ⭐☆☆☆☆ "Не работает, не подключается" — в Play Store + +Получаем: +> 🐛 Багрепорт в Telegram → быстрая помощь → довольный пользователь → ⭐⭐⭐⭐⭐ + +--- + +**Готово к использованию!** 🚀 diff --git a/TELEGRAM_BOT_SETUP_RU.md b/TELEGRAM_BOT_SETUP_RU.md new file mode 100644 index 00000000..ae766c0e --- /dev/null +++ b/TELEGRAM_BOT_SETUP_RU.md @@ -0,0 +1,254 @@ +# 🤖 Настройка Telegram бота для багрепортов + +## Шаг 1: Создать бота + +1. Откройте Telegram +2. Найдите **@BotFather** +3. Отправьте команду: `/newbot` +4. Введите имя бота: `Umbrix Bug Report Bot` +5. Введите username: `@Dorod_bug_bot` (или любой свободный) +6. **Скопируйте TOKEN** — это строка типа `7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA` + +``` +📋 Пример токена: +7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA +``` + +## Шаг 2: Создать ПРИВАТНЫЙ канал для логов + +### 📱 В мобильном приложении Telegram: + +1. **Откройте Telegram** на телефоне +2. **Нажмите на ☰ меню** (три полоски слева вверху) +3. **Выберите "Новый канал"** + - Если не видите — нажмите на карандаш ✏️ справа внизу, там будет "Новый канал" +4. **Введите название:** `Umbrix Bug Reports` (или любое) +5. **Введите описание** (необязательно) +6. **Нажмите "Создать"** +7. **‼️ ВАЖНО: Выберите тип канала:** + - ❌ НЕ нажимайте "Публичный канал" + - ✅ Просто нажмите "Пропустить" или "Далее" + - Это сделает канал **ПРИВАТНЫМ** по умолчанию +8. **Нажмите "Сохранить"** + +### 💻 В десктопном приложении Telegram: + +1. **Откройте Telegram** на компьютере +2. **Нажмите ☰ меню** (слева вверху) +3. **Выберите "Новый канал"** +4. **Введите название:** `Umbrix Bug Reports` +5. **Нажмите "Далее"** +6. **‼️ ВАЖНО:** На вопросе "Публичный или приватный?" + - ✅ Выберите **"Приватный канал"** + - ❌ НЕ выбирайте "Публичный" +7. **Нажмите "Сохранить"** + +### 🤖 Добавить бота как администратора: + +1. **Откройте ваш новый канал** `Umbrix Bug Reports` +2. **Нажмите на название канала** вверху +3. **Выберите "Администраторы"** (или ⚙️ → "Управление каналом" → "Администраторы") +4. **Нажмите "Добавить администратора"** +5. **В поиске введите:** `@Dorod_bug_bot` (имя вашего бота) +6. **Выберите бота** из списка +7. **Дайте права:** + - ✅ **"Публикация сообщений"** — ОБЯЗАТЕЛЬНО включите! + - Остальное можно оставить выключенным +8. **Нажмите "Сохранить"** или "Готово" + +✅ **Готово!** Теперь ваш бот может отправлять сообщения в канал. + +## Шаг 3: Получить Chat ID + +### Способ 1 (Простой): + +1. Отправьте любое сообщение в канал +2. Перешлите это сообщение боту **@userinfobot** +3. Он покажет Chat ID канала + +### Способ 2 (Через API): + +1. Отправьте любое сообщение в канал +2. Откройте в браузере: + ``` + https://api.telegram.org/bot/getUpdates + ``` + Замените `` на токен из Шага 1 + +3. Найдите в ответе `"chat":{"id":-1001234567890}` +4. Это ваш Chat ID (обычно начинается с `-100`) + +```json +📋 Пример Chat ID для канала: +-1001234567890 +``` + +## Шаг 4: Настроить в приложении + +Откройте файл и замените значения: + +```dart +// lib/core/telegram_config.dart + +class TelegramConfig { + /// Токен из @BotFather + static const String botToken = '6789012345:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw'; + + /// Chat ID вашего приватного канала + static const String chatId = '-1001234567890'; + + static bool get isConfigured { + return botToken != 'YOUR_BOT_TOKEN_HERE' && chatId != 'YOUR_CHAT_ID_HERE'; + } +} +``` + +## ⚠️ ВАЖНО для безопасности! + +### ✅ ПРАВИЛЬНО: +- Используйте **ПРИВАТНЫЙ** канал (никто не видит кроме вас) +- Добавьте `.gitignore` чтобы не залить токен в git: + ``` + lib/core/telegram_config.dart + ``` + +### ❌ НЕПРАВИЛЬНО: +- Публичный канал (все смогут читать баги) +- Коммитить токен в git (украдут бота) + +## 🔐 Про анонимность + +### Что мы НЕ собираем автоматически: + +❌ Имя пользователя +❌ Email +❌ Телефон +❌ IP адрес +❌ Конфигурацию серверов (URL, пароли) +❌ Историю посещений + +### Что собираем: + +✅ Тип ОС (Android/iOS/Windows) — **анонимно** +✅ Версия ОС — **анонимно** +✅ Статус подключения +✅ Задержка пинга (число) +✅ Технические логи (без IP, без паролей) + +### Код для проверки: + +Посмотрите в [bug_report_service.dart](lib/features/bug_report/data/bug_report_service.dart#L120): + +```dart +/// Получить информацию об устройстве для логов (анонимно) +static String getAnonymousDeviceInfo() { + // Только общая информация без идентификаторов + if (Platform.isAndroid) { + return 'Android ${Platform.operatingSystemVersion}'; + } else if (Platform.isIOS) { + return 'iOS ${Platform.operatingSystemVersion}'; + } + // ... и т.д. + // НЕТ никаких device ID, IMEI, или других идентификаторов! +} +``` + +## 📱 Как сделать ЕЩЁ более анонимным? + +Если хотите убрать даже версию ОС: + +```dart +static String getAnonymousDeviceInfo() { + if (Platform.isAndroid) return 'Android'; + if (Platform.isIOS) return 'iOS'; + if (Platform.isWindows) return 'Windows'; + if (Platform.isMacOS) return 'macOS'; + if (Platform.isLinux) return 'Linux'; + return 'Unknown'; +} +``` + +Тогда будет только `"Android"` без версии. + +## 🧪 Тестирование + +После настройки проверьте: + +```dart +import 'package:umbrix/core/telegram_config.dart'; + +void main() { + print('Configured: ${TelegramConfig.isConfigured}'); + print('Bot: ${TelegramConfig.botToken}'); + print('Chat: ${TelegramConfig.chatId}'); +} +``` + +Должно вывести: +``` +Configured: true +Bot: 6789012345:AAHdqTcvCH... +Chat: -1001234567890 +``` + +## 📨 Пример отчёта в Telegram + +Вот что придёт в ваш канал: + +``` +📱 Umbrix Logs +Device: Android 12 +📅 2026-01-22T15:30:00.000Z +━━━━━━━━━━━━━━━━ +🐛 БАГРЕПОРТ UMBRIX +═══════════════════════════════ + +📝 ОПИСАНИЕ ПРОБЛЕМЫ: +Не могу подключиться, постоянно таймаут + +💻 УСТРОЙСТВО: +Android 12 + +🔌 СТАТУС ПОДКЛЮЧЕНИЯ: +DISCONNECTED +Ошибка: Connection timeout + +🌐 ПРОКСИ: +Тип: vmess, Тег: server-1, Пинг: 450ms +Задержка: 450ms (🟠 Медленно) + +📋 ЛОГИ: +[ERROR] Failed to connect... +[WARN] Timeout exceeded... +``` + +## ❓ FAQ + +**Q: Обязательно ли канал? Может группу?** +A: Лучше канал — он проще и не спамит уведомлениями других участников. + +**Q: Можно несколько каналов для разных устройств?** +A: Да, создайте несколько конфигов или используйте разные билды. + +**Q: Что если не хочу использовать Telegram?** +A: Можно заменить `TelegramLogger` на отправку email, webhook, или другой сервис. + +**Q: Бот будет постить от имени пользователя?** +A: Нет, бот постит от своего имени. Пользователи останутся анонимными. + +**Q: Можно добавить больше информации?** +A: Да, смотрите [BUG_REPORT_INTEGRATION.md](BUG_REPORT_INTEGRATION.md) + +## ✅ Готово! + +Теперь запустите приложение: +1. Зайдите в Настройки → Логи +2. Нажмите "Сообщить о проблеме" +3. Напишите тестовый отчёт +4. Отправьте + +Отчёт должен прийти в ваш канал! 🎉 + +--- + +**Безопасно. Анонимно. Работает.** 🔐 diff --git a/build_with_sentry.sh b/build_with_sentry.sh new file mode 100755 index 00000000..6448261a --- /dev/null +++ b/build_with_sentry.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Скрипт сборки Umbrix с поддержкой Sentry crash reports + +set -e + +# Загружаем переменные из .env.local (если существует) +if [ -f .env.local ]; then + echo "📝 Загружаем конфигурацию из .env.local..." + export $(grep -v '^#' .env.local | xargs) +else + echo "⚠️ Файл .env.local не найден. Sentry будет отключён." + echo " Создай .env.local из .env.example и добавь SENTRY_DSN" +fi + +# Проверяем наличие DSN +if [ -z "$SENTRY_DSN" ]; then + echo "⚠️ SENTRY_DSN не задан → crash reports будут отключены" + echo " Для включения: добавь SENTRY_DSN в .env.local" +else + echo "✅ Sentry включён (DSN найден)" +fi + +# Выбор платформы +PLATFORM=${1:-android} + +case $PLATFORM in + android) + echo "🤖 Сборка Android APK..." + flutter build apk --release --dart-define sentry_dsn="$SENTRY_DSN" + echo "✅ APK: build/app/outputs/flutter-apk/app-release.apk" + ;; + + linux) + echo "🐧 Сборка Linux..." + flutter build linux --release --dart-define sentry_dsn="$SENTRY_DSN" + echo "✅ Linux: build/linux/x64/release/bundle/" + ;; + + windows) + echo "🪟 Сборка Windows..." + flutter build windows --release --dart-define sentry_dsn="$SENTRY_DSN" + echo "✅ Windows: build/windows/x64/runner/Release/" + ;; + + *) + echo "❌ Неизвестная платформа: $PLATFORM" + echo "Использование: ./build_with_sentry.sh [android|linux|windows]" + exit 1 + ;; +esac + +echo "" +echo "🎉 Сборка завершена!" diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 0173a7d4..cd2517d0 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -163,15 +163,6 @@ Future _performBootstrap( ); } - // Автопроверка обновлений для Android - if (Platform.isAndroid) { - _safeInit( - "auto check updates", - () => container.read(appUpdateNotifierProvider.notifier).checkSilently(), - timeout: 5000, - ); - } - if (Platform.isAndroid) { await _safeInit( "android display mode", diff --git a/lib/core/telegram_config.dart b/lib/core/telegram_config.dart index f0fcc794..f809c74b 100644 --- a/lib/core/telegram_config.dart +++ b/lib/core/telegram_config.dart @@ -7,11 +7,11 @@ library; class TelegramConfig { /// Токен вашего Telegram бота от @BotFather /// Пример: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz123456789' - static const String botToken = 'YOUR_BOT_TOKEN_HERE'; + static const String botToken = '7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA'; /// Chat ID группы/канала куда отправлять логи /// Пример: '-1001234567890' - static const String chatId = 'YOUR_CHAT_ID_HERE'; + static const String chatId = '-1003546852118'; /// Проверка что конфиг настроен static bool get isConfigured { diff --git a/lib/features/app/widget/app.dart b/lib/features/app/widget/app.dart index 598ab44f..cd3a286e 100644 --- a/lib/features/app/widget/app.dart +++ b/lib/features/app/widget/app.dart @@ -9,10 +9,7 @@ import 'package:umbrix/core/model/constants.dart'; import 'package:umbrix/core/router/router.dart'; import 'package:umbrix/core/theme/app_theme.dart'; import 'package:umbrix/core/theme/theme_preferences.dart'; -import 'package:umbrix/core/notification/in_app_notification_controller.dart'; import 'package:umbrix/features/app_update/notifier/app_update_notifier.dart'; -import 'package:umbrix/features/app_update/notifier/app_update_state.dart'; -import 'package:umbrix/features/app_update/widget/new_version_dialog.dart'; import 'package:umbrix/features/connection/widget/connection_wrapper.dart'; import 'package:umbrix/features/profile/notifier/profiles_update_notifier.dart'; import 'package:umbrix/features/shortcut/shortcut_wrapper.dart'; @@ -37,31 +34,6 @@ class App extends HookConsumerWidget with PresLogger { ref.listen(foregroundProfilesUpdateNotifierProvider, (_, __) {}); - // Слушаем состояние обновлений и показываем диалог - ref.listen(appUpdateNotifierProvider, (previous, next) { - if (next is AppUpdateStateAvailable) { - // Получаем BuildContext через router - final context = router.routerDelegate.navigatorKey.currentContext; - if (context != null && context.mounted) { - // Показываем диалог обновления - Future.delayed(const Duration(milliseconds: 500), () { - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => NewVersionDialog( - newVersion: next.versionInfo, - onIgnore: () { - ref.read(appUpdateNotifierProvider.notifier).ignoreRelease(next.versionInfo); - }, - ), - ); - } - }); - } - } - }); - return WindowWrapper( TrayWrapper( ShortcutWrapper( diff --git a/lib/features/app_update/widget/new_version_dialog.dart b/lib/features/app_update/widget/new_version_dialog.dart index 9429853c..a854af82 100644 --- a/lib/features/app_update/widget/new_version_dialog.dart +++ b/lib/features/app_update/widget/new_version_dialog.dart @@ -79,7 +79,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { fileExt = '.zip'; final zipUrl = newVersion.findAssetByExtension('.zip'); if (zipUrl == null) { - fileExt = '.exe'; // Fallback на .exe если нет .zip + fileExt = '.exe'; // Fallback на .exe если нет .zip } } else if (Platform.isMacOS) fileExt = '.dmg'; @@ -135,13 +135,23 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { if (context.mounted) { CustomToast('Установка обновления...', type: AlertType.info).show(context); } - + // Запускаем установщик в тихом режиме с правами администратора // /VERYSILENT - без UI, /SUPPRESSMSGBOXES - без диалогов // /NORESTART - не перезагружать систему final result = await Process.run( 'powershell', - ['-Command', 'Start-Process', '-FilePath', '"$savePath"', '-ArgumentList', '"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"', '-Verb', 'RunAs', '-Wait'], + [ + '-Command', + 'Start-Process', + '-FilePath', + '"$savePath"', + '-ArgumentList', + '"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"', + '-Verb', + 'RunAs', + '-Wait' + ], ); if (result.exitCode == 0) { @@ -172,25 +182,33 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { // Получить путь к исполняемому файлу приложения final exePath = Platform.resolvedExecutable; final appDir = Directory(exePath).parent.path; - + // Распаковать во временную папку final tempDir = Directory('${Directory.systemTemp.path}\\umbrix_update_${DateTime.now().millisecondsSinceEpoch}'); await tempDir.create(recursive: true); - + loggy.info('Extracting ZIP to: ${tempDir.path}'); - + // Распаковка через PowerShell final extractResult = await Process.run( 'powershell', - ['-Command', 'Expand-Archive', '-Path', '"$savePath"', '-DestinationPath', '"${tempDir.path}"', '-Force'], + [ + '-Command', + 'Expand-Archive', + '-Path', + '"$savePath"', + '-DestinationPath', + '"${tempDir.path}"', + '-Force' + ], ); - + if (extractResult.exitCode != 0) { throw Exception('Failed to extract ZIP: ${extractResult.stderr}'); } - + loggy.info('ZIP extracted successfully'); - + // Скрипт для замены файлов после закрытия приложения final updateScript = ''' @echo off @@ -209,23 +227,23 @@ start "" "$exePath" echo Update complete! del "%~f0" '''; - + final scriptPath = '${Directory.systemTemp.path}\\umbrix_update.bat'; await File(scriptPath).writeAsString(updateScript); - + if (context.mounted) { CustomToast.success('Обновление установлено! Приложение перезагрузится...').show(context); context.pop(); } - + // Запустить скрипт и закрыть приложение await Process.start('cmd', ['/c', scriptPath], mode: ProcessStartMode.detached); - + // Задержка перед выходом Future.delayed(const Duration(seconds: 1), () { exit(0); }); - + return; } catch (e) { loggy.warning('Failed to install from ZIP: $e'); diff --git a/lib/features/bug_report/data/bug_report_service.dart b/lib/features/bug_report/data/bug_report_service.dart new file mode 100644 index 00000000..f9ed523e --- /dev/null +++ b/lib/features/bug_report/data/bug_report_service.dart @@ -0,0 +1,285 @@ +import 'dart:io'; +import 'package:umbrix/core/telegram/telegram_logger.dart'; +import 'package:umbrix/features/log/data/log_path_resolver.dart'; +import 'package:umbrix/features/log/data/log_data_providers.dart'; +import 'package:umbrix/features/connection/notifier/connection_notifier.dart'; +import 'package:umbrix/features/connection/model/connection_status.dart'; +import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart'; +import 'package:umbrix/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Сервис для отправки багрепортов в техподдержку +class BugReportService with InfraLogger { + BugReportService({ + required this.telegramLogger, + required this.logPathResolver, + required this.ref, + }); + + final TelegramLogger telegramLogger; + final LogPathResolver logPathResolver; + final Ref ref; + + /// Собрать диагностическую информацию + Future collectDiagnostics() async { + try { + // 1. Информация о системе (анонимно) + final deviceInfo = TelegramLogger.getAnonymousDeviceInfo(); + + // 2. Статус подключения + String connectionStatus = 'Неизвестно'; + String? connectionError; + try { + final connection = await ref.read(connectionNotifierProvider.future); + connectionStatus = connection.format(); + if (connection is Disconnected && connection.connectionFailure != null) { + connectionError = connection.connectionFailure.toString(); + } + } catch (e) { + connectionStatus = 'Ошибка получения статуса'; + connectionError = e.toString(); + } + + // 3. Активный прокси и его задержка + String? activeProxyInfo; + int? pingDelay; + try { + final activeProxy = await ref.read(activeProxyNotifierProvider.future); + if (activeProxy != null) { + pingDelay = activeProxy.urlTestDelay; + activeProxyInfo = 'Тип: ${activeProxy.type}, Тег: ${activeProxy.tag}, Пинг: ${pingDelay}ms'; + } else { + activeProxyInfo = 'Нет активного прокси'; + } + } catch (e) { + activeProxyInfo = 'Ошибка получения прокси: $e'; + } + + // 4. Последние логи (последние 100 строк из каждого файла) + final coreLogs = await _readLastLines(logPathResolver.coreFile(), 100); + final appLogs = await _readLastLines(logPathResolver.appFile(), 100); + + return DiagnosticInfo( + deviceInfo: deviceInfo, + connectionStatus: connectionStatus, + connectionError: connectionError, + activeProxyInfo: activeProxyInfo, + pingDelay: pingDelay, + coreLogsPreview: coreLogs, + appLogsPreview: appLogs, + timestamp: DateTime.now(), + ); + } catch (e, st) { + loggy.error('Ошибка сбора диагностики', e, st); + rethrow; + } + } + + /// Отправить багрепорт в Telegram + Future sendBugReport({ + required String userDescription, + bool includeLogs = true, + }) async { + try { + final diagnostics = await collectDiagnostics(); + + // Формируем текст репорта + final reportText = _formatBugReport(userDescription, diagnostics); + + // Отправляем как текст (если влезает в лимит) + bool success = false; + if (reportText.length < 3500) { + success = await telegramLogger.sendLogsAsText( + reportText, + deviceInfo: diagnostics.deviceInfo, + ); + } + + // Если текст слишком большой или отправка не удалась - отправляем файлы + if (!success && includeLogs) { + success = await _sendLogsAsFiles(diagnostics, userDescription); + } + + if (success) { + return BugReportResult.success( + message: 'Отчёт отправлен в техподдержку', + ); + } else { + return BugReportResult.failure( + error: 'Не удалось отправить отчёт. Telegram бот не настроен?', + ); + } + } catch (e, st) { + loggy.error('Ошибка отправки багрепорта', e, st); + return BugReportResult.failure( + error: 'Ошибка: ${e.toString()}', + ); + } + } + + /// Отправить логи как файлы + Future _sendLogsAsFiles(DiagnosticInfo diagnostics, String userDescription) async { + try { + // Создаём временный файл с полным отчётом + final tempDir = Directory.systemTemp; + final reportFile = File('${tempDir.path}/umbrix_bug_report_${DateTime.now().millisecondsSinceEpoch}.txt'); + + final fullReport = _formatBugReport(userDescription, diagnostics, includeFullLogs: true); + await reportFile.writeAsString(fullReport); + + final success = await telegramLogger.sendLogsAsFile( + reportFile, + deviceInfo: diagnostics.deviceInfo, + ); + + // Удаляем временный файл + try { + await reportFile.delete(); + } catch (_) {} + + return success; + } catch (e, st) { + loggy.error('Ошибка отправки файлов логов', e, st); + return false; + } + } + + /// Прочитать последние N строк из файла + Future _readLastLines(File file, int maxLines) async { + try { + if (!await file.exists()) { + return '(Файл не существует)'; + } + + final lines = await file.readAsLines(); + final lastLines = lines.length > maxLines ? lines.sublist(lines.length - maxLines) : lines; + + return lastLines.join('\n'); + } catch (e) { + return '(Ошибка чтения: $e)'; + } + } + + /// Форматировать багрепорт для отправки + String _formatBugReport( + String userDescription, + DiagnosticInfo diagnostics, { + bool includeFullLogs = false, + }) { + final buffer = StringBuffer(); + + buffer.writeln('🐛 БАГРЕПОРТ UMBRIX'); + buffer.writeln('═══════════════════════════════'); + buffer.writeln(); + + // Описание пользователя + buffer.writeln('📝 ОПИСАНИЕ ПРОБЛЕМЫ:'); + buffer.writeln(userDescription.trim()); + buffer.writeln(); + buffer.writeln('═══════════════════════════════'); + + // Система + buffer.writeln('💻 УСТРОЙСТВО:'); + buffer.writeln(diagnostics.deviceInfo); + buffer.writeln(); + + // Подключение + buffer.writeln('🔌 СТАТУС ПОДКЛЮЧЕНИЯ:'); + buffer.writeln(diagnostics.connectionStatus); + if (diagnostics.connectionError != null) { + buffer.writeln('Ошибка: ${diagnostics.connectionError}'); + } + buffer.writeln(); + + // Прокси + buffer.writeln('🌐 ПРОКСИ:'); + buffer.writeln(diagnostics.activeProxyInfo ?? 'Не определён'); + if (diagnostics.pingDelay != null) { + final delayMs = diagnostics.pingDelay!; + final quality = delayMs < 100 + ? '✅ Отлично' + : delayMs < 300 + ? '🟡 Нормально' + : delayMs < 1000 + ? '🟠 Медленно' + : '🔴 Очень медленно'; + buffer.writeln('Задержка: ${delayMs}ms ($quality)'); + } + buffer.writeln(); + + // Время создания + buffer.writeln('🕐 ВРЕМЯ:'); + buffer.writeln(diagnostics.timestamp.toIso8601String()); + buffer.writeln(); + + // Логи (preview или full) + if (includeFullLogs) { + buffer.writeln('═══════════════════════════════'); + buffer.writeln('📋 CORE LOGS (ПОЛНЫЕ):'); + buffer.writeln(diagnostics.coreLogsPreview); + buffer.writeln(); + buffer.writeln('📋 APP LOGS (ПОЛНЫЕ):'); + buffer.writeln(diagnostics.appLogsPreview); + } else { + buffer.writeln('═══════════════════════════════'); + buffer.writeln('📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК):'); + buffer.writeln(); + buffer.writeln('Core:'); + final coreLines = diagnostics.coreLogsPreview.split('\n'); + buffer.writeln(coreLines.length > 20 ? coreLines.sublist(coreLines.length - 20).join('\n') : diagnostics.coreLogsPreview); + buffer.writeln(); + buffer.writeln('App:'); + final appLines = diagnostics.appLogsPreview.split('\n'); + buffer.writeln(appLines.length > 20 ? appLines.sublist(appLines.length - 20).join('\n') : diagnostics.appLogsPreview); + } + + return buffer.toString(); + } +} + +/// Диагностическая информация для багрепорта +class DiagnosticInfo { + final String deviceInfo; + final String connectionStatus; + final String? connectionError; + final String? activeProxyInfo; + final int? pingDelay; + final String coreLogsPreview; + final String appLogsPreview; + final DateTime timestamp; + + DiagnosticInfo({ + required this.deviceInfo, + required this.connectionStatus, + this.connectionError, + this.activeProxyInfo, + this.pingDelay, + required this.coreLogsPreview, + required this.appLogsPreview, + required this.timestamp, + }); +} + +/// Результат отправки багрепорта +class BugReportResult { + final bool isSuccess; + final String message; + final String? error; + + BugReportResult.success({required this.message}) + : isSuccess = true, + error = null; + + BugReportResult.failure({required this.error}) + : isSuccess = false, + message = ''; +} + +/// Provider для сервиса багрепортов +final bugReportServiceProvider = Provider((ref) { + return BugReportService( + telegramLogger: TelegramLogger(), + logPathResolver: ref.watch(logPathResolverProvider), + ref: ref, + ); +}); diff --git a/lib/features/bug_report/widget/bug_report_dialog.dart b/lib/features/bug_report/widget/bug_report_dialog.dart new file mode 100644 index 00000000..b48b55cb --- /dev/null +++ b/lib/features/bug_report/widget/bug_report_dialog.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:umbrix/features/bug_report/data/bug_report_service.dart'; +import 'package:umbrix/core/localization/translations.dart'; +import 'package:umbrix/utils/utils.dart'; + +/// Диалог для отправки багрепорта в техподдержку +class BugReportDialog extends HookConsumerWidget { + const BugReportDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final theme = Theme.of(context); + + final descriptionController = useTextEditingController(); + final isSending = useState(false); + final includeLogs = useState(true); + + Future sendReport() async { + if (descriptionController.text.trim().isEmpty) { + CustomToast.error('Пожалуйста, опишите проблему').show(context); + return; + } + + isSending.value = true; + + try { + final service = ref.read(bugReportServiceProvider); + final result = await service.sendBugReport( + userDescription: descriptionController.text, + includeLogs: includeLogs.value, + ); + + if (context.mounted) { + if (result.isSuccess) { + CustomToast.success(result.message).show(context); + Navigator.of(context).pop(); + } else { + CustomToast.error(result.error ?? 'Неизвестная ошибка').show(context); + } + } + } catch (e) { + if (context.mounted) { + CustomToast.error('Ошибка: $e').show(context); + } + } finally { + isSending.value = false; + } + } + + return AlertDialog( + title: Row( + children: [ + Icon( + FluentIcons.bug_20_regular, + color: theme.colorScheme.error, + ), + const Gap(12), + const Text('Сообщить о проблеме'), + ], + ), + content: SizedBox( + width: 500, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Опишите проблему, с которой вы столкнулись. Мы автоматически соберём диагностическую информацию (логи, статус подключения, задержку).', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const Gap(16), + TextField( + controller: descriptionController, + maxLines: 6, + enabled: !isSending.value, + decoration: InputDecoration( + labelText: 'Описание проблемы *', + hintText: 'Например: "Не могу подключиться к серверу, ошибка таймаута"', + border: const OutlineInputBorder(), + helperText: 'Чем подробнее, тем лучше', + ), + ), + const Gap(16), + CheckboxListTile( + title: const Text('Включить логи'), + subtitle: Text( + 'Отправить файлы логов для диагностики', + style: theme.textTheme.bodySmall, + ), + value: includeLogs.value, + onChanged: isSending.value + ? null + : (value) { + includeLogs.value = value ?? true; + }, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + ), + const Gap(8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + FluentIcons.shield_checkmark_20_regular, + size: 20, + color: theme.colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + 'Мы не собираем личные данные. Только техническая информация.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: isSending.value ? null : () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + FilledButton.icon( + onPressed: isSending.value ? null : sendReport, + icon: isSending.value + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(FluentIcons.send_20_regular), + label: Text(isSending.value ? 'Отправка...' : 'Отправить'), + ), + ], + ); + } + + /// Показать диалог багрепорта + static Future show(BuildContext context) { + return showDialog( + context: context, + builder: (context) => const BugReportDialog(), + ); + } +} diff --git a/lib/features/settings/widgets/logs_setting_tiles.dart b/lib/features/settings/widgets/logs_setting_tiles.dart index c36f7d19..718e9607 100644 --- a/lib/features/settings/widgets/logs_setting_tiles.dart +++ b/lib/features/settings/widgets/logs_setting_tiles.dart @@ -2,6 +2,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:umbrix/core/localization/translations.dart'; import 'package:umbrix/core/router/router.dart'; +import 'package:umbrix/features/bug_report/widget/bug_report_dialog.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; /// Секция "Логи и отладка" в настройках @@ -14,6 +15,20 @@ class LogsSettingTiles extends HookConsumerWidget { return Column( children: [ + // Кнопка "Сообщить о проблеме" + ListTile( + title: const Text('Сообщить о проблеме'), + subtitle: const Text('Отправить багрепорт в техподдержку с автоматическими логами'), + leading: Icon( + FluentIcons.bug_20_regular, + color: Theme.of(context).colorScheme.error, + ), + trailing: const Icon(FluentIcons.send_20_regular), + onTap: () { + BugReportDialog.show(context); + }, + ), + const Divider(), // Переход на страницу логов ListTile( title: Text(t.logs.pageTitle), diff --git a/pubspec.yaml b/pubspec.yaml index 74117dca..d4606069 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: umbrix description: Cross Platform Multi Protocol Proxy Frontend. publish_to: "none" -version: 1.7.6+176 +version: 1.7.8+178 environment: sdk: ">=3.3.0 <4.0.0"