From e39cb9dec91dcfc69b01b8780d9e4971cbca2240 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Thu, 23 Jul 2026 21:03:55 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/contacts.dart | 78 ++++ lib/backend/modules/messages.dart | 8 + lib/core/cache/info_cache.dart | 4 + lib/core/storage/app_database.dart | 9 + .../screens/chats/chat_info_screen.dart | 106 +++++- .../screens/chats/chat_list_screen.dart | 2 + lib/frontend/screens/chats/chat_screen.dart | 25 +- .../screens/contacts/edit_contact_sheet.dart | 346 ++++++++++++++++++ lib/l10n/app_en.arb | 12 +- lib/l10n/app_localizations.dart | 60 +++ lib/l10n/app_localizations_en.dart | 31 ++ lib/l10n/app_localizations_ru.dart | 31 ++ lib/l10n/app_ru.arb | 12 +- 13 files changed, 709 insertions(+), 15 deletions(-) create mode 100644 lib/frontend/screens/contacts/edit_contact_sheet.dart diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index c87e7b4..1ef25ea 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -1,10 +1,12 @@ import 'package:flutter/foundation.dart'; +import '../../core/cache/info_cache.dart'; import '../../core/config/debug_test.dart'; import '../../core/protocol/opcode_map.dart'; import '../../core/protocol/packet.dart'; import '../../core/storage/app_database.dart'; import '../../core/utils/logger.dart'; +import '../../models/contact_info.dart'; import '../api.dart'; import 'messages.dart'; @@ -241,6 +243,82 @@ class ContactsModule { ); } + static Future updateContact( + Api api, { + required int contactId, + required String firstName, + required String lastName, + }) async { + final Packet resp; + try { + resp = await api.sendRequest(Opcode.contactUpdate, { + 'contactId': contactId, + 'action': 'UPDATE', + 'firstName': firstName, + 'lastName': lastName, + }); + } catch (_) { + return null; + } + + final profile = await AppDatabase.loadActiveProfile(); + if (profile == null) return null; + + final data = resp.payload; + final contact = (data is Map && data['contact'] is Map) + ? (data['contact'] as Map).cast() + : null; + if (contact == null) return null; + + final row = _parseContact(contact, profile.id); + if (row == null) return null; + + await AppDatabase.saveContacts([row]); + primeContactCache(contact); + ContactInfoFetch.putContact(contactId, contact); + revision.value++; + return CachedContact.fromDbRow(row); + } + + static Future removeContact(Api api, int contactId) async { + try { + await api.sendRequest(Opcode.contactUpdate, { + 'contactId': contactId, + 'action': 'REMOVE', + }); + } catch (_) { + return false; + } + + final profile = await AppDatabase.loadActiveProfile(); + if (profile != null) { + await AppDatabase.deleteContact(profile.id, contactId); + } + + ContactCache.remove(contactId); + + ContactInfo? info = ContactInfoFetch.peek(contactId); + if (info == null) { + ContactInfoFetch.invalidate(contactId); + info = await ContactInfoFetch.get(contactId, forceRefresh: true); + } + + final rawNames = info?.raw['names']; + if (info != null && rawNames is List) { + final stripped = rawNames + .where((n) => !(n is Map && n['type'] == 'CUSTOM')) + .toList(); + final newRaw = Map.from(info.raw)..['names'] = stripped; + ContactInfoFetch.putContact(contactId, newRaw); + primeContactCache(newRaw); + } else { + ContactInfoFetch.invalidate(contactId); + } + + revision.value++; + return true; + } + static Future syncFromLoginPayload( Map data, int accountId, diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 2b99101..ab948df 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -80,6 +80,14 @@ class ContactCache { static bool isOfficial(int id) => _optionsCache[id]?.contains('OFFICIAL') ?? false; + static void remove(int id) { + _nameCache.remove(id); + _avatarCache.remove(id); + _optionsCache.remove(id); + _phoneCache.remove(id); + _scheduleSave(); + } + static void clear() { _nameCache.clear(); _avatarCache.clear(); diff --git a/lib/core/cache/info_cache.dart b/lib/core/cache/info_cache.dart index 52eb7b4..c69f1aa 100644 --- a/lib/core/cache/info_cache.dart +++ b/lib/core/cache/info_cache.dart @@ -109,6 +109,10 @@ class ContactInfoFetch { static void invalidate(int id) => _cache.invalidate(id); static void clear() => _cache.clear(); + static void putContact(int id, Map contact) { + _cache.putValue(id, ContactInfo.fromMap(Map.from(contact))); + } + static Future _fetch(int id) async { final api = _api; if (api == null || api.state != SessionState.online) return null; diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index bb5caa8..ce13def 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -883,6 +883,15 @@ class AppDatabase { ); } + static Future deleteContact(int accountId, int id) async { + final db = await _instance; + await db.delete( + 'contacts', + where: 'account_id = ? AND id = ?', + whereArgs: [accountId, id], + ); + } + static Future saveMessages(List> rows) async { final db = await _instance; await db.transaction((txn) async { diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index 2789b30..470ea71 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:komet/main.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../contacts/edit_contact_sheet.dart'; import '../../../backend/modules/messages.dart' show ContactCache; import '../../../core/cache/info_cache.dart'; import '../../../core/config/app_show_extra_info.dart'; @@ -77,6 +78,7 @@ class _ChatInfoScreenState extends State { String _selectedTab = ''; bool _descExpanded = false; bool _showRealName = false; + String? _nameOverride; int? _otherId; ContactInfo? _contactData; @@ -275,12 +277,7 @@ class _ChatInfoScreenState extends State { icon: Icon(Icons.arrow_back, color: cs.onSurface), onPressed: () => Navigator.pop(context), ), - actions: [ - IconButton( - icon: Icon(Icons.more_vert, color: cs.onSurface), - onPressed: () {}, - ), - ], + actions: [_buildMoreButton(cs)], ), SliverToBoxAdapter(child: _buildBody(cs)), ], @@ -316,6 +313,96 @@ class _ChatInfoScreenState extends State { ); } + ContactName? _nameEntry(String type) { + final data = _contactData; + if (data == null) return null; + for (final n in data.names) { + if (n.type == type) return n; + } + return null; + } + + bool get _isContact => _nameEntry('CUSTOM') != null; + + String _joinName(String first, String last) => last.trim().isEmpty + ? first.trim() + : '${first.trim()} ${last.trim()}'; + + Widget _buildMoreButton(ColorScheme cs) { + final canEdit = widget.chatType == 'DIALOG' && _isContact; + if (!canEdit) { + return IconButton( + icon: Icon(Icons.more_vert, color: cs.onSurface), + onPressed: () {}, + ); + } + final l10n = AppLocalizations.of(context)!; + return PopupMenuButton( + icon: Icon(Icons.more_vert, color: cs.onSurface), + onSelected: (v) { + if (v == 'edit') _openEdit(); + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Icon(Symbols.edit, size: 20, color: cs.onSurface), + const SizedBox(width: 12), + Text(l10n.editContactMenu), + ], + ), + ), + ], + ); + } + + Future _openEdit() async { + final custom = _nameEntry('CUSTOM'); + final oneme = _nameEntry('ONEME'); + final peerId = _otherId ?? widget.dialogPeerId ?? 0; + if (peerId == 0) return; + + final result = await showEditContactSheet( + context, + contactId: peerId, + avatarUrl: _contactData?.avatarUrl ?? widget.imageUrl, + customFirst: custom?.firstName ?? '', + customLast: custom?.lastName ?? '', + onemeFirst: oneme?.firstName ?? '', + onemeLast: oneme?.lastName ?? '', + ); + if (!mounted || result == null) return; + + switch (result.action) { + case EditContactAction.updated: + _applyUpdatedNames(result.firstName, result.lastName); + case EditContactAction.removed: + Navigator.of(context).pop(); + } + } + + void _applyUpdatedNames(String first, String last) { + final data = _contactData; + final newCustom = ContactName( + type: 'CUSTOM', + name: first, + firstName: first, + lastName: last, + ); + final others = data == null + ? [] + : data.names.where((n) => n.type != 'CUSTOM').toList(); + setState(() { + _contactData = ContactInfo( + raw: data?.raw ?? const {}, + names: [newCustom, ...others], + ); + _nameOverride = _joinName(first, last); + _showRealName = false; + }); + } + String? get _realName { final data = _contactData; if (data == null) return null; @@ -339,19 +426,20 @@ class _ChatInfoScreenState extends State { fontSize: 22, fontWeight: FontWeight.w700, ); + final custom = _nameOverride ?? widget.name; final real = _realName; final hasToggle = - widget.chatType == 'DIALOG' && real != null && real != widget.name; + widget.chatType == 'DIALOG' && real != null && real != custom; final nameSwap = AnimatedTextSwap( showAlternate: _showRealName, alignment: Alignment.center, alternate: Text( - real ?? widget.name, + real ?? custom, style: nameStyle, textAlign: TextAlign.center, ), - child: Text(widget.name, style: nameStyle, textAlign: TextAlign.center), + child: Text(custom, style: nameStyle, textAlign: TextAlign.center), ); if (!hasToggle) return nameSwap; diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 8d35e38..e398957 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -583,6 +583,7 @@ class _ChatListScreenState extends State storiesModule.storiesChanged.addListener(_onStoriesDataChanged); KometSettings.hideAllChatsFolder.addListener(_requestReload); KometSettings.showHiddenChats.addListener(_requestReload); + ContactsModule.revision.addListener(_requestReload); _maybeLoadStories(); _typingSub = api.pushStream .where((p) => p.opcode == Opcode.notifTyping) @@ -1228,6 +1229,7 @@ class _ChatListScreenState extends State storiesModule.storiesChanged.removeListener(_onStoriesDataChanged); KometSettings.hideAllChatsFolder.removeListener(_requestReload); KometSettings.showHiddenChats.removeListener(_requestReload); + ContactsModule.revision.removeListener(_requestReload); _loginSub?.cancel(); _stateSub?.cancel(); _typingSub?.cancel(); diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 5c8df93..79816d9 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -27,6 +27,7 @@ import '../../../main.dart'; import '../../../l10n/app_localizations.dart'; import '../../../backend/api.dart'; import '../../../backend/modules/messages.dart'; +import '../../../backend/modules/contacts.dart'; import '../../../backend/modules/animoji.dart'; import '../../../models/animoji.dart'; import '../../../backend/modules/complaints.dart'; @@ -632,6 +633,7 @@ class _ChatScreenState extends State }); debugForceOffline.addListener(_recomputeHeaderStatus); PresenceFetch.revision.addListener(_onPresenceChanged); + ContactsModule.revision.addListener(_onContactsChanged); _floatingDateAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 220), @@ -987,7 +989,7 @@ class _ChatScreenState extends State MaterialPageRoute( builder: (_) => ChatInfoScreen( chatId: widget.chatId, - name: widget.name, + name: _headerName(), imageUrl: widget.imageUrl, chatType: widget.chatType, initialTab: initialTab, @@ -1823,6 +1825,7 @@ class _ChatScreenState extends State .listenable(widget.chatId) .removeListener(_recomputeHeaderStatus); PresenceFetch.revision.removeListener(_onPresenceChanged); + ContactsModule.revision.removeListener(_onContactsChanged); if (_wallpaperListening) { ChatWallpaperStore.instance.revision.removeListener( _applyEffectiveWallpaper, @@ -2794,6 +2797,22 @@ class _ChatScreenState extends State } } + void _onContactsChanged() { + if (mounted) setState(() {}); + } + + String _headerName() { + if (_commentsMode) return AppLocalizations.of(context)!.commentsTitle; + if (widget.chatType == 'DIALOG') { + final otherId = _resolveOtherId(); + if (otherId != null) { + final cached = ContactCache.get(otherId); + if (cached != null && cached.isNotEmpty) return cached; + } + } + return widget.name; + } + PreferredSizeWidget _buildAppBar(ColorScheme cs) { final glossy = AppVisualStyle.current.value.glossyChrome; final searchT = Curves.easeOut.transform(_searchAnim.value.clamp(0.0, 1.0)); @@ -2879,9 +2898,7 @@ class _ChatScreenState extends State cs: cs, embedded: widget.embedded, chatId: widget.chatId, - name: _commentsMode - ? AppLocalizations.of(context)!.commentsTitle - : widget.name, + name: _headerName(), imageUrl: widget.imageUrl, chatType: widget.chatType, isOfficial: chat?.isOfficial ?? false, diff --git a/lib/frontend/screens/contacts/edit_contact_sheet.dart b/lib/frontend/screens/contacts/edit_contact_sheet.dart new file mode 100644 index 0000000..0a0374b --- /dev/null +++ b/lib/frontend/screens/contacts/edit_contact_sheet.dart @@ -0,0 +1,346 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:komet/backend/modules/contacts.dart'; +import 'package:komet/frontend/widgets/custom_notification.dart'; +import 'package:komet/frontend/widgets/komet_avatar.dart'; +import 'package:komet/l10n/app_localizations.dart'; +import 'package:komet/main.dart'; + +enum EditContactAction { updated, removed } + +class EditContactResult { + final EditContactAction action; + final String firstName; + final String lastName; + + const EditContactResult( + this.action, { + this.firstName = '', + this.lastName = '', + }); +} + +Future showEditContactSheet( + BuildContext context, { + required int contactId, + required String avatarUrl, + required String customFirst, + required String customLast, + required String onemeFirst, + required String onemeLast, +}) { + return showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: Colors.black.withValues(alpha: 0.28), + transitionDuration: const Duration(milliseconds: 260), + pageBuilder: (_, _, _) => const SizedBox.shrink(), + transitionBuilder: (_, anim, _, _) { + final t = Curves.easeOutCubic.transform(anim.value); + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 14 * t, sigmaY: 14 * t), + child: Opacity( + opacity: anim.value, + child: Transform.scale( + scale: 0.94 + 0.06 * t, + child: _EditContactCard( + contactId: contactId, + avatarUrl: avatarUrl, + customFirst: customFirst, + customLast: customLast, + onemeFirst: onemeFirst, + onemeLast: onemeLast, + ), + ), + ), + ); + }, + ); +} + +class _EditContactCard extends StatefulWidget { + final int contactId; + final String avatarUrl; + final String customFirst; + final String customLast; + final String onemeFirst; + final String onemeLast; + + const _EditContactCard({ + required this.contactId, + required this.avatarUrl, + required this.customFirst, + required this.customLast, + required this.onemeFirst, + required this.onemeLast, + }); + + @override + State<_EditContactCard> createState() => _EditContactCardState(); +} + +class _EditContactCardState extends State<_EditContactCard> { + late final TextEditingController _firstCtrl; + late final TextEditingController _lastCtrl; + + bool _saving = false; + bool _deleting = false; + + @override + void initState() { + super.initState(); + _firstCtrl = TextEditingController(text: widget.customFirst); + _lastCtrl = TextEditingController(text: widget.customLast); + } + + @override + void dispose() { + _firstCtrl.dispose(); + _lastCtrl.dispose(); + super.dispose(); + } + + bool get _busy => _saving || _deleting; + + bool get _dirty => + _firstCtrl.text.trim() != widget.customFirst.trim() || + _lastCtrl.text.trim() != widget.customLast.trim(); + + Future _save() async { + if (!_dirty || _busy) return; + setState(() => _saving = true); + + final first = _firstCtrl.text.trim(); + final last = _lastCtrl.text.trim(); + final sendFirst = first.isEmpty ? widget.onemeFirst : first; + final sendLast = last.isEmpty ? widget.onemeLast : last; + + final updated = await ContactsModule.updateContact( + api, + contactId: widget.contactId, + firstName: sendFirst, + lastName: sendLast, + ); + if (!mounted) return; + + if (updated == null) { + setState(() => _saving = false); + showCustomNotification( + context, + AppLocalizations.of(context)!.editContactError, + ); + return; + } + + Navigator.of(context).pop( + EditContactResult( + EditContactAction.updated, + firstName: updated.firstName, + lastName: updated.lastName ?? '', + ), + ); + } + + Future _delete() async { + if (_busy) return; + final l10n = AppLocalizations.of(context)!; + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.editContactDeleteConfirmTitle), + content: Text(l10n.editContactDeleteConfirmBody), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: Text(l10n.editContactDeleteCancel), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: Text(l10n.editContactDelete), + ), + ], + ), + ); + if (confirmed != true || !mounted) return; + + setState(() => _deleting = true); + final ok = await ContactsModule.removeContact(api, widget.contactId); + if (!mounted) return; + + if (!ok) { + setState(() => _deleting = false); + showCustomNotification(context, l10n.editContactError); + return; + } + + Navigator.of(context).pop( + const EditContactResult(EditContactAction.removed), + ); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + final width = MediaQuery.sizeOf(context).width; + final avatarName = widget.customFirst.isNotEmpty + ? widget.customFirst + : widget.onemeFirst; + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Material( + color: Colors.transparent, + child: Container( + width: width > 420 ? 380 : double.infinity, + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(22), + ), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: KometAvatar( + name: avatarName, + size: 88, + imageUrl: widget.avatarUrl.isEmpty + ? null + : widget.avatarUrl, + ), + ), + const SizedBox(height: 20), + _inputRow( + cs, + controller: _firstCtrl, + hint: l10n.editContactFirstName, + ), + _divider(cs), + _inputRow( + cs, + controller: _lastCtrl, + hint: l10n.editContactLastName, + ), + AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + child: _dirty + ? Padding( + padding: const EdgeInsets.only(top: 12), + child: _saveButton(cs, l10n), + ) + : const SizedBox(width: double.infinity), + ), + const SizedBox(height: 4), + _deleteButton(cs, l10n), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _inputRow( + ColorScheme cs, { + required TextEditingController controller, + required String hint, + }) { + final hasText = controller.text.isNotEmpty; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + maxLength: 60, + enabled: !_busy, + onChanged: (_) => setState(() {}), + style: TextStyle(color: cs.onSurface, fontSize: 16), + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + counterText: '', + hintText: hint, + hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), + ), + ), + if (hasText) + InkWell( + onTap: _busy + ? null + : () => setState(() => controller.clear()), + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + Icons.close, + size: 18, + color: cs.onSurfaceVariant, + ), + ), + ), + ], + ), + ); + } + + Widget _saveButton(ColorScheme cs, AppLocalizations l10n) { + return SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _busy ? null : _save, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: _saving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text( + l10n.editContactSave, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ); + } + + Widget _deleteButton(ColorScheme cs, AppLocalizations l10n) { + return SizedBox( + width: double.infinity, + child: TextButton( + onPressed: _busy ? null : _delete, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + foregroundColor: cs.error, + ), + child: _deleting + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: cs.error), + ) + : Text( + l10n.editContactDelete, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ); + } + + Widget _divider(ColorScheme cs) => + Divider(height: 1, thickness: 0.5, color: cs.outlineVariant); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 69fe0ba..3fa78a1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -934,5 +934,15 @@ }, "addContactNotFoundSubtitle": "This number isn't on the app yet", "addContactSearchOther": "Search for other number", - "addContactError": "Couldn't add contact" + "addContactError": "Couldn't add contact", + "editContactMenu": "Edit contact", + "editContactTitle": "Edit contact", + "editContactFirstName": "First name", + "editContactLastName": "Last name", + "editContactSave": "Save", + "editContactDelete": "Delete contact", + "editContactDeleteConfirmTitle": "Delete contact?", + "editContactDeleteConfirmBody": "This contact will be removed from your list.", + "editContactDeleteCancel": "Cancel", + "editContactError": "Couldn't save changes" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c96cd1a..118922c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4123,6 +4123,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Couldn\'t add contact'** String get addContactError; + + /// No description provided for @editContactMenu. + /// + /// In en, this message translates to: + /// **'Edit contact'** + String get editContactMenu; + + /// No description provided for @editContactTitle. + /// + /// In en, this message translates to: + /// **'Edit contact'** + String get editContactTitle; + + /// No description provided for @editContactFirstName. + /// + /// In en, this message translates to: + /// **'First name'** + String get editContactFirstName; + + /// No description provided for @editContactLastName. + /// + /// In en, this message translates to: + /// **'Last name'** + String get editContactLastName; + + /// No description provided for @editContactSave. + /// + /// In en, this message translates to: + /// **'Save'** + String get editContactSave; + + /// No description provided for @editContactDelete. + /// + /// In en, this message translates to: + /// **'Delete contact'** + String get editContactDelete; + + /// No description provided for @editContactDeleteConfirmTitle. + /// + /// In en, this message translates to: + /// **'Delete contact?'** + String get editContactDeleteConfirmTitle; + + /// No description provided for @editContactDeleteConfirmBody. + /// + /// In en, this message translates to: + /// **'This contact will be removed from your list.'** + String get editContactDeleteConfirmBody; + + /// No description provided for @editContactDeleteCancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get editContactDeleteCancel; + + /// No description provided for @editContactError. + /// + /// In en, this message translates to: + /// **'Couldn\'t save changes'** + String get editContactError; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 59867d9..e7be3b4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2148,4 +2148,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addContactError => 'Couldn\'t add contact'; + + @override + String get editContactMenu => 'Edit contact'; + + @override + String get editContactTitle => 'Edit contact'; + + @override + String get editContactFirstName => 'First name'; + + @override + String get editContactLastName => 'Last name'; + + @override + String get editContactSave => 'Save'; + + @override + String get editContactDelete => 'Delete contact'; + + @override + String get editContactDeleteConfirmTitle => 'Delete contact?'; + + @override + String get editContactDeleteConfirmBody => + 'This contact will be removed from your list.'; + + @override + String get editContactDeleteCancel => 'Cancel'; + + @override + String get editContactError => 'Couldn\'t save changes'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5e9f539..4ebe91a 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2161,4 +2161,35 @@ class AppLocalizationsRu extends AppLocalizations { @override String get addContactError => 'Не удалось добавить контакт'; + + @override + String get editContactMenu => 'Редактировать контакт'; + + @override + String get editContactTitle => 'Редактировать контакт'; + + @override + String get editContactFirstName => 'Имя'; + + @override + String get editContactLastName => 'Фамилия'; + + @override + String get editContactSave => 'Сохранить'; + + @override + String get editContactDelete => 'Удалить контакт'; + + @override + String get editContactDeleteConfirmTitle => 'Удалить контакт?'; + + @override + String get editContactDeleteConfirmBody => + 'Контакт будет удалён из вашего списка.'; + + @override + String get editContactDeleteCancel => 'Отмена'; + + @override + String get editContactError => 'Не удалось сохранить изменения'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 79593f5..a05d607 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -709,5 +709,15 @@ }, "addContactNotFoundSubtitle": "Этого номера пока нет в приложении", "addContactSearchOther": "Искать другой номер", - "addContactError": "Не удалось добавить контакт" + "addContactError": "Не удалось добавить контакт", + "editContactMenu": "Редактировать контакт", + "editContactTitle": "Редактировать контакт", + "editContactFirstName": "Имя", + "editContactLastName": "Фамилия", + "editContactSave": "Сохранить", + "editContactDelete": "Удалить контакт", + "editContactDeleteConfirmTitle": "Удалить контакт?", + "editContactDeleteConfirmBody": "Контакт будет удалён из вашего списка.", + "editContactDeleteCancel": "Отмена", + "editContactError": "Не удалось сохранить изменения" }