больше информации в подробнее

This commit is contained in:
Jganenok
2026-05-13 21:26:46 +07:00
parent c6ea5d6dcf
commit 07d39ffa24
14 changed files with 1724 additions and 39 deletions
+120
View File
@@ -27,6 +27,7 @@ class PrivacyConfig {
final String chatsInvite;
final bool pushNewContacts;
final bool unsafeFiles;
final String phoneNumberPrivacy;
final String inactiveTtl;
final bool showReadMark;
final bool altKeyboard;
@@ -48,6 +49,7 @@ class PrivacyConfig {
required this.chatsInvite,
required this.pushNewContacts,
required this.unsafeFiles,
required this.phoneNumberPrivacy,
required this.inactiveTtl,
required this.showReadMark,
required this.altKeyboard,
@@ -71,6 +73,7 @@ class PrivacyConfig {
chatsInvite: map['CHATS_INVITE']?.toString() ?? 'CONTACTS',
pushNewContacts: map['PUSH_NEW_CONTACTS'] ?? false,
unsafeFiles: map['UNSAFE_FILES'] ?? true,
phoneNumberPrivacy: map['PHONE_NUMBER_PRIVACY']?.toString() ?? 'ALL',
inactiveTtl: map['INACTIVE_TTL']?.toString() ?? '6M',
showReadMark: map['SHOW_READ_MARK'] ?? true,
altKeyboard: map['ALT_KEYBOARD'] ?? false,
@@ -94,6 +97,7 @@ class PrivacyConfig {
'CHATS_INVITE': chatsInvite,
'PUSH_NEW_CONTACTS': pushNewContacts,
'UNSAFE_FILES': unsafeFiles,
'PHONE_NUMBER_PRIVACY': phoneNumberPrivacy,
'INACTIVE_TTL': inactiveTtl,
'SHOW_READ_MARK': showReadMark,
'ALT_KEYBOARD': altKeyboard,
@@ -125,6 +129,7 @@ class PrivacyConfig {
chatsInvite: 'CONTACTS',
pushNewContacts: false,
unsafeFiles: true,
phoneNumberPrivacy: 'ALL',
inactiveTtl: '6M',
showReadMark: true,
altKeyboard: false,
@@ -946,6 +951,12 @@ class AccountModule {
logger.w('Папки чатов: $e');
}
try {
await _saveLoginInfo(data, profile.id);
} catch (e) {
logger.w('Info: $e');
}
return LoginResult(
profile: profile,
updatedToken: updatedToken,
@@ -980,6 +991,115 @@ class AccountModule {
}
}
Future<void> _saveLoginInfo(
Map<dynamic, dynamic> data,
int accountId,
) async {
final contact = data['profile']?['contact'] as Map?;
final videoChatHistory = data['videoChatHistory'];
final chats = data['chats'] as List?;
final config = data['config'] as Map?;
final serverConfig = config?['server'] as Map?;
final userConfig = config?['user'] as Map?;
final yMap = serverConfig?['y-map'] as Map?;
final whiteListLinks = serverConfig?['white-list-links'] as List?;
final fileUploadUnsupported = serverConfig?['file-upload-unsupported-types'] as List?;
final time = data['time'] as int?;
final info = {
'registrationTime': contact?['registrationTime'],
'country': contact?['country'],
'videoChatHistory': videoChatHistory,
'updateTime': contact?['updateTime'],
'id': contact?['id'],
'chatMarker': chats != null && chats.isNotEmpty
? _extractChatMarker(chats.cast<Map>())
: null,
'time': time,
'server': serverConfig != null
? _extractServerInfo(serverConfig, yMap, whiteListLinks, fileUploadUnsupported)
: null,
'user': userConfig != null ? _extractUserConfig(userConfig) : null,
};
await AppDatabase.saveLoginInfo(accountId, jsonEncode(info));
}
Map<String, dynamic> _extractChatMarker(List<Map> chats) {
int? latestTime;
for (final chat in chats) {
final lastEventTime = chat['lastEventTime'] as int?;
if (lastEventTime != null && (latestTime == null || lastEventTime > latestTime)) {
latestTime = lastEventTime;
}
}
return {'chatMarker': latestTime};
}
Map<String, dynamic> _extractServerInfo(
Map serverConfig,
Map? yMap,
List? whiteListLinks,
List? fileUploadUnsupported,
) {
return {
'account-removal-enabled': serverConfig['account-removal-enabled'],
'image-size': serverConfig['image-size'],
'gce': serverConfig['gce'],
'gcce': serverConfig['gcce'],
'max-msg-length': serverConfig['max-msg-length'],
'quotes-enabled': serverConfig['quotes-enabled'],
'calls-endpoint': serverConfig['calls-endpoint'],
'send-location-enabled': serverConfig['send-location-enabled'],
'lgce': serverConfig['lgce'],
'wud': serverConfig['wud'],
'video-msg-enabled': serverConfig['video-msg-enabled'],
'grse': serverConfig['grse'],
'edit-timeout': serverConfig['edit-timeout'],
'image-quality': serverConfig['image-quality'],
'unsafe-files-alert': serverConfig['unsafe-files-alert'],
'account-nickname-enabled': serverConfig['account-nickname-enabled'],
'mentions_entity_names_limit': serverConfig['mentions_entity_names_limit'],
'reactions-enabled': serverConfig['reactions-enabled'],
'y-map': yMap != null ? {
'tile': yMap['tile'],
'geocoder': yMap['geocoder'],
'static': yMap['static'],
} : null,
'white-list-links': whiteListLinks,
'file-upload-unsupported-types': fileUploadUnsupported,
};
}
Map<String, dynamic> _extractUserConfig(Map userConfig) {
return {
'CHATS_PUSH_NOTIFICATION': userConfig['CHATS_PUSH_NOTIFICATION'],
'PUSH_DETAILS': userConfig['PUSH_DETAILS'],
'PUSH_SOUND': userConfig['PUSH_SOUND'],
'PHONE_NUMBER_PRIVACY': userConfig['PHONE_NUMBER_PRIVACY'],
'INACTIVE_TTL': userConfig['INACTIVE_TTL'],
'SHOW_READ_MARK': userConfig['SHOW_READ_MARK'],
'AUDIO_TRANSCRIPTION_ENABLED': userConfig['AUDIO_TRANSCRIPTION_ENABLED'],
'SEARCH_BY_PHONE': userConfig['SEARCH_BY_PHONE'],
'INCOMING_CALL': userConfig['INCOMING_CALL'],
'DOUBLE_TAP_REACTION_DISABLED': userConfig['DOUBLE_TAP_REACTION_DISABLED'],
'SAFE_MODE_NO_PIN': userConfig['SAFE_MODE_NO_PIN'],
'CHATS_PUSH_SOUND': userConfig['CHATS_PUSH_SOUND'],
'DOUBLE_TAP_REACTION_VALUE': userConfig['DOUBLE_TAP_REACTION_VALUE'],
'FAMILY_PROTECTION': userConfig['FAMILY_PROTECTION'],
'HIDDEN': userConfig['HIDDEN'],
'CHATS_INVITE': userConfig['CHATS_INVITE'],
'PUSH_NEW_CONTACTS': userConfig['PUSH_NEW_CONTACTS'],
'UNSAFE_FILES': userConfig['UNSAFE_FILES'],
'DONT_DISTURB_UNTIL': userConfig['DONT_DISTURB_UNTIL'],
'ALT_KEYBOARD': userConfig['ALT_KEYBOARD'],
'CONTENT_LEVEL_ACCESS': userConfig['CONTENT_LEVEL_ACCESS'],
'STICKERS_SUGGEST': userConfig['STICKERS_SUGGEST'],
'SAFE_MODE': userConfig['SAFE_MODE'],
'M_CALL_PUSH_NOTIFICATION': userConfig['M_CALL_PUSH_NOTIFICATION'],
};
}
Future<RequestCodeResult> _requestCodeInternal(
String phone,
AuthRequestType type,
+22
View File
@@ -1,7 +1,9 @@
import 'dart:convert';
import '../../core/protocol/opcode_map.dart';
import '../../core/storage/app_database.dart';
import '../../core/utils/logger.dart';
import '../api.dart';
Map<int, int> _parseParticipants(dynamic raw) {
try {
@@ -304,4 +306,24 @@ class ChatsModule {
final name = nameRaw;
return name['name'] as String?;
}
static Future<Map<String, dynamic>?> getChatInfo(Api api, int chatId) async {
final packet = await api.sendRequest(Opcode.chatInfo, {
'chatIds': [chatId],
});
if (packet.isError) return null;
final payload = packet.payload as Map?;
final chats = payload?['chats'] as List?;
if (chats == null || chats.isEmpty) return null;
return Map<String, dynamic>.from(chats.first as Map);
}
static Future<dynamic> searchById(Api api, int userId) async {
final packet = await api.sendRequest(Opcode.publicSearch, {
'query': userId.toString(),
'from': 0,
'count': 10,
});
return packet.payload;
}
}
+14
View File
@@ -124,6 +124,7 @@ abstract class SyncKey {
static const configHash = 'config_hash';
static const chatCacheFingerprint = 'chat_cache_fingerprint';
static const serverTime = 'server_time';
static const loginInfo = 'login_info';
}
class AppDatabase {
@@ -392,6 +393,19 @@ class AppDatabase {
return rows.first['value'] as String;
}
static Future<void> saveLoginInfo(int accountId, String jsonInfo) async {
final db = await _instance;
await db.insert('sync_state', {
'account_id': accountId,
'key': SyncKey.loginInfo,
'value': jsonInfo,
}, conflictAlgorithm: ConflictAlgorithm.replace);
}
static Future<String?> getLoginInfo(int accountId) async {
return getSyncValue(accountId, SyncKey.loginInfo);
}
static Future<void> close() async {
await _db?.close();
_db = null;
+1 -1
View File
@@ -1,7 +1,7 @@
import 'package:shared_preferences/shared_preferences.dart';
class SpoofingService {
static const String hardcodedAppVersion = '26.8.1';
static const String hardcodedAppVersion = '26.15.3';
static const int hardcodedBuildNumber = 6606;
static Future<Map<String, dynamic>?> getSpoofedSessionData() async {
+351 -28
View File
@@ -1,4 +1,9 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/protocol/opcode_map.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart' as main;
import '../../widgets/custom_notification.dart';
class ChatInfoScreen extends StatefulWidget {
final int chatId;
@@ -13,49 +18,367 @@ class ChatInfoScreen extends StatefulWidget {
required this.imageUrl,
required this.chatType,
});
@override
State<ChatInfoScreen> createState() => _ChatInfoScreenState();
}
class _ChatInfoScreenState extends State<ChatInfoScreen>{
class _ChatInfoScreenState extends State<ChatInfoScreen>
with TickerProviderStateMixin {
bool _isLoading = true;
Map<String, dynamic>? _chatData;
late AnimationController _shimmerController;
@override
void initState() {
super.initState();
_shimmerController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
_loadChatData();
}
@override
void dispose() {
_shimmerController.dispose();
super.dispose();
}
Future<void> _loadChatData() async {
try {
final packet = await main.api.sendRequest(Opcode.chatInfo, {
'chatIds': [widget.chatId],
});
final payload = packet.payload as Map?;
if (payload == null) {
if (mounted) setState(() => _isLoading = false);
return;
}
final errorField = payload['error'];
if (errorField != null) {
String errorMsg = 'Error';
if (errorField is Map) {
errorMsg = errorField['localizedMessage'] ?? errorField['message'] ?? errorField.toString();
} else if (errorField is String) {
errorMsg = errorField;
}
if (mounted) showCustomNotification(context, errorMsg);
setState(() => _isLoading = false);
return;
}
final chats = payload['chats'] as List?;
if (chats != null && chats.isNotEmpty) {
_chatData = Map<String, dynamic>.from(chats.first as Map);
} else if (chats != null && chats.isEmpty) {
if (mounted) showCustomNotification(context, 'No data found');
}
if (mounted) 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: Text(
l10n?.chatInfoTitle ?? 'Info',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
),
body:
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.imageUrl.isNotEmpty)
CircleAvatar(
radius: 36,
backgroundImage: NetworkImage(widget.imageUrl),
)
else
CircleAvatar(
radius: 36,
backgroundColor: cs.primaryContainer,
child: Text(
widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?',
style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12),
),
),
Text(
widget.name,
style: TextStyle(color: Colors.white, fontSize: 24, height: 1.3),
body: _isLoading
? _buildShimmer(cs)
: _chatData == null
? Center(
child: Text(
'No data',
style: TextStyle(color: cs.onSurfaceVariant),
),
)
: _buildContent(cs, l10n),
);
}
Widget _buildShimmer(ColorScheme cs) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Center(
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
shape: BoxShape.circle,
),
],
),
),
const SizedBox(height: 12),
Center(
child: Container(
width: 120,
height: 20,
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(6),
),
),
),
const SizedBox(height: 24),
...List.generate(
10,
(_) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
height: 48,
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildContent(ColorScheme cs, AppLocalizations? l10n) {
final chat = _chatData!;
final type = chat['type'] as String? ?? '';
return ListView(
padding: const EdgeInsets.all(16),
children: [
Center(
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: cs.primaryContainer,
),
child: widget.imageUrl.isNotEmpty
? ClipOval(
child: Image.network(widget.imageUrl, fit: BoxFit.cover),
)
: Center(
child: Text(
widget.name.isNotEmpty
? widget.name[0].toUpperCase()
: '?',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
),
),
const SizedBox(height: 12),
Center(
child: Text(
widget.name,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 24),
if (type == 'CHANNEL') ...[
_buildSectionTitle('Channel', cs),
_buildRow(
l10n?.chatInfoSubscribers ?? 'subscribers:',
(chat['participantsCount'] as int?)?.toString() ?? '-',
cs,
),
if ((chat['link'] as String?)?.isNotEmpty ?? false)
_buildRow(
l10n?.chatInfoLink ?? 'link:',
chat['link'] as String,
cs,
),
_buildRow(
l10n?.chatInfoOfficial ?? 'official:',
(chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-',
cs,
),
_buildRow(
l10n?.chatInfoComments ?? 'comments:',
(chat['options']?['COMMENTS'] as bool?)?.toString() ?? '-',
cs,
),
_buildRow(
l10n?.chatInfoAplus ?? 'approved by Roskomnadzor:',
(chat['options']?['A_PLUS_CHANNEL'] as bool?)?.toString() ?? '-',
cs,
),
_buildRow(
l10n?.chatInfoSignAdmin ?? 'admin signature:',
(chat['options']?['SIGN_ADMIN'] as bool?)?.toString() ?? '-',
cs,
),
if ((chat['modified'] as int?) != null)
_buildRow(
l10n?.chatInfoLastChanged ?? 'last changed:',
_formatTs(chat['modified'] as int),
cs,
),
if ((chat['created'] as int?) != null)
_buildRow(
l10n?.chatInfoCreated ?? 'created:',
_formatTs(chat['created'] as int),
cs,
),
],
if (type == 'CHAT') ...[
_buildSectionTitle('Chat', cs),
_buildRow(
l10n?.chatInfoMembers ?? 'members:',
(chat['participantsCount'] as int?)?.toString() ?? '-',
cs,
),
if ((chat['hasBots'] as bool?) ?? false)
_buildRow(
l10n?.chatInfoHasBots ?? 'has bots:',
'true',
cs,
),
if ((chat['blockedParticipantsCount'] as int?) != null &&
chat['blockedParticipantsCount'] > 0)
_buildRow(
l10n?.chatInfoBlockedCount ?? 'blocked in group:',
(chat['blockedParticipantsCount'] as int).toString(),
cs,
),
_buildRow(
l10n?.chatInfoOfficialStatus ?? 'official status:',
(chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-',
cs,
),
if ((chat['modified'] as int?) != null)
_buildRow(
l10n?.chatInfoLastChanged ?? 'last changed:',
_formatTs(chat['modified'] as int),
cs,
),
if ((chat['joinTime'] as int?) != null && chat['joinTime'] != 1)
_buildRow(
l10n?.chatInfoJoined ?? 'joined:',
_formatTs(chat['joinTime'] as int),
cs,
),
if ((chat['created'] as int?) != null)
_buildRow(
l10n?.chatInfoGroupCreated ?? 'group created:',
_formatTs(chat['created'] as int),
cs,
),
if ((chat['owner'] as int?) != null)
_buildRow(
l10n?.chatInfoGroupOwner ?? 'group owner:',
(chat['owner'] as int).toString(),
cs,
),
],
if (type == 'DIALOG') ...[
if ((chat['created'] as int?) != null &&
chat['created'] != 0 &&
chat['created'] != 1)
_buildRow(
l10n?.chatInfoDialogStarted ?? 'dialog started:',
_formatTs(chat['created'] as int),
cs,
),
],
const SizedBox(height: 120),
],
);
}
Widget _buildSectionTitle(String title, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4),
child: Text(
title,
style: TextStyle(
color: cs.primary,
fontSize: 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
}
}
Widget _buildRow(String label, String value, ColorScheme cs) {
return Container(
margin: const EdgeInsets.only(bottom: 1),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
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,
),
),
],
),
);
}
String _formatTs(int ts) {
if (ts < 1000000000000) return ts.toString();
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
}
@@ -1,11 +1,55 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../backend/modules/chats.dart';
import '../../../core/utils/logger.dart';
import '../../../main.dart';
class DebugMenuScreen extends StatelessWidget {
class DebugMenuScreen extends StatefulWidget {
const DebugMenuScreen({super.key});
@override
State<DebugMenuScreen> createState() => _DebugMenuScreenState();
}
class _DebugMenuScreenState extends State<DebugMenuScreen> {
final _idController = TextEditingController();
String? _searchResult;
bool _isSearching = false;
@override
void dispose() {
_idController.dispose();
super.dispose();
}
Future<void> _search() async {
final id = int.tryParse(_idController.text);
if (id == null) return;
setState(() {
_isSearching = true;
_searchResult = null;
});
try {
final result = await ChatsModule.searchById(api, id);
logger.i('searchById result: $result');
if (!mounted) return;
if (result is Map && result.containsKey('error')) {
final errorMsg = result['localizedMessage'] ?? result['message'] ?? result['error'] ?? 'Error';
setState(() => _searchResult = 'Error: $errorMsg');
} else if (result is Map) {
setState(() => _searchResult = result.toString());
} else {
setState(() => _searchResult = result?.toString() ?? 'null');
}
} catch (e) {
if (mounted) {
setState(() => _searchResult = 'Exception: $e');
}
} finally {
if (mounted) setState(() => _isSearching = false);
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
@@ -115,10 +159,92 @@ class DebugMenuScreen extends StatelessWidget {
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Поиск по ID (opcode 60)',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: _idController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Введите user ID',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => _search(),
),
),
const SizedBox(width: 12),
FilledButton(
onPressed: _isSearching ? null : _search,
child: _isSearching
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Symbols.search, size: 20),
),
],
),
if (_searchResult != null) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
constraints: const BoxConstraints(maxHeight: 400),
child: SingleChildScrollView(
child: Text(
_searchResult!,
style: TextStyle(
color: cs.onSurface,
fontSize: 12,
fontFamily: 'monospace',
),
),
),
),
],
],
),
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 120)),
],
),
),
);
}
}
}
@@ -0,0 +1,286 @@
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 '../../../l10n/app_localizations.dart';
import '../../widgets/custom_notification.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 (jsonStr != null) {
setState(() => _info = jsonDecode(jsonStr) as Map<String, dynamic>);
}
if (mounted) 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: Text(
l10n?.infoTitle ?? 'Info',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _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 server = info['server'] as Map<String, dynamic>?;
final user = info['user'] as Map<String, dynamic>?;
final yMap = server?['y-map'] as Map<String, dynamic>?;
final accountKeys = <String, String>{
'registrationTime': l10n.infoRegistrationTime,
'country': l10n.infoCountry,
'videoChatHistory': l10n.infoVideoChatHistory,
'updateTime': l10n.infoUpdateTime,
'id': l10n.infoId,
'chatMarker': l10n.infoChatMarker,
};
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 ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSectionTitle(l10n.infoAccountSection, cs),
...accountKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(info[e.key]), cs)),
const SizedBox(height: 16),
_buildSectionTitle(l10n.infoServerSection, cs),
...serverKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(server?[e.key]), cs)),
const SizedBox(height: 8),
_buildSectionTitle(l10n.infoYMapSection, cs),
_buildRow('tile', l10n.infoTile, yMap?['tile']?.toString() ?? '-', cs),
_buildRow('geocoder', l10n.infoGeocoder, yMap?['geocoder']?.toString() ?? '-', cs),
_buildRow('static', l10n.infoStatic, yMap?['static']?.toString() ?? '-', cs),
const SizedBox(height: 8),
_buildSectionTitle(l10n.infoFileUploadTypes, cs),
_buildListRow(server?['file-upload-unsupported-types'] as List?, cs),
const SizedBox(height: 8),
_buildSectionTitle(l10n.infoWhiteListLinks, cs),
_buildListRow(server?['white-list-links'] as List?, cs),
const SizedBox(height: 8),
_buildSectionTitle(l10n.infoUserSection, cs),
if (user != null)
...user.entries
.where((e) => e.value != null)
.map((e) => _buildRow(e.key, e.key, e.value.toString(), cs)),
const SizedBox(height: 120),
],
);
}
Widget _buildSectionTitle(String title, ColorScheme cs) {
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4),
child: Text(
title,
style: TextStyle(
color: cs.primary,
fontSize: 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
);
}
Widget _buildRow(String key, String label, String value, ColorScheme cs) {
return Container(
margin: const EdgeInsets.only(bottom: 1),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
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 _buildListRow(List? items, ColorScheme cs) {
if (items == null || items.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)),
);
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: items
.map(
(item) => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
item.toString(),
style: TextStyle(fontSize: 13, color: cs.onSurface),
),
),
)
.toList(),
),
);
}
String _formatValue(dynamic value) {
if (value == null) return '-';
if (value is Map && value.containsKey('chatMarker')) {
final ts = value['chatMarker'] as int?;
if (ts != null) {
final dt = DateTime.fromMillisecondsSinceEpoch(ts);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
return '-';
}
if (value is int && value > 1000000000000) {
final dt = DateTime.fromMillisecondsSinceEpoch(value);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
if (value is int && value > 86400) {
final weeks = value ~/ 604800;
final days = (value % 604800) ~/ 86400;
if (weeks > 0) {
return '$weeks ${_w(weeks)} ${days > 0 ? '$days ${_d(days)}' : ''}'.trim();
}
final h = value ~/ 3600;
final m = (value % 3600) ~/ 60;
if (h > 0) return '${h}h ${m}m';
return '${m}m';
}
return value.toString();
}
String _w(int n) {
final m = n % 10;
if (m == 1 && n != 11) return 'нед';
if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'нед';
return 'нед';
}
String _d(int n) {
final m = n % 10;
if (m == 1 && n != 11) return 'дн';
if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'дн';
return 'дн';
}
}
@@ -222,6 +222,7 @@ class _SecurityScreenState extends State<SecurityScreen>
case 'CONTACTS':
return 'Мои контакты';
case 'NONE':
case 'NOBODY':
return 'Никто';
default:
return value;
@@ -481,9 +482,31 @@ class _SecurityScreenState extends State<SecurityScreen>
icon: Icons.visibility_off_outlined,
label: 'Видеть статус «в сети»',
value: _privacyConfig?.hidden == true ? 'Никто' : 'Мои контакты',
isLast: true,
isLast: false,
onTap: () => _showHiddenStatusSheet(context, cs),
),
_buildOptionRow(
cs,
icon: Symbols.contact_page,
label: 'Видеть мой номер',
value: _getPrivacyLabel(
_privacyConfig?.phoneNumberPrivacy ?? 'ALL',
),
isLast: true,
onTap: () => _showOptionSheet(
context,
cs,
title: 'Видеть мой номер',
currentValue: _privacyConfig?.phoneNumberPrivacy ?? 'ALL',
options: const [
('ALL', 'Все'),
('CONTACTS', 'Мои контакты'),
('NOBODY', 'Никто'),
],
onSelect: (value) =>
_updateSetting('PHONE_NUMBER_PRIVACY', value),
),
),
],
],
),
+17 -4
View File
@@ -8,6 +8,7 @@ import '../../../l10n/app_localizations.dart';
import '../auth/proxy_settings_sheet.dart';
import 'debug_menu_screen.dart';
import 'devices_screen.dart';
import 'info_screen.dart';
import 'security_screen.dart';
import 'spoof_screen.dart';
@@ -96,15 +97,27 @@ class _SettingsTabState extends State<SettingsTab> {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: _buildSection(
child: _buildSection(
context,
cs,
items: const [
_SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'),
_SettingsItem(
items: [
const _SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'),
const _SettingsItem(
icon: Symbols.language,
label: 'Войти в Сферум',
),
_SettingsItem(
icon: Symbols.info,
label: 'Info',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const InfoScreen(),
),
);
},
),
],
),
),
+56 -1
View File
@@ -104,5 +104,60 @@
}
}
},
"profileMenuSpoof": "Spoofing"
"profileMenuSpoof": "Spoofing",
"infoTitle": "Info",
"infoAccountSection": "Account",
"infoServerSection": "Server",
"infoUserSection": "User",
"infoYMapSection": "Y-Map",
"infoFileUploadTypes": "file-upload-unsupported-types",
"infoWhiteListLinks": "white-list-links",
"infoRegistrationTime": "registrationTime",
"infoCountry": "country",
"infoVideoChatHistory": "videoChatHistory",
"infoUpdateTime": "updateTime",
"infoId": "id",
"infoChatMarker": "chatMarker",
"infoAccountRemovalEnabled": "account-removal-enabled",
"infoImageSize": "image-size",
"infoGce": "gce",
"infoGcce": "gcce",
"infoMaxMsgLength": "max-msg-length",
"infoQuotesEnabled": "quotes-enabled",
"infoCallsEndpoint": "calls-endpoint",
"infoSendLocationEnabled": "send-location-enabled",
"infoLgce": "lgce",
"infoWud": "wud",
"infoVideoMsgEnabled": "video-msg-enabled",
"infoGrse": "grse",
"infoEditTimeout": "edit-timeout",
"infoImageQuality": "image-quality",
"infoUnsafeFilesAlert": "unsafe-files-alert",
"infoAccountNicknameEnabled": "account-nickname-enabled",
"infoMentionsEntityNamesLimit": "mentions_entity_names_limit",
"infoReactionsEnabled": "reactions-enabled",
"infoTile": "tile",
"infoGeocoder": "geocoder",
"infoStatic": "static",
"chatInfoSubscribers": "subscribers:",
"chatInfoInvitedBy": "invited by:",
"chatInfoLink": "link:",
"chatInfoOfficial": "official:",
"chatInfoComments": "comments:",
"chatInfoAplus": "approved by Roskomnadzor:",
"chatInfoSignAdmin": "admin signature:",
"chatInfoLastChanged": "last changed:",
"chatInfoJoinTime": "joined:",
"chatInfoCreated": "created:",
"chatInfoTitle": "Info",
"chatInfoMembers": "members:",
"chatInfoLastSeen": "last seen recently",
"chatInfoHasBots": "has bots:",
"chatInfoBlockedCount": "blocked in group:",
"chatInfoOfficialStatus": "official status:",
"chatInfoLastChanged": "last changed:",
"chatInfoJoined": "joined:",
"chatInfoGroupCreated": "group created:",
"chatInfoGroupOwner": "group owner:",
"chatInfoDialogStarted": "dialog started:"
}
+324
View File
@@ -631,6 +631,330 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Spoofing'**
String get profileMenuSpoof;
/// No description provided for @infoTitle.
///
/// In en, this message translates to:
/// **'Info'**
String get infoTitle;
/// No description provided for @infoAccountSection.
///
/// In en, this message translates to:
/// **'Account'**
String get infoAccountSection;
/// No description provided for @infoServerSection.
///
/// In en, this message translates to:
/// **'Server'**
String get infoServerSection;
/// No description provided for @infoUserSection.
///
/// In en, this message translates to:
/// **'User'**
String get infoUserSection;
/// No description provided for @infoYMapSection.
///
/// In en, this message translates to:
/// **'Y-Map'**
String get infoYMapSection;
/// No description provided for @infoFileUploadTypes.
///
/// In en, this message translates to:
/// **'file-upload-unsupported-types'**
String get infoFileUploadTypes;
/// No description provided for @infoWhiteListLinks.
///
/// In en, this message translates to:
/// **'white-list-links'**
String get infoWhiteListLinks;
/// No description provided for @infoRegistrationTime.
///
/// In en, this message translates to:
/// **'registrationTime'**
String get infoRegistrationTime;
/// No description provided for @infoCountry.
///
/// In en, this message translates to:
/// **'country'**
String get infoCountry;
/// No description provided for @infoVideoChatHistory.
///
/// In en, this message translates to:
/// **'videoChatHistory'**
String get infoVideoChatHistory;
/// No description provided for @infoUpdateTime.
///
/// In en, this message translates to:
/// **'updateTime'**
String get infoUpdateTime;
/// No description provided for @infoId.
///
/// In en, this message translates to:
/// **'id'**
String get infoId;
/// No description provided for @infoChatMarker.
///
/// In en, this message translates to:
/// **'chatMarker'**
String get infoChatMarker;
/// No description provided for @infoAccountRemovalEnabled.
///
/// In en, this message translates to:
/// **'account-removal-enabled'**
String get infoAccountRemovalEnabled;
/// No description provided for @infoImageSize.
///
/// In en, this message translates to:
/// **'image-size'**
String get infoImageSize;
/// No description provided for @infoGce.
///
/// In en, this message translates to:
/// **'gce'**
String get infoGce;
/// No description provided for @infoGcce.
///
/// In en, this message translates to:
/// **'gcce'**
String get infoGcce;
/// No description provided for @infoMaxMsgLength.
///
/// In en, this message translates to:
/// **'max-msg-length'**
String get infoMaxMsgLength;
/// No description provided for @infoQuotesEnabled.
///
/// In en, this message translates to:
/// **'quotes-enabled'**
String get infoQuotesEnabled;
/// No description provided for @infoCallsEndpoint.
///
/// In en, this message translates to:
/// **'calls-endpoint'**
String get infoCallsEndpoint;
/// No description provided for @infoSendLocationEnabled.
///
/// In en, this message translates to:
/// **'send-location-enabled'**
String get infoSendLocationEnabled;
/// No description provided for @infoLgce.
///
/// In en, this message translates to:
/// **'lgce'**
String get infoLgce;
/// No description provided for @infoWud.
///
/// In en, this message translates to:
/// **'wud'**
String get infoWud;
/// No description provided for @infoVideoMsgEnabled.
///
/// In en, this message translates to:
/// **'video-msg-enabled'**
String get infoVideoMsgEnabled;
/// No description provided for @infoGrse.
///
/// In en, this message translates to:
/// **'grse'**
String get infoGrse;
/// No description provided for @infoEditTimeout.
///
/// In en, this message translates to:
/// **'edit-timeout'**
String get infoEditTimeout;
/// No description provided for @infoImageQuality.
///
/// In en, this message translates to:
/// **'image-quality'**
String get infoImageQuality;
/// No description provided for @infoUnsafeFilesAlert.
///
/// In en, this message translates to:
/// **'unsafe-files-alert'**
String get infoUnsafeFilesAlert;
/// No description provided for @infoAccountNicknameEnabled.
///
/// In en, this message translates to:
/// **'account-nickname-enabled'**
String get infoAccountNicknameEnabled;
/// No description provided for @infoMentionsEntityNamesLimit.
///
/// In en, this message translates to:
/// **'mentions_entity_names_limit'**
String get infoMentionsEntityNamesLimit;
/// No description provided for @infoReactionsEnabled.
///
/// In en, this message translates to:
/// **'reactions-enabled'**
String get infoReactionsEnabled;
/// No description provided for @infoTile.
///
/// In en, this message translates to:
/// **'tile'**
String get infoTile;
/// No description provided for @infoGeocoder.
///
/// In en, this message translates to:
/// **'geocoder'**
String get infoGeocoder;
/// No description provided for @infoStatic.
///
/// In en, this message translates to:
/// **'static'**
String get infoStatic;
/// No description provided for @chatInfoSubscribers.
///
/// In en, this message translates to:
/// **'subscribers:'**
String get chatInfoSubscribers;
/// No description provided for @chatInfoInvitedBy.
///
/// In en, this message translates to:
/// **'invited by:'**
String get chatInfoInvitedBy;
/// No description provided for @chatInfoLink.
///
/// In en, this message translates to:
/// **'link:'**
String get chatInfoLink;
/// No description provided for @chatInfoOfficial.
///
/// In en, this message translates to:
/// **'official:'**
String get chatInfoOfficial;
/// No description provided for @chatInfoComments.
///
/// In en, this message translates to:
/// **'comments:'**
String get chatInfoComments;
/// No description provided for @chatInfoAplus.
///
/// In en, this message translates to:
/// **'approved by Roskomnadzor:'**
String get chatInfoAplus;
/// No description provided for @chatInfoSignAdmin.
///
/// In en, this message translates to:
/// **'admin signature:'**
String get chatInfoSignAdmin;
/// No description provided for @chatInfoLastChanged.
///
/// In en, this message translates to:
/// **'last changed:'**
String get chatInfoLastChanged;
/// No description provided for @chatInfoJoinTime.
///
/// In en, this message translates to:
/// **'joined:'**
String get chatInfoJoinTime;
/// No description provided for @chatInfoCreated.
///
/// In en, this message translates to:
/// **'created:'**
String get chatInfoCreated;
/// No description provided for @chatInfoTitle.
///
/// In en, this message translates to:
/// **'Info'**
String get chatInfoTitle;
/// No description provided for @chatInfoMembers.
///
/// In en, this message translates to:
/// **'members:'**
String get chatInfoMembers;
/// No description provided for @chatInfoLastSeen.
///
/// In en, this message translates to:
/// **'last seen recently'**
String get chatInfoLastSeen;
/// No description provided for @chatInfoHasBots.
///
/// In en, this message translates to:
/// **'has bots:'**
String get chatInfoHasBots;
/// No description provided for @chatInfoBlockedCount.
///
/// In en, this message translates to:
/// **'blocked in group:'**
String get chatInfoBlockedCount;
/// No description provided for @chatInfoOfficialStatus.
///
/// In en, this message translates to:
/// **'official status:'**
String get chatInfoOfficialStatus;
/// No description provided for @chatInfoJoined.
///
/// In en, this message translates to:
/// **'joined:'**
String get chatInfoJoined;
/// No description provided for @chatInfoGroupCreated.
///
/// In en, this message translates to:
/// **'group created:'**
String get chatInfoGroupCreated;
/// No description provided for @chatInfoGroupOwner.
///
/// In en, this message translates to:
/// **'group owner:'**
String get chatInfoGroupOwner;
/// No description provided for @chatInfoDialogStarted.
///
/// In en, this message translates to:
/// **'dialog started:'**
String get chatInfoDialogStarted;
}
class _AppLocalizationsDelegate
+162
View File
@@ -289,4 +289,166 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get profileMenuSpoof => 'Spoofing';
@override
String get infoTitle => 'Info';
@override
String get infoAccountSection => 'Account';
@override
String get infoServerSection => 'Server';
@override
String get infoUserSection => 'User';
@override
String get infoYMapSection => 'Y-Map';
@override
String get infoFileUploadTypes => 'file-upload-unsupported-types';
@override
String get infoWhiteListLinks => 'white-list-links';
@override
String get infoRegistrationTime => 'registrationTime';
@override
String get infoCountry => 'country';
@override
String get infoVideoChatHistory => 'videoChatHistory';
@override
String get infoUpdateTime => 'updateTime';
@override
String get infoId => 'id';
@override
String get infoChatMarker => 'chatMarker';
@override
String get infoAccountRemovalEnabled => 'account-removal-enabled';
@override
String get infoImageSize => 'image-size';
@override
String get infoGce => 'gce';
@override
String get infoGcce => 'gcce';
@override
String get infoMaxMsgLength => 'max-msg-length';
@override
String get infoQuotesEnabled => 'quotes-enabled';
@override
String get infoCallsEndpoint => 'calls-endpoint';
@override
String get infoSendLocationEnabled => 'send-location-enabled';
@override
String get infoLgce => 'lgce';
@override
String get infoWud => 'wud';
@override
String get infoVideoMsgEnabled => 'video-msg-enabled';
@override
String get infoGrse => 'grse';
@override
String get infoEditTimeout => 'edit-timeout';
@override
String get infoImageQuality => 'image-quality';
@override
String get infoUnsafeFilesAlert => 'unsafe-files-alert';
@override
String get infoAccountNicknameEnabled => 'account-nickname-enabled';
@override
String get infoMentionsEntityNamesLimit => 'mentions_entity_names_limit';
@override
String get infoReactionsEnabled => 'reactions-enabled';
@override
String get infoTile => 'tile';
@override
String get infoGeocoder => 'geocoder';
@override
String get infoStatic => 'static';
@override
String get chatInfoSubscribers => 'subscribers:';
@override
String get chatInfoInvitedBy => 'invited by:';
@override
String get chatInfoLink => 'link:';
@override
String get chatInfoOfficial => 'official:';
@override
String get chatInfoComments => 'comments:';
@override
String get chatInfoAplus => 'approved by Roskomnadzor:';
@override
String get chatInfoSignAdmin => 'admin signature:';
@override
String get chatInfoLastChanged => 'last changed:';
@override
String get chatInfoJoinTime => 'joined:';
@override
String get chatInfoCreated => 'created:';
@override
String get chatInfoTitle => 'Info';
@override
String get chatInfoMembers => 'members:';
@override
String get chatInfoLastSeen => 'last seen recently';
@override
String get chatInfoHasBots => 'has bots:';
@override
String get chatInfoBlockedCount => 'blocked in group:';
@override
String get chatInfoOfficialStatus => 'official status:';
@override
String get chatInfoJoined => 'joined:';
@override
String get chatInfoGroupCreated => 'group created:';
@override
String get chatInfoGroupOwner => 'group owner:';
@override
String get chatInfoDialogStarted => 'dialog started:';
}
+162
View File
@@ -291,4 +291,166 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get profileMenuSpoof => 'Подмена данных';
@override
String get infoTitle => 'Info';
@override
String get infoAccountSection => 'Аккаунт';
@override
String get infoServerSection => 'Сервер';
@override
String get infoUserSection => 'Пользователь';
@override
String get infoYMapSection => 'Y-Map';
@override
String get infoFileUploadTypes => 'запрещённые типы файлов';
@override
String get infoWhiteListLinks => 'безопасные ссылки';
@override
String get infoRegistrationTime => 'Дата регистрации:';
@override
String get infoCountry => 'Регион аккаунта:';
@override
String get infoVideoChatHistory => 'videoChatHistory';
@override
String get infoUpdateTime => 'Последнее обновление аватарки:';
@override
String get infoId => 'id аккаунта:';
@override
String get infoChatMarker => 'chatMarker';
@override
String get infoAccountRemovalEnabled => 'Мгновенное удаление аккаунта:';
@override
String get infoImageSize => 'image-size';
@override
String get infoGce => 'gce';
@override
String get infoGcce => 'gcce';
@override
String get infoMaxMsgLength => 'макс. длина сообщения:';
@override
String get infoQuotesEnabled => 'quotes-enabled';
@override
String get infoCallsEndpoint => 'calls-endpoint';
@override
String get infoSendLocationEnabled => 'отправка гео.:';
@override
String get infoLgce => 'lgce';
@override
String get infoWud => 'wud';
@override
String get infoVideoMsgEnabled => 'Кружки:';
@override
String get infoGrse => 'grse';
@override
String get infoEditTimeout => 'Можно редактировать сообщение в течении:';
@override
String get infoImageQuality => 'image-quality';
@override
String get infoUnsafeFilesAlert => 'unsafe-files-alert';
@override
String get infoAccountNicknameEnabled => 'account-nickname-enabled';
@override
String get infoMentionsEntityNamesLimit => 'макс. кол-во упоминаний:';
@override
String get infoReactionsEnabled => 'reactions-enabled';
@override
String get infoTile => 'tile';
@override
String get infoGeocoder => 'geocoder';
@override
String get infoStatic => 'static';
@override
String get chatInfoSubscribers => 'подписчиков:';
@override
String get chatInfoInvitedBy => 'Приглашён от:';
@override
String get chatInfoLink => 'ссылка:';
@override
String get chatInfoOfficial => 'оффициальный:';
@override
String get chatInfoComments => 'комментарии:';
@override
String get chatInfoAplus => 'подтверждён Роскомнадзором:';
@override
String get chatInfoSignAdmin => 'Подпись админов:';
@override
String get chatInfoLastChanged => 'последнее изменение:';
@override
String get chatInfoJoinTime => 'заход в канал:';
@override
String get chatInfoCreated => 'канал создан:';
@override
String get chatInfoTitle => 'Информация';
@override
String get chatInfoMembers => 'участников:';
@override
String get chatInfoLastSeen => 'был(а) недавно';
@override
String get chatInfoHasBots => 'Есть боты:';
@override
String get chatInfoBlockedCount => 'в ЧС группы:';
@override
String get chatInfoOfficialStatus => 'Официальный статус:';
@override
String get chatInfoJoined => 'Зашли в:';
@override
String get chatInfoGroupCreated => 'Группа создана в:';
@override
String get chatInfoGroupOwner => 'Создатель группы:';
@override
String get chatInfoDialogStarted => 'ЛС начат в:';
}
+56 -1
View File
@@ -104,5 +104,60 @@
}
}
},
"profileMenuSpoof": "Подмена данных"
"profileMenuSpoof": "Подмена данных",
"infoTitle": "Info",
"infoAccountSection": "Аккаунт",
"infoServerSection": "Сервер",
"infoUserSection": "Пользователь",
"infoYMapSection": "Y-Map",
"infoFileUploadTypes": "запрещённые типы файлов",
"infoWhiteListLinks": "безопасные ссылки",
"infoRegistrationTime": "Дата регистрации:",
"infoCountry": "Регион аккаунта:",
"infoVideoChatHistory": "videoChatHistory",
"infoUpdateTime": "Последнее обновление аватарки:",
"infoId": "id аккаунта:",
"infoChatMarker": "chatMarker",
"infoAccountRemovalEnabled": "Мгновенное удаление аккаунта:",
"infoImageSize": "image-size",
"infoGce": "gce",
"infoGcce": "gcce",
"infoMaxMsgLength": "макс. длина сообщения:",
"infoQuotesEnabled": "quotes-enabled",
"infoCallsEndpoint": "calls-endpoint",
"infoSendLocationEnabled": "отправка гео.:",
"infoLgce": "lgce",
"infoWud": "wud",
"infoVideoMsgEnabled": "Кружки:",
"infoGrse": "grse",
"infoEditTimeout": "Можно редактировать сообщение в течении:",
"infoImageQuality": "image-quality",
"infoUnsafeFilesAlert": "unsafe-files-alert",
"infoAccountNicknameEnabled": "account-nickname-enabled",
"infoMentionsEntityNamesLimit": "макс. кол-во упоминаний:",
"infoReactionsEnabled": "reactions-enabled",
"infoTile": "tile",
"infoGeocoder": "geocoder",
"infoStatic": "static",
"chatInfoSubscribers": "подписчиков:",
"chatInfoInvitedBy": "Приглашён от:",
"chatInfoLink": "ссылка:",
"chatInfoOfficial": "оффициальный:",
"chatInfoComments": "комментарии:",
"chatInfoAplus": "подтверждён Роскомнадзором:",
"chatInfoSignAdmin": "Подпись админов:",
"chatInfoLastChanged": "последнее изменение:",
"chatInfoJoinTime": "заход в канал:",
"chatInfoCreated": "канал создан:",
"chatInfoTitle": "Информация",
"chatInfoMembers": "участников:",
"chatInfoLastSeen": "был(а) недавно",
"chatInfoHasBots": "Есть боты:",
"chatInfoBlockedCount": "в ЧС группы:",
"chatInfoOfficialStatus": "Официальный статус:",
"chatInfoLastChanged": "последнее изменение:",
"chatInfoJoined": "Зашли в:",
"chatInfoGroupCreated": "Группа создана в:",
"chatInfoGroupOwner": "Создатель группы:",
"chatInfoDialogStarted": "ЛС начат в:"
}