небольшие изменения

This commit is contained in:
Jganenokk
2026-07-05 19:07:27 +07:00
parent 26f0094705
commit 9f9b0e0734
214 changed files with 27592 additions and 15353 deletions
+149
View File
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/utils/format.dart';
class DebugCacheSection extends StatelessWidget {
final int cacheSize;
final bool clearingCache;
final String cacheLimitLabel;
final VoidCallback onPickCacheLimit;
final VoidCallback onClearCache;
const DebugCacheSection({
super.key,
required this.cacheSize,
required this.clearingCache,
required this.cacheLimitLabel,
required this.onPickCacheLimit,
required this.onClearCache,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onPickCacheLimit,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.data_usage,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Лимит кэша медиа',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
cacheLimitLabel,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: clearingCache ? null : onClearCache,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.delete_sweep,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Очистить кэш медиа',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
clearingCache
? 'Очистка…'
: 'Занято: ${formatBytes(cacheSize)}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
if (clearingCache)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onSurfaceVariant,
),
),
],
),
),
),
),
),
],
);
}
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../widgets/glossy_pill.dart';
class DebugToggleTile extends StatelessWidget {
final IconData icon;
final String title;
final String Function(bool value)? subtitle;
final ValueListenable<bool> valueListenable;
final ValueChanged<bool> onChanged;
const DebugToggleTile({
super.key,
required this.icon,
required this.title,
this.subtitle,
required this.valueListenable,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return ValueListenableBuilder<bool>(
valueListenable: valueListenable,
builder: (context, value, _) {
final resolvedSubtitle = subtitle?.call(value);
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17),
child: Row(
children: [
Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
if (resolvedSubtitle != null) ...[
const SizedBox(height: 2),
Text(
resolvedSubtitle,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
],
),
),
Switch(value: value, onChanged: onChanged),
],
),
);
},
);
}
}
@@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/config/app_commands.dart';
import '../../core/config/app_digital_id_mode.dart';
import '../../core/config/app_link_preview.dart';
import '../../core/config/app_pranks.dart';
import '../../core/config/app_show_extra_info.dart';
import '../../core/config/app_stories.dart';
import '../../core/config/app_swipe_back_desktop.dart';
import '../screens/digital_id/digital_id_web_screen.dart';
import '../widgets/custom_notification.dart';
import 'debug_toggle_tile.dart';
class DebugFeatureTogglesSection extends StatelessWidget {
const DebugFeatureTogglesSection({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.swipe_right,
title: 'Свайп-назад в десктоп-режиме',
subtitle: (_) =>
'Включает жест «провести от левого края, чтобы '
'закрыть» внутри встроенной панели чата на '
'десктопе — для тестирования курсором',
valueListenable: AppSwipeBackDesktop.current,
onChanged: AppSwipeBackDesktop.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.auto_awesome,
title: 'Приколь4ики',
valueListenable: AppPranks.current,
onChanged: AppPranks.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.badge,
title: 'Нативный Цифровой ID',
subtitle: (native) => native
? 'Нативный экран (REST ext-api.max.ru)'
: 'Оригинальная страница в WebView',
valueListenable: AppDigitalIdNative.current,
onChanged: AppDigitalIdNative.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () async {
await resetDigitalIdWebData();
if (!context.mounted) return;
showCustomNotification(
context,
'Цифровой ID сброшен — Госуслуги спросят вход заново',
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.restart_alt,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Сбросить Цифровой ID',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Очистить куки и данные WebView',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.amp_stories,
title: 'Истории',
subtitle: (_) => 'Отображение ленты историй в списке чатов',
valueListenable: AppStories.current,
onChanged: AppStories.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.terminal,
title: 'Команды',
subtitle: (_) => 'Панель команд по вводу «/» в строке сообщения',
valueListenable: AppCommands.current,
onChanged: AppCommands.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.link,
title: 'Предпросмотр ссылок',
subtitle: (_) => 'Карточки с превью для ссылок в сообщениях',
valueListenable: AppLinkPreview.current,
onChanged: AppLinkPreview.save,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.info,
title: 'Доп. информация',
subtitle: (_) =>
'Раздел «Info» в настройках и вкладка с '
'технической информацией в профиле собеседника',
valueListenable: AppShowExtraInfo.current,
onChanged: AppShowExtraInfo.save,
),
),
],
);
}
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
class DebugHeaderSection extends StatelessWidget {
const DebugHeaderSection({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Row(
children: [
IconButton(
icon: Icon(
Symbols.arrow_back,
color: cs.onSurface,
size: 24,
weight: 400,
),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 4),
Expanded(
child: Text(
'Для разработчиков',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.w700,
fontFamily: 'Outfit',
),
),
),
],
);
}
}
+431
View File
@@ -0,0 +1,431 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../widgets/custom_notification.dart';
import '../widgets/glossy_pill.dart';
class DebugIdSearchSection extends StatelessWidget {
final TextEditingController idController;
final bool isSearching;
final bool hasSearched;
final List<SearchHit> hits;
final Map<String, String> errors;
final VoidCallback onSearch;
const DebugIdSearchSection({
super.key,
required this.idController,
required this.isSearching,
required this.hasSearched,
required this.hits,
required this.errors,
required this.onSearch,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Поиск по ID',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Параллельно: contactInfo (32) + chatInfo (48) + publicSearch (60)',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: idController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Введите ID',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => onSearch(),
),
),
const SizedBox(width: 12),
FilledButton(
onPressed: isSearching ? null : onSearch,
child: isSearching
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Symbols.search, size: 20),
),
],
),
if (hasSearched && !isSearching) ...[
const SizedBox(height: 12),
if (hits.isEmpty && errors.isEmpty)
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'Ничего не найдено',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
),
for (final hit in hits) ...[
_SearchResultCard(hit: hit),
const SizedBox(height: 8),
],
for (final entry in errors.entries) ...[
_ErrorChip(label: entry.key, message: entry.value),
const SizedBox(height: 6),
],
],
],
),
);
}
}
enum HitKind { dialog, chat, channel, bot, official, contact, user, unknown }
class SearchHit {
final String source;
final int id;
final String title;
final String? subtitle;
final String? avatarUrl;
final List<HitKind> badges;
final bool isChatEntity;
SearchHit({
required this.source,
required this.id,
required this.title,
required this.avatarUrl,
required this.badges,
required this.isChatEntity,
this.subtitle,
});
static SearchHit? fromContact(String source, Map raw) {
final id = raw['id'];
if (id is! int) return null;
final namesRaw = raw['names'];
String title = 'User #$id';
if (namesRaw is List && namesRaw.isNotEmpty) {
final n = namesRaw.first;
if (n is Map) {
final full = n['name']?.toString();
if (full != null && full.isNotEmpty) title = full;
}
}
final opts = (raw['options'] is List)
? (raw['options'] as List).whereType<String>().toSet()
: <String>{};
final badges = <HitKind>[];
if (opts.contains('BOT')) badges.add(HitKind.bot);
if (opts.contains('OFFICIAL')) badges.add(HitKind.official);
if (badges.isEmpty) badges.add(HitKind.contact);
return SearchHit(
source: source,
id: id,
title: title,
subtitle: (raw['description'] as String?)?.trim().isNotEmpty == true
? raw['description'] as String
: (raw['phone'] != null ? 'Телефон скрыт' : null),
avatarUrl: raw['baseUrl'] as String?,
badges: badges,
isChatEntity: false,
);
}
static SearchHit? fromChat(String source, Map raw) {
final id = raw['id'];
if (id is! int) return null;
final type = (raw['type'] as String?) ?? 'CHAT';
final title = (raw['title'] as String?) ?? 'Chat #$id';
final pCount = raw['participantsCount'] as int?;
final badges = <HitKind>[];
switch (type) {
case 'DIALOG':
badges.add(HitKind.dialog);
case 'CHANNEL':
badges.add(HitKind.channel);
case 'CHAT':
badges.add(HitKind.chat);
default:
badges.add(HitKind.unknown);
}
final opts = raw['options'];
if (opts is Map && opts['OFFICIAL'] == true) {
badges.add(HitKind.official);
}
String? subtitle;
if (type == 'CHANNEL') {
subtitle = pCount != null ? 'Канал · $pCount подписч.' : 'Канал';
} else if (type == 'CHAT') {
subtitle = pCount != null ? 'Группа · $pCount участн.' : 'Группа';
} else {
subtitle = 'Диалог';
}
return SearchHit(
source: source,
id: id,
title: title,
subtitle: subtitle,
avatarUrl: raw['baseIconUrl'] as String?,
badges: badges,
isChatEntity: true,
);
}
}
class _SearchResultCard extends StatelessWidget {
final SearchHit hit;
const _SearchResultCard({required this.hit});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_HitAvatar(hit: hit, cs: cs),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Flexible(
child: Text(
hit.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
for (final b in hit.badges) ...[
const SizedBox(width: 6),
_BadgeChip(kind: b, cs: cs),
],
],
),
if (hit.subtitle != null) ...[
const SizedBox(height: 2),
Text(
hit.subtitle!,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 2),
Row(
children: [
Text(
'id: ${hit.id}',
style: TextStyle(
color: cs.outline,
fontSize: 11,
fontFamily: 'monospace',
),
),
const SizedBox(width: 8),
Text(
'via ${hit.source}',
style: TextStyle(color: cs.outline, fontSize: 11),
),
],
),
],
),
),
IconButton(
tooltip: 'Скопировать id',
icon: Icon(
Symbols.content_copy,
size: 18,
color: cs.onSurfaceVariant,
),
onPressed: () async {
await Clipboard.setData(ClipboardData(text: hit.id.toString()));
if (context.mounted) {
showCustomNotification(context, 'id скопирован');
}
},
),
],
),
);
}
}
class _HitAvatar extends StatelessWidget {
final SearchHit hit;
final ColorScheme cs;
const _HitAvatar({required this.hit, required this.cs});
@override
Widget build(BuildContext context) {
const size = 44.0;
final url = hit.avatarUrl;
if (url != null && url.isNotEmpty) {
return ClipOval(
child: CachedNetworkImage(
imageUrl: url,
width: size,
height: size,
fit: BoxFit.cover,
placeholder: (_, _) => _fallback(),
errorWidget: (_, _, _) => _fallback(),
),
);
}
return _fallback();
}
Widget _fallback() {
final initial = hit.title.isNotEmpty ? hit.title[0].toUpperCase() : '?';
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: cs.primaryContainer,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
initial,
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _BadgeChip extends StatelessWidget {
final HitKind kind;
final ColorScheme cs;
const _BadgeChip({required this.kind, required this.cs});
@override
Widget build(BuildContext context) {
String label;
Color bg;
Color fg;
switch (kind) {
case HitKind.bot:
label = 'Bot';
bg = cs.tertiaryContainer;
fg = cs.onTertiaryContainer;
case HitKind.official:
label = '';
bg = cs.primary;
fg = cs.onPrimary;
case HitKind.contact:
label = 'Контакт';
bg = cs.surface;
fg = cs.onSurfaceVariant;
case HitKind.user:
label = 'User';
bg = cs.surface;
fg = cs.onSurfaceVariant;
case HitKind.dialog:
label = 'Диалог';
bg = cs.secondaryContainer;
fg = cs.onSecondaryContainer;
case HitKind.chat:
label = 'Группа';
bg = cs.secondaryContainer;
fg = cs.onSecondaryContainer;
case HitKind.channel:
label = 'Канал';
bg = cs.tertiaryContainer;
fg = cs.onTertiaryContainer;
case HitKind.unknown:
label = '?';
bg = cs.surface;
fg = cs.onSurfaceVariant;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: TextStyle(color: fg, fontSize: 10, fontWeight: FontWeight.w600),
),
);
}
}
class _ErrorChip extends StatelessWidget {
final String label;
final String message;
const _ErrorChip({required this.label, required this.message});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: cs.errorContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Symbols.error_outline, size: 16, color: cs.onErrorContainer),
const SizedBox(width: 8),
Expanded(
child: Text(
'$label: $message',
style: TextStyle(color: cs.onErrorContainer, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../main.dart';
import '../screens/profile/traffic_monitor_screen.dart';
import '../widgets/connection_status.dart';
import 'debug_toggle_tile.dart';
class DebugNetworkSection extends StatelessWidget {
final KometAppState? appState;
const DebugNetworkSection({super.key, required this.appState});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final state = appState;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: state == null
? const SizedBox.shrink()
: DebugToggleTile(
icon: Symbols.speed,
title: 'Оверлей FPS',
subtitle: (_) =>
'Показ текущего фреймрейта поверх интерфейса',
valueListenable: state.fpsOverlayEnabled,
onChanged: state.setFpsOverlayEnabled,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: state == null
? const SizedBox.shrink()
: DebugToggleTile(
icon: Symbols.vpn_key_off,
title: 'Обход VPN',
subtitle: (_) =>
'Если обнаружен VPN (tun-интерфейс), '
'подключаться напрямую через Wi-Fi или '
'моб. сеть в обход туннеля. Только Android',
valueListenable: state.vpnBypassEnabled,
onChanged: state.setVpnBypassEnabled,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: DebugToggleTile(
icon: Symbols.wifi_off,
title: 'Офлайн (тест)',
subtitle: (_) =>
'Показать индикаторы соединения во всех '
'экранах, не разрывая реальную сессию',
valueListenable: debugForceOffline,
onChanged: (v) => debugForceOffline.value = v,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: state == null
? const SizedBox.shrink()
: DebugToggleTile(
icon: Symbols.gpp_bad,
title: 'Отключить проверку TLS',
subtitle: (_) =>
'Принимать любой сертификат сервера. '
'Только для отладки через MitM-прокси — '
'соединение становится уязвимым к '
'перехвату трафика',
valueListenable: state.tlsInsecureEnabled,
onChanged: state.setTlsInsecureEnabled,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const TrafficMonitorScreen()),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.lan,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Монитор трафика',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Реалтайм: домены, опкоды и payload внутри '
'сокет-соединения',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
],
);
}
}
+210
View File
@@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/storage/app_database.dart';
import '../screens/calls/call_screen.dart';
import '../widgets/glossy_pill.dart';
import '../widgets/login_success_screen.dart';
class DebugPreviewsSection extends StatelessWidget {
final bool micSignalOn;
final ValueChanged<bool> onMicSignalChanged;
const DebugPreviewsSection({
super.key,
required this.micSignalOn,
required this.onMicSignalChanged,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () async {
final profile = await AppDatabase.loadActiveProfile();
if (!context.mounted) return;
final avatar = await precacheLoginAvatar(
context,
profile?.baseUrl,
);
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
LoginSuccessScreen(preview: true, avatar: avatar),
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.celebration,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'test hello',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Показать приветственную анимацию входа',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Экран звонка',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Превью экранов звонков',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 12),
_DebugCallButton(
label: 'Экран звонка (превью)',
icon: Symbols.phone,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const CallScreen(name: 'Кирил Г.'),
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Сигнал микрофона (тест)',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Шлёт change-media-settings в активный звонок, '
'не меняя реальный микрофон',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Switch(value: micSignalOn, onChanged: onMicSignalChanged),
],
),
],
),
),
),
],
);
}
}
class _DebugCallButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onTap;
const _DebugCallButton({
required this.label,
required this.icon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Material(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: cs.onSurfaceVariant, size: 22, fill: 1),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../screens/auth/login_screen.dart';
class DebugQuickActionsSection extends StatelessWidget {
final VoidCallback onExportLog;
const DebugQuickActionsSection({super.key, required this.onExportLog});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onExportLog,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.bug_report,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Отладочный лог',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Все запросы за последние 3 захода в приложение',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.save_alt,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.dialpad,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Экран ввода номера',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Открыть без выхода из аккаунта и обрыва сессии',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
],
);
}
}
+180
View File
@@ -0,0 +1,180 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../main.dart';
import '../widgets/glossy_pill.dart';
class DebugSyncProbeSection extends StatefulWidget {
const DebugSyncProbeSection({super.key});
@override
State<DebugSyncProbeSection> createState() => _DebugSyncProbeSectionState();
}
class _DebugSyncProbeSectionState extends State<DebugSyncProbeSection> {
final _phoneController = TextEditingController();
final _nameController = TextEditingController();
bool _loading = false;
String? _result;
@override
void dispose() {
_phoneController.dispose();
_nameController.dispose();
super.dispose();
}
Future<void> _send() async {
final phone = _phoneController.text.trim();
final name = _nameController.text.trim();
if (phone.isEmpty) {
setState(() => _result = 'Введите номер');
return;
}
setState(() {
_loading = true;
_result = null;
});
try {
final packet = await api.sendRequest(Opcode.sync, {
'contactList': {
phone: {'firstName': name},
},
});
if (!mounted) return;
setState(() {
_loading = false;
_result = _pretty(packet.payload);
});
} on PacketError catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_result = 'PacketError: ${e.message}';
});
} catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_result = 'Ошибка: $e';
});
}
}
String _pretty(dynamic payload) {
const encoder = JsonEncoder.withIndent(' ');
try {
return encoder.convert(_jsonSafe(payload));
} catch (_) {
return payload.toString();
}
}
dynamic _jsonSafe(dynamic v) {
if (v is Map) {
return v.map((k, val) => MapEntry(k.toString(), _jsonSafe(val)));
}
if (v is List) return v.map(_jsonSafe).toList();
if (v is String || v is num || v is bool || v == null) return v;
return v.toString();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GlossyPill(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
depth: 6,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sync contactList (21)',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Резолв контакта по номеру и имени, полный ответ сервера',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
const SizedBox(height: 12),
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
enabled: !_loading,
decoration: InputDecoration(
hintText: '+6282233831826',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
),
),
const SizedBox(height: 10),
TextField(
controller: _nameController,
enabled: !_loading,
decoration: InputDecoration(
hintText: 'Имя',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _loading ? null : _send,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Отправить'),
),
if (_result != null) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: SelectableText(
_result!,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontFamily: 'monospace',
),
),
),
],
],
),
);
}
}