Files
IKAR/lib/frontend/screens/profile/info_screen.dart
T

445 lines
14 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/storage/token_storage.dart';
import '../../../core/utils/format.dart';
import '../../../l10n/app_localizations.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/section_header.dart';
import '../../widgets/small_spinner.dart';
class InfoScreen extends StatefulWidget {
const InfoScreen({super.key});
@override
State<InfoScreen> createState() => _InfoScreenState();
}
class _InfoScreenState extends State<InfoScreen> {
bool _isLoading = true;
Map<String, dynamic>? _info;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
try {
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) {
if (mounted) setState(() => _isLoading = false);
return;
}
final jsonStr = await AppDatabase.getLoginInfo(accountId);
if (!mounted) return;
if (jsonStr != null) {
setState(() => _info = jsonDecode(jsonStr) as Map<String, dynamic>);
}
setState(() => _isLoading = false);
} catch (e) {
if (mounted) {
showCustomNotification(context, 'Error: $e');
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context);
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
leading: IconButton(
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: () => Navigator.pop(context),
),
title: ConnectionTitleText(
l10n?.infoTitle ?? 'Info',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
),
body: _isLoading
? const Center(child: SmallSpinner(size: 36))
: _info == null
? Center(
child: Text(
'No data',
style: TextStyle(color: cs.onSurfaceVariant),
),
)
: _buildContent(cs, l10n!),
);
}
Widget _buildContent(ColorScheme cs, AppLocalizations l10n) {
final info = _info!;
final chats = _asStringMap(info['chats']);
final server = _asStringMap(info['server']);
final user = _asStringMap(info['user']);
final experiments = _asStringMap(info['experiments']);
final chatSettings = _asStringMap(info['chatSettings']);
final yMap = _asStringMap(server?['y-map']);
final accountKeys = <String, String>{
'registrationTime': l10n.infoRegistrationTime,
'country': l10n.infoCountry,
'videoChatHistory': l10n.infoVideoChatHistory,
'updateTime': l10n.infoUpdateTime,
'id': l10n.infoId,
'phone': l10n.infoPhone,
'photoId': l10n.infoPhotoId,
'accountStatus': l10n.infoAccountStatus,
'contactOptions': l10n.infoContactOptions,
'profileOptions': l10n.infoProfileOptions,
'names': l10n.infoNames,
'baseUrl': l10n.infoBaseUrl,
'baseRawUrl': l10n.infoBaseRawUrl,
};
final packetKeys = <String, String>{
'chatMarker': l10n.infoChatMarker,
'time': l10n.infoServerTime,
'updates': l10n.infoUpdates,
'messagesCount': l10n.infoMessagesCount,
'contactsCount': l10n.infoContactsCount,
'presenceCount': l10n.infoPresenceCount,
'configHash': l10n.infoConfigHash,
};
final chatKeys = <String, String>{
'count': l10n.infoChatsCount,
'active': l10n.infoChatsActive,
'hidden': l10n.infoChatsHidden,
'dialogs': l10n.infoChatsDialogs,
'groups': l10n.infoChatsGroups,
'channels': l10n.infoChatsChannels,
'unread': l10n.infoChatsUnread,
'newMessages': l10n.infoChatsNewMessages,
'messages': l10n.infoChatsMessages,
};
final serverKeys = <String, String>{
'account-removal-enabled': l10n.infoAccountRemovalEnabled,
'image-size': l10n.infoImageSize,
'gce': l10n.infoGce,
'gcce': l10n.infoGcce,
'max-msg-length': l10n.infoMaxMsgLength,
'quotes-enabled': l10n.infoQuotesEnabled,
'calls-endpoint': l10n.infoCallsEndpoint,
'send-location-enabled': l10n.infoSendLocationEnabled,
'lgce': l10n.infoLgce,
'wud': l10n.infoWud,
'video-msg-enabled': l10n.infoVideoMsgEnabled,
'grse': l10n.infoGrse,
'edit-timeout': l10n.infoEditTimeout,
'image-quality': l10n.infoImageQuality,
'unsafe-files-alert': l10n.infoUnsafeFilesAlert,
'account-nickname-enabled': l10n.infoAccountNicknameEnabled,
'mentions_entity_names_limit': l10n.infoMentionsEntityNamesLimit,
'reactions-enabled': l10n.infoReactionsEnabled,
};
return SelectionArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SectionHeader(l10n.infoAccountSection),
...accountKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, info[entry.key], cs),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoPacketSection),
...packetKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, info[entry.key], cs),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoChatsSection),
...chatKeys.entries.map(
(entry) => _buildDataRow(
'chats.${entry.key}',
entry.value,
chats?[entry.key],
cs,
),
),
const SizedBox(height: 16),
SectionHeader(l10n.infoServerSection),
...serverKeys.entries.map(
(entry) =>
_buildDataRow(entry.key, entry.value, server?[entry.key], cs),
),
..._buildDynamicRows(
server,
cs,
excludedKeys: {
...serverKeys.keys,
'y-map',
'file-upload-unsupported-types',
'white-list-links',
},
),
const SizedBox(height: 8),
SectionHeader(l10n.infoYMapSection),
_buildDataRow('y-map.tile', l10n.infoTile, yMap?['tile'], cs),
_buildDataRow(
'y-map.geocoder',
l10n.infoGeocoder,
yMap?['geocoder'],
cs,
),
_buildDataRow('y-map.static', l10n.infoStatic, yMap?['static'], cs),
const SizedBox(height: 8),
SectionHeader(l10n.infoFileUploadTypes),
_buildListValueRow(
'file-upload-unsupported-types',
l10n.infoFileUploadTypes,
server?['file-upload-unsupported-types'] as List?,
cs,
showLabel: false,
),
const SizedBox(height: 8),
SectionHeader(l10n.infoWhiteListLinks),
_buildListValueRow(
'white-list-links',
l10n.infoWhiteListLinks,
server?['white-list-links'] as List?,
cs,
showLabel: false,
),
if (chatSettings != null && chatSettings.isNotEmpty) ...[
const SizedBox(height: 8),
SectionHeader(l10n.infoChatSettingsSection),
..._buildDynamicRows(chatSettings, cs),
],
if (experiments != null && experiments.isNotEmpty) ...[
const SizedBox(height: 8),
SectionHeader(l10n.infoExperimentsSection),
..._buildDynamicRows(experiments, cs),
],
const SizedBox(height: 8),
SectionHeader(l10n.infoUserSection),
..._buildDynamicRows(user, cs),
const SizedBox(height: 120),
],
),
);
}
List<Widget> _buildDynamicRows(
Map<String, dynamic>? values,
ColorScheme cs, {
Set<String> excludedKeys = const {},
}) {
if (values == null) return [];
final entries = <MapEntry<String, dynamic>>[];
for (final entry in values.entries) {
if (excludedKeys.contains(entry.key) || entry.value == null) continue;
_flattenEntry(entry.key, entry.value, entries);
}
entries.sort((a, b) => a.key.compareTo(b.key));
return entries
.map((entry) => _buildDataRow(entry.key, entry.key, entry.value, cs))
.toList();
}
void _flattenEntry(
String key,
dynamic value,
List<MapEntry<String, dynamic>> target,
) {
final map = _asStringMap(value);
if (map == null || map.isEmpty) {
target.add(MapEntry(key, value));
return;
}
for (final entry in map.entries) {
_flattenEntry('$key.${entry.key}', entry.value, target);
}
}
Widget _buildDataRow(
String key,
String label,
dynamic value,
ColorScheme cs,
) {
if (value is List) {
return _buildListValueRow(key, label, value, cs);
}
return _buildRow(key, label, _formatValue(value, key), cs);
}
Widget _buildRow(String key, String label, String value, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(bottom: 1),
child: GlossyPill(
key: ValueKey('info-$key'),
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
depth: 6,
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
label,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.end,
),
),
],
),
),
);
}
Widget _buildListValueRow(
String key,
String label,
List? items,
ColorScheme cs, {
bool showLabel = true,
}) {
final values = items ?? const [];
final simple = values.every(
(item) => item == null || item is String || item is num || item is bool,
);
return Padding(
padding: const EdgeInsets.only(bottom: 1),
child: GlossyPill(
key: ValueKey('info-$key'),
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.all(16),
depth: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel) ...[
Text(
label,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 8),
],
if (values.isEmpty)
Text('-', style: TextStyle(color: cs.onSurfaceVariant))
else if (simple)
Wrap(
spacing: 8,
runSpacing: 4,
children: values
.map(
(item) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_formatValue(item, key),
style: TextStyle(fontSize: 13, color: cs.onSurface),
),
),
)
.toList(),
)
else
Text(
const JsonEncoder.withIndent(' ').convert(values),
style: TextStyle(
color: cs.onSurface,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
String _formatValue(dynamic value, String key) {
if (value == null) return '-';
if (value is Map && value.containsKey('chatMarker')) {
final ts = value['chatMarker'] as int?;
return ts != null ? _formatTs(ts) : '-';
}
if (key == 'phone' && value is num && value > 0) return '+$value';
if (value is int && value > 1000000000000) return _formatTs(value);
if (key == 'edit-timeout' && value is int && value > 0) {
final weeks = value ~/ 604800;
final days = (value % 604800) ~/ 86400;
if (weeks > 0) {
final l10n = AppLocalizations.of(context)!;
final weeksText = l10n.infoScreenWeeksShort(weeks);
if (days == 0) return weeksText;
return '$weeksText ${l10n.infoScreenDaysShort(days)}';
}
final h = value ~/ 3600;
final m = (value % 3600) ~/ 60;
if (h > 0) return '${h}h ${m}m';
return '${m}m';
}
return value.toString();
}
String _formatTs(int ts) {
if (ts < 1000000000000) return ts.toString();
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
return '${dt.year}-${pad2(dt.month)}-${pad2(dt.day)} '
'${pad2(dt.hour)}:${pad2(dt.minute)}:${pad2(dt.second)}';
}
Map<String, dynamic>? _asStringMap(dynamic value) {
if (value is! Map) return null;
return value.map((key, item) => MapEntry(key.toString(), item));
}
}