feat: изменение контакта
This commit is contained in:
@@ -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<CachedContact?> 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<dynamic, dynamic>()
|
||||
: 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<bool> 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<String, dynamic>.from(info.raw)..['names'] = stripped;
|
||||
ContactInfoFetch.putContact(contactId, newRaw);
|
||||
primeContactCache(newRaw);
|
||||
} else {
|
||||
ContactInfoFetch.invalidate(contactId);
|
||||
}
|
||||
|
||||
revision.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<void> syncFromLoginPayload(
|
||||
Map<dynamic, dynamic> data,
|
||||
int accountId,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Vendored
+4
@@ -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<dynamic, dynamic> contact) {
|
||||
_cache.putValue(id, ContactInfo.fromMap(Map<String, dynamic>.from(contact)));
|
||||
}
|
||||
|
||||
static Future<ContactInfo?> _fetch(int id) async {
|
||||
final api = _api;
|
||||
if (api == null || api.state != SessionState.online) return null;
|
||||
|
||||
@@ -883,6 +883,15 @@ class AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> deleteContact(int accountId, int id) async {
|
||||
final db = await _instance;
|
||||
await db.delete(
|
||||
'contacts',
|
||||
where: 'account_id = ? AND id = ?',
|
||||
whereArgs: [accountId, id],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> saveMessages(List<Map<String, dynamic>> rows) async {
|
||||
final db = await _instance;
|
||||
await db.transaction((txn) async {
|
||||
|
||||
@@ -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<ChatInfoScreen> {
|
||||
String _selectedTab = '';
|
||||
bool _descExpanded = false;
|
||||
bool _showRealName = false;
|
||||
String? _nameOverride;
|
||||
|
||||
int? _otherId;
|
||||
ContactInfo? _contactData;
|
||||
@@ -275,12 +277,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
|
||||
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<ChatInfoScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
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<String>(
|
||||
icon: Icon(Icons.more_vert, color: cs.onSurface),
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') _openEdit();
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.edit, size: 20, color: cs.onSurface),
|
||||
const SizedBox(width: 12),
|
||||
Text(l10n.editContactMenu),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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
|
||||
? <ContactName>[]
|
||||
: data.names.where((n) => n.type != 'CUSTOM').toList();
|
||||
setState(() {
|
||||
_contactData = ContactInfo(
|
||||
raw: data?.raw ?? const <String, dynamic>{},
|
||||
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<ChatInfoScreen> {
|
||||
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;
|
||||
|
||||
@@ -583,6 +583,7 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
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<ChatListScreen>
|
||||
storiesModule.storiesChanged.removeListener(_onStoriesDataChanged);
|
||||
KometSettings.hideAllChatsFolder.removeListener(_requestReload);
|
||||
KometSettings.showHiddenChats.removeListener(_requestReload);
|
||||
ContactsModule.revision.removeListener(_requestReload);
|
||||
_loginSub?.cancel();
|
||||
_stateSub?.cancel();
|
||||
_typingSub?.cancel();
|
||||
|
||||
@@ -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<ChatScreen>
|
||||
});
|
||||
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<ChatScreen>
|
||||
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<ChatScreen>
|
||||
.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<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
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<ChatScreen>
|
||||
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,
|
||||
|
||||
@@ -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<EditContactResult?> showEditContactSheet(
|
||||
BuildContext context, {
|
||||
required int contactId,
|
||||
required String avatarUrl,
|
||||
required String customFirst,
|
||||
required String customLast,
|
||||
required String onemeFirst,
|
||||
required String onemeLast,
|
||||
}) {
|
||||
return showGeneralDialog<EditContactResult>(
|
||||
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<void> _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<void> _delete() async {
|
||||
if (_busy) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showDialog<bool>(
|
||||
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);
|
||||
}
|
||||
+11
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 => 'Не удалось сохранить изменения';
|
||||
}
|
||||
|
||||
+11
-1
@@ -709,5 +709,15 @@
|
||||
},
|
||||
"addContactNotFoundSubtitle": "Этого номера пока нет в приложении",
|
||||
"addContactSearchOther": "Искать другой номер",
|
||||
"addContactError": "Не удалось добавить контакт"
|
||||
"addContactError": "Не удалось добавить контакт",
|
||||
"editContactMenu": "Редактировать контакт",
|
||||
"editContactTitle": "Редактировать контакт",
|
||||
"editContactFirstName": "Имя",
|
||||
"editContactLastName": "Фамилия",
|
||||
"editContactSave": "Сохранить",
|
||||
"editContactDelete": "Удалить контакт",
|
||||
"editContactDeleteConfirmTitle": "Удалить контакт?",
|
||||
"editContactDeleteConfirmBody": "Контакт будет удалён из вашего списка.",
|
||||
"editContactDeleteCancel": "Отмена",
|
||||
"editContactError": "Не удалось сохранить изменения"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user