небольшие изменения
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user