Files
umbrix/lib/features/log/overview/logs_overview_page.dart
Umbrix Developer 76a374950f feat: mobile-like window size and always-visible stats
- Changed window size to mobile phone format (400x800)
- Removed width condition for ActiveProxyFooter - now always visible
- Added run-umbrix.sh launch script with icon copying
- Stats cards now display on all screen sizes
2026-01-17 13:09:20 +03:00

431 lines
20 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fpdart/fpdart.dart';
import 'package:gap/gap.dart';
import 'package:umbrix/core/localization/translations.dart';
import 'package:umbrix/core/model/failures.dart';
import 'package:umbrix/core/preferences/general_preferences.dart';
import 'package:umbrix/core/telegram/telegram_logger.dart';
import 'package:umbrix/core/telegram_config.dart';
import 'package:umbrix/core/widget/adaptive_icon.dart';
import 'package:umbrix/features/common/nested_app_bar.dart';
import 'package:umbrix/features/config_option/data/config_option_repository.dart';
import 'package:umbrix/features/log/data/log_data_providers.dart';
import 'package:umbrix/features/log/model/log_level.dart';
import 'package:umbrix/features/log/overview/logs_overview_notifier.dart';
import 'package:umbrix/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
class LogsOverviewPage extends HookConsumerWidget with PresLogger {
const LogsOverviewPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final state = ref.watch(logsOverviewNotifierProvider);
final notifier = ref.watch(logsOverviewNotifierProvider.notifier);
final debug = ref.watch(debugModeNotifierProvider);
final pathResolver = ref.watch(logPathResolverProvider);
final filterController = useTextEditingController(text: state.filter);
// UMBRIX: Кнопки логов доступны всегда (не только в debug режиме)
final List<PopupMenuEntry> popupButtons = [
PopupMenuItem(
child: Text(t.logs.shareCoreLogs),
onTap: () async {
await UriUtils.tryShareOrLaunchFile(
Uri.parse(pathResolver.coreFile().path),
fileOrDir: pathResolver.directory.uri,
);
},
),
PopupMenuItem(
child: Text(t.logs.shareAppLogs),
onTap: () async {
await UriUtils.tryShareOrLaunchFile(
Uri.parse(pathResolver.appFile().path),
fileOrDir: pathResolver.directory.uri,
);
},
),
];
// Добавляем кнопку отправки в Telegram только если настроен
if (TelegramConfig.isConfigured) {
popupButtons.addAll([
const PopupMenuDivider(),
PopupMenuItem(
child: const Row(
children: [
Icon(FluentIcons.send_20_regular, size: 18),
Gap(8),
Text('Отправить разработчику'),
],
),
onTap: () async {
// Показываем диалог с подтверждением
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Отправить логи?'),
content: const Text(
'Логи будут отправлены анонимно разработчику Umbrix через Telegram. '
'Это поможет улучшить приложение.\n\n'
'Отправляется только общая информация об ОС, без личных данных.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Отмена'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Отправить'),
),
],
),
);
if (confirmed == true) {
// Показываем индикатор загрузки
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
try {
final telegramLogger = TelegramLogger();
final deviceInfo = TelegramLogger.getAnonymousDeviceInfo();
// Отправляем файл логов
final success = await telegramLogger.sendLogsAsFile(
pathResolver.appFile(),
deviceInfo: deviceInfo,
);
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор
// Показываем результат
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success ? '✅ Логи успешно отправлены! Спасибо за помощь!' : '❌ Ошибка отправки. Проверьте интернет-соединение.',
),
duration: const Duration(seconds: 3),
),
);
} catch (e) {
if (!context.mounted) return;
Navigator.of(context).pop(); // Закрываем индикатор
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('❌ Ошибка отправки логов'),
duration: Duration(seconds: 3),
),
);
}
}
},
),
]);
}
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: MultiSliver(
children: [
NestedAppBar(
forceElevated: innerBoxIsScrolled,
title: Text(t.logs.pageTitle),
actions: [
if (state.paused)
IconButton(
onPressed: notifier.resume,
icon: const Icon(FluentIcons.play_20_regular),
tooltip: t.logs.resumeTooltip,
iconSize: 20,
)
else
IconButton(
onPressed: notifier.pause,
icon: const Icon(FluentIcons.pause_20_regular),
tooltip: t.logs.pauseTooltip,
iconSize: 20,
),
IconButton(
onPressed: notifier.clear,
icon: const Icon(FluentIcons.delete_lines_20_regular),
tooltip: t.logs.clearTooltip,
iconSize: 20,
),
if (popupButtons.isNotEmpty)
PopupMenuButton(
icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) {
return popupButtons;
},
),
],
),
SliverPinnedHeader(
child: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
// Карточка настроек логирования
Container(
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
FluentIcons.settings_20_regular,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Text(
'Настройки логирования',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const Gap(12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Уровень детализации',
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(4),
Text(
'Влияет на объём записываемой информации',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Gap(2),
Row(
children: [
Icon(
FluentIcons.shield_checkmark_20_regular,
size: 14,
color: Theme.of(context).colorScheme.primary.withOpacity(0.7),
),
const Gap(4),
Text(
'Макс. 1000 записей • 5 МБ на файл',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 11,
color: Theme.of(context).colorScheme.primary.withOpacity(0.7),
),
),
],
),
],
),
),
const Gap(12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<LogLevel>(
value: ref.watch(ConfigOptions.logLevel),
onChanged: (value) {
if (value != null) {
ref.read(ConfigOptions.logLevel.notifier).update(value);
}
},
underline: const SizedBox(),
isDense: true,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
fontSize: 13,
),
dropdownColor: Theme.of(context).colorScheme.primaryContainer,
items: LogLevel.choices.map((level) {
return DropdownMenuItem(
value: level,
child: Text(level.name.toUpperCase()),
);
}).toList(),
),
),
],
),
],
),
),
// Фильтры поиска
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
Flexible(
child: TextFormField(
controller: filterController,
onChanged: notifier.filterMessage,
decoration: InputDecoration(
isDense: true,
hintText: t.logs.filterHint,
prefixIcon: const Icon(FluentIcons.search_20_regular, size: 18),
),
),
),
const Gap(12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<Option<LogLevel>>(
value: optionOf(state.levelFilter),
onChanged: (v) {
if (v == null) return;
notifier.filterLevel(v.toNullable());
},
underline: const SizedBox(),
borderRadius: BorderRadius.circular(8),
isDense: true,
items: [
DropdownMenuItem(
value: none(),
child: Text(t.logs.allLevelsFilter),
),
...LogLevel.choices.map(
(e) => DropdownMenuItem(
value: some(e),
child: Text(e.name),
),
),
],
),
),
],
),
),
],
),
),
),
],
),
),
];
},
body: Builder(
builder: (context) {
return CustomScrollView(
primary: false,
reverse: true,
slivers: <Widget>[
switch (state.logs) {
AsyncData(value: final logs) => SliverList.builder(
itemCount: logs.length,
itemBuilder: (context, index) {
final log = logs[index];
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (log.level != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
log.level!.name.toUpperCase(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: log.level!.color,
),
),
if (log.time != null)
Text(
log.time!.toString(),
style: Theme.of(context).textTheme.labelSmall,
),
],
),
Text(
log.message,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
if (index != 0)
const Divider(
indent: 16,
endIndent: 16,
height: 4,
),
],
);
},
),
AsyncError(:final error) => SliverErrorBodyPlaceholder(
t.presentShortError(error),
),
_ => const SliverLoadingBodyPlaceholder(),
},
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context,
),
),
],
);
},
),
),
);
}
}