добавлены анимации на экране сообщений, добавлено редактирование профиля (локально), изменена панель сообщений
добавлен баг с незагрузкой аватарок в чатах
This commit is contained in:
BIN
android/app/src/main/jniLibs/arm64-v8a/libeslz4-android64.so
Normal file
BIN
android/app/src/main/jniLibs/arm64-v8a/libeslz4-android64.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/arm64-v8a/liblz4.so
Normal file
BIN
android/app/src/main/jniLibs/arm64-v8a/liblz4.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/armeabi-v7a/libeslz4-android32.so
Normal file
BIN
android/app/src/main/jniLibs/armeabi-v7a/libeslz4-android32.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/armeabi-v7a/liblz4.so
Normal file
BIN
android/app/src/main/jniLibs/armeabi-v7a/liblz4.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86/libeslz4-android32.so
Normal file
BIN
android/app/src/main/jniLibs/x86/libeslz4-android32.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86/liblz4.so
Normal file
BIN
android/app/src/main/jniLibs/x86/liblz4.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86_64/libeslz4-android64.so
Normal file
BIN
android/app/src/main/jniLibs/x86_64/libeslz4-android64.so
Normal file
Binary file not shown.
BIN
android/app/src/main/jniLibs/x86_64/liblz4.so
Normal file
BIN
android/app/src/main/jniLibs/x86_64/liblz4.so
Normal file
Binary file not shown.
@@ -18,6 +18,7 @@ import 'package:gwid/services/account_manager.dart';
|
|||||||
import 'package:gwid/services/avatar_cache_service.dart';
|
import 'package:gwid/services/avatar_cache_service.dart';
|
||||||
import 'package:gwid/services/cache_service.dart';
|
import 'package:gwid/services/cache_service.dart';
|
||||||
import 'package:gwid/services/chat_cache_service.dart';
|
import 'package:gwid/services/chat_cache_service.dart';
|
||||||
|
import 'package:gwid/services/profile_cache_service.dart';
|
||||||
import 'package:gwid/spoofing_service.dart';
|
import 'package:gwid/spoofing_service.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|||||||
@@ -69,6 +69,37 @@ extension ApiServiceChats on ApiService {
|
|||||||
_sendInitialSetupRequests();
|
_sendInitialSetupRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile != null && authToken != null) {
|
||||||
|
try {
|
||||||
|
final accountManager = AccountManager();
|
||||||
|
await accountManager.initialize();
|
||||||
|
final currentAccount = accountManager.currentAccount;
|
||||||
|
if (currentAccount != null && currentAccount.token == authToken) {
|
||||||
|
final profileObj = Profile.fromJson(profile);
|
||||||
|
await accountManager.updateAccountProfile(
|
||||||
|
currentAccount.id,
|
||||||
|
profileObj,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profileCache = ProfileCacheService();
|
||||||
|
await profileCache.initialize();
|
||||||
|
await profileCache.syncWithServerProfile(profileObj);
|
||||||
|
} catch (e) {
|
||||||
|
print('[ProfileCache] Ошибка синхронизации профиля: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
print(
|
||||||
|
'[_sendAuthRequestAfterHandshake] ✅ Профиль сохранен в AccountManager',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'[_sendAuthRequestAfterHandshake] Ошибка сохранения профиля в AccountManager: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) {
|
if (_onlineCompleter != null && !_onlineCompleter!.isCompleted) {
|
||||||
_onlineCompleter!.complete();
|
_onlineCompleter!.complete();
|
||||||
}
|
}
|
||||||
@@ -95,9 +126,10 @@ extension ApiServiceChats on ApiService {
|
|||||||
};
|
};
|
||||||
_lastChatsPayload = result;
|
_lastChatsPayload = result;
|
||||||
|
|
||||||
final contacts = contactListJson
|
final contacts = (contactListJson as List)
|
||||||
.map((json) => Contact.fromJson(json))
|
.map((json) => Contact.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList()
|
||||||
|
.cast<Contact>();
|
||||||
updateContactCache(contacts);
|
updateContactCache(contacts);
|
||||||
_lastChatsAt = DateTime.now();
|
_lastChatsAt = DateTime.now();
|
||||||
_preloadContactAvatars(contacts);
|
_preloadContactAvatars(contacts);
|
||||||
@@ -241,7 +273,11 @@ extension ApiServiceChats on ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final payload = {"chatsCount": 100};
|
// Используем opcode 48 для запроса конкретных чатов
|
||||||
|
// chatIds:[0] - это "Избранное" (Saved Messages)
|
||||||
|
final payload = {
|
||||||
|
"chatIds": [0],
|
||||||
|
};
|
||||||
|
|
||||||
final int chatSeq = _sendMessage(48, payload);
|
final int chatSeq = _sendMessage(48, payload);
|
||||||
final chatResponse = await messages.firstWhere(
|
final chatResponse = await messages.firstWhere(
|
||||||
@@ -265,15 +301,16 @@ extension ApiServiceChats on ApiService {
|
|||||||
contactIds.addAll(participants.keys.map((id) => int.parse(id)));
|
contactIds.addAll(participants.keys.map((id) => int.parse(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<dynamic> contactListJson = [];
|
||||||
|
if (contactIds.isNotEmpty) {
|
||||||
final int contactSeq = _sendMessage(32, {
|
final int contactSeq = _sendMessage(32, {
|
||||||
"contactIds": contactIds.toList(),
|
"contactIds": contactIds.toList(),
|
||||||
});
|
});
|
||||||
final contactResponse = await messages.firstWhere(
|
final contactResponse = await messages.firstWhere(
|
||||||
(msg) => msg['seq'] == contactSeq,
|
(msg) => msg['seq'] == contactSeq,
|
||||||
);
|
);
|
||||||
|
contactListJson = contactResponse['payload']?['contacts'] ?? [];
|
||||||
final List<dynamic> contactListJson =
|
}
|
||||||
contactResponse['payload']?['contacts'] ?? [];
|
|
||||||
|
|
||||||
final result = {
|
final result = {
|
||||||
'chats': chatListJson,
|
'chats': chatListJson,
|
||||||
@@ -283,8 +320,8 @@ extension ApiServiceChats on ApiService {
|
|||||||
};
|
};
|
||||||
_lastChatsPayload = result;
|
_lastChatsPayload = result;
|
||||||
|
|
||||||
final contacts = contactListJson
|
final List<Contact> contacts = contactListJson
|
||||||
.map((json) => Contact.fromJson(json))
|
.map((json) => Contact.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
updateContactCache(contacts);
|
updateContactCache(contacts);
|
||||||
_lastChatsAt = DateTime.now();
|
_lastChatsAt = DateTime.now();
|
||||||
@@ -295,7 +332,7 @@ extension ApiServiceChats on ApiService {
|
|||||||
unawaited(_chatCacheService.cacheContacts(contacts));
|
unawaited(_chatCacheService.cacheContacts(contacts));
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Ошибка получения чатов: $e');
|
print('Ошибка получения чатов через opcode 48: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,6 +482,14 @@ extension ApiServiceChats on ApiService {
|
|||||||
currentAccount.id,
|
currentAccount.id,
|
||||||
profileObj,
|
profileObj,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profileCache = ProfileCacheService();
|
||||||
|
await profileCache.initialize();
|
||||||
|
await profileCache.syncWithServerProfile(profileObj);
|
||||||
|
} catch (e) {
|
||||||
|
print('[ProfileCache] Ошибка синхронизации профиля: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Ошибка сохранения профиля в AccountManager: $e');
|
print('Ошибка сохранения профиля в AccountManager: $e');
|
||||||
@@ -510,8 +555,8 @@ extension ApiServiceChats on ApiService {
|
|||||||
};
|
};
|
||||||
_lastChatsPayload = result;
|
_lastChatsPayload = result;
|
||||||
|
|
||||||
final contacts = contactListJson
|
final List<Contact> contacts = contactListJson
|
||||||
.map((json) => Contact.fromJson(json))
|
.map((json) => Contact.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
updateContactCache(contacts);
|
updateContactCache(contacts);
|
||||||
_lastChatsAt = DateTime.now();
|
_lastChatsAt = DateTime.now();
|
||||||
|
|||||||
@@ -679,6 +679,7 @@ extension ApiServiceConnection on ApiService {
|
|||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
_isSessionOnline = false;
|
_isSessionOnline = false;
|
||||||
_isSessionReady = false;
|
_isSessionReady = false;
|
||||||
|
_handshakeSent = false;
|
||||||
_onlineCompleter = Completer<void>();
|
_onlineCompleter = Completer<void>();
|
||||||
_chatsFetchedInThisSession = false;
|
_chatsFetchedInThisSession = false;
|
||||||
|
|
||||||
|
|||||||
1070
lib/chat_screen.dart
1070
lib/chat_screen.dart
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,10 @@ import 'package:gwid/user_id_lookup_screen.dart';
|
|||||||
import 'package:gwid/screens/music_library_screen.dart';
|
import 'package:gwid/screens/music_library_screen.dart';
|
||||||
import 'package:gwid/widgets/message_preview_dialog.dart';
|
import 'package:gwid/widgets/message_preview_dialog.dart';
|
||||||
import 'package:gwid/services/chat_read_settings_service.dart';
|
import 'package:gwid/services/chat_read_settings_service.dart';
|
||||||
|
import 'package:gwid/services/local_profile_manager.dart';
|
||||||
|
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||||
|
import 'package:gwid/widgets/contact_avatar_widget.dart';
|
||||||
|
import 'package:gwid/services/contact_local_names_service.dart';
|
||||||
import 'package:gwid/services/account_manager.dart';
|
import 'package:gwid/services/account_manager.dart';
|
||||||
import 'package:gwid/models/account.dart';
|
import 'package:gwid/models/account.dart';
|
||||||
|
|
||||||
@@ -163,25 +167,48 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
_isProfileLoading = true;
|
_isProfileLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Profile? serverProfile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final accountManager = AccountManager();
|
final accountManager = AccountManager();
|
||||||
await accountManager.initialize();
|
await accountManager.initialize();
|
||||||
final currentAccount = accountManager.currentAccount;
|
final currentAccount = accountManager.currentAccount;
|
||||||
if (currentAccount?.profile != null && mounted) {
|
if (currentAccount?.profile != null) {
|
||||||
setState(() {
|
serverProfile = currentAccount!.profile;
|
||||||
_myProfile = currentAccount!.profile;
|
|
||||||
_isProfileLoading = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Ошибка загрузки профиля из AccountManager: $e');
|
print('Ошибка загрузки профиля из AccountManager: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
final cachedProfileData = ApiService.instance.lastChatsPayload?['profile'];
|
if (serverProfile == null) {
|
||||||
if (cachedProfileData != null && mounted) {
|
final cachedProfileData =
|
||||||
|
ApiService.instance.lastChatsPayload?['profile'];
|
||||||
|
if (cachedProfileData != null) {
|
||||||
|
serverProfile = Profile.fromJson(cachedProfileData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profileManager = LocalProfileManager();
|
||||||
|
await profileManager.initialize();
|
||||||
|
final actualProfile = await profileManager.getActualProfile(
|
||||||
|
serverProfile,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted && actualProfile != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_myProfile = Profile.fromJson(cachedProfileData);
|
_myProfile = actualProfile;
|
||||||
|
_isProfileLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Ошибка загрузки локального профиля: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted && serverProfile != null) {
|
||||||
|
setState(() {
|
||||||
|
_myProfile = serverProfile;
|
||||||
_isProfileLoading = false;
|
_isProfileLoading = false;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -277,6 +304,20 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
final opcode = message['opcode'];
|
final opcode = message['opcode'];
|
||||||
final cmd = message['cmd'];
|
final cmd = message['cmd'];
|
||||||
final payload = message['payload'];
|
final payload = message['payload'];
|
||||||
|
|
||||||
|
if (opcode == 19 && cmd == 1 && payload != null) {
|
||||||
|
final profileData = payload['profile'];
|
||||||
|
if (profileData != null) {
|
||||||
|
print('🔄 ChatsScreen: Получен профиль из opcode 19, обновляем UI');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_myProfile = Profile.fromJson(profileData);
|
||||||
|
_isProfileLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (payload == null) return;
|
if (payload == null) return;
|
||||||
final chatIdValue = payload['chatId'];
|
final chatIdValue = payload['chatId'];
|
||||||
final int? chatId = chatIdValue != null ? chatIdValue as int? : null;
|
final int? chatId = chatIdValue != null ? chatIdValue as int? : null;
|
||||||
@@ -640,6 +681,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
final chats = data['chats'] as List<dynamic>;
|
final chats = data['chats'] as List<dynamic>;
|
||||||
final contacts = data['contacts'] as List<dynamic>;
|
final contacts = data['contacts'] as List<dynamic>;
|
||||||
|
final profileData = data['profile'];
|
||||||
|
|
||||||
_allChats = chats
|
_allChats = chats
|
||||||
.where((json) => json != null)
|
.where((json) => json != null)
|
||||||
@@ -650,6 +692,14 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
final contact = Contact.fromJson(contactJson);
|
final contact = Contact.fromJson(contactJson);
|
||||||
_contacts[contact.id] = contact;
|
_contacts[contact.id] = contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (profileData != null) {
|
||||||
|
_myProfile = Profile.fromJson(profileData);
|
||||||
|
_isProfileLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_filterChats();
|
_filterChats();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1061,7 +1111,14 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
final isSelected = selectedContacts.contains(contact.id);
|
final isSelected = selectedContacts.contains(contact.id);
|
||||||
|
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
title: Text(contact.name),
|
title: Text(
|
||||||
|
getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
),
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
contact.firstName.isNotEmpty &&
|
contact.firstName.isNotEmpty &&
|
||||||
contact.lastName.isNotEmpty
|
contact.lastName.isNotEmpty
|
||||||
@@ -1387,12 +1444,20 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
|
|
||||||
if (contact == null) continue;
|
if (contact == null) continue;
|
||||||
|
|
||||||
if (contact.name.toLowerCase().contains(query)) {
|
final displayName = getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (displayName.toLowerCase().contains(query) ||
|
||||||
|
contact.name.toLowerCase().contains(query)) {
|
||||||
results.add(
|
results.add(
|
||||||
SearchResult(
|
SearchResult(
|
||||||
chat: chat,
|
chat: chat,
|
||||||
contact: contact,
|
contact: contact,
|
||||||
matchedText: contact.name,
|
matchedText: displayName,
|
||||||
matchType: 'name',
|
matchType: 'name',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1465,6 +1530,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
final chats = data['chats'] as List;
|
final chats = data['chats'] as List;
|
||||||
final contacts = data['contacts'] as List;
|
final contacts = data['contacts'] as List;
|
||||||
|
final profileData = data['profile'];
|
||||||
|
|
||||||
_allChats = chats
|
_allChats = chats
|
||||||
.where((json) => json != null)
|
.where((json) => json != null)
|
||||||
@@ -1477,6 +1543,13 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
_contacts[contact.id] = contact;
|
_contacts[contact.id] = contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileData != null) {
|
||||||
|
setState(() {
|
||||||
|
_myProfile = Profile.fromJson(profileData);
|
||||||
|
_isProfileLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_filterChats();
|
_filterChats();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1710,7 +1783,9 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
ApiService.instance.getBlockedContacts();
|
ApiService.instance.getBlockedContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_loadFolders(snapshot.data!);
|
_loadFolders(snapshot.data!);
|
||||||
|
});
|
||||||
|
|
||||||
_loadChatOrder().then((_) {
|
_loadChatOrder().then((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -2285,7 +2360,15 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
title: _buildHighlightedText(contact.name, result.matchedText),
|
title: _buildHighlightedText(
|
||||||
|
getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
),
|
||||||
|
result.matchedText,
|
||||||
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -2394,7 +2477,12 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
} else if (isSavedMessages) {
|
} else if (isSavedMessages) {
|
||||||
title = "Избранное";
|
title = "Избранное";
|
||||||
} else if (contact != null) {
|
} else if (contact != null) {
|
||||||
title = contact.name;
|
title = getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
);
|
||||||
} else if (chat.title?.isNotEmpty == true) {
|
} else if (chat.title?.isNotEmpty == true) {
|
||||||
title = chat.title!;
|
title = chat.title!;
|
||||||
} else {
|
} else {
|
||||||
@@ -2453,38 +2541,40 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
children: [
|
children: [
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
isSavedMessages || isGroupChat
|
||||||
|
? CircleAvatar(
|
||||||
radius: 28,
|
radius: 28,
|
||||||
backgroundColor: colors.primaryContainer,
|
backgroundColor: colors.primaryContainer,
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
!isSavedMessages &&
|
isGroupChat && chat.baseIconUrl != null
|
||||||
!isGroupChat &&
|
|
||||||
contact?.photoBaseUrl != null
|
|
||||||
? NetworkImage(contact?.photoBaseUrl ?? '')
|
|
||||||
: (isGroupChat && chat.baseIconUrl != null)
|
|
||||||
? NetworkImage(chat.baseIconUrl ?? '')
|
? NetworkImage(chat.baseIconUrl ?? '')
|
||||||
: null,
|
: null,
|
||||||
child:
|
child:
|
||||||
isSavedMessages ||
|
isSavedMessages ||
|
||||||
(isGroupChat && chat.baseIconUrl == null)
|
(isGroupChat && chat.baseIconUrl == null)
|
||||||
? Icon(
|
? Icon(
|
||||||
isSavedMessages ? Icons.bookmark : Icons.group,
|
isSavedMessages
|
||||||
|
? Icons.bookmark
|
||||||
|
: Icons.group,
|
||||||
color: colors.onPrimaryContainer,
|
color: colors.onPrimaryContainer,
|
||||||
size: 20,
|
size: 20,
|
||||||
)
|
)
|
||||||
: (contact?.photoBaseUrl == null
|
: null,
|
||||||
? Text(
|
)
|
||||||
(contact != null &&
|
: contact != null
|
||||||
contact.name.isNotEmpty)
|
? ContactAvatarWidget(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalAvatarUrl: contact.photoBaseUrl,
|
||||||
|
radius: 28,
|
||||||
|
fallbackText: contact.name.isNotEmpty
|
||||||
? contact.name[0].toUpperCase()
|
? contact.name[0].toUpperCase()
|
||||||
: '?',
|
: '?',
|
||||||
style: TextStyle(
|
backgroundColor: colors.primaryContainer,
|
||||||
color: colors.onSurface,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: null),
|
: CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor: colors.primaryContainer,
|
||||||
|
child: const Text('?'),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (chat.newMessages > 0)
|
if (chat.newMessages > 0)
|
||||||
@@ -3823,7 +3913,12 @@ class _ChatsScreenState extends State<ChatsScreen>
|
|||||||
contact = _contacts[otherParticipantId];
|
contact = _contacts[otherParticipantId];
|
||||||
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
title = contact.name;
|
title = getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
);
|
||||||
} else if (chat.title?.isNotEmpty == true) {
|
} else if (chat.title?.isNotEmpty == true) {
|
||||||
title = chat.title!;
|
title = chat.title!;
|
||||||
} else {
|
} else {
|
||||||
@@ -4531,7 +4626,12 @@ class _AddChatsToFolderDialogState extends State<_AddChatsToFolderDialog> {
|
|||||||
contact = widget.contacts[otherParticipantId];
|
contact = widget.contacts[otherParticipantId];
|
||||||
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
title = contact.name;
|
title = getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
);
|
||||||
} else if (chat.title?.isNotEmpty == true) {
|
} else if (chat.title?.isNotEmpty == true) {
|
||||||
title = chat.title!;
|
title = chat.title!;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'connection_lifecycle_manager.dart';
|
|||||||
import 'services/cache_service.dart';
|
import 'services/cache_service.dart';
|
||||||
import 'services/avatar_cache_service.dart';
|
import 'services/avatar_cache_service.dart';
|
||||||
import 'services/chat_cache_service.dart';
|
import 'services/chat_cache_service.dart';
|
||||||
|
import 'services/contact_local_names_service.dart';
|
||||||
import 'services/version_checker.dart';
|
import 'services/version_checker.dart';
|
||||||
import 'services/account_manager.dart';
|
import 'services/account_manager.dart';
|
||||||
import 'services/music_player_service.dart';
|
import 'services/music_player_service.dart';
|
||||||
@@ -28,6 +29,7 @@ Future<void> main() async {
|
|||||||
await CacheService().initialize();
|
await CacheService().initialize();
|
||||||
await AvatarCacheService().initialize();
|
await AvatarCacheService().initialize();
|
||||||
await ChatCacheService().initialize();
|
await ChatCacheService().initialize();
|
||||||
|
await ContactLocalNamesService().initialize();
|
||||||
print("Сервисы кеширования инициализированы");
|
print("Сервисы кеширования инициализированы");
|
||||||
|
|
||||||
print("Инициализируем AccountManager...");
|
print("Инициализируем AccountManager...");
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/models/profile.dart';
|
import 'package:gwid/models/profile.dart';
|
||||||
import 'package:gwid/phone_entry_screen.dart';
|
import 'package:gwid/phone_entry_screen.dart';
|
||||||
|
import 'package:gwid/services/profile_cache_service.dart';
|
||||||
|
import 'package:gwid/services/local_profile_manager.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@@ -20,40 +21,98 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
late final TextEditingController _lastNameController;
|
late final TextEditingController _lastNameController;
|
||||||
late final TextEditingController _descriptionController;
|
late final TextEditingController _descriptionController;
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||||
|
final ProfileCacheService _profileCache = ProfileCacheService();
|
||||||
|
final LocalProfileManager _profileManager = LocalProfileManager();
|
||||||
|
|
||||||
|
Profile? _actualProfile;
|
||||||
|
String? _localAvatarPath;
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_firstNameController = TextEditingController(
|
_initializeProfileData();
|
||||||
text: widget.myProfile?.firstName ?? '',
|
|
||||||
);
|
|
||||||
_lastNameController = TextEditingController(
|
|
||||||
text: widget.myProfile?.lastName ?? '',
|
|
||||||
);
|
|
||||||
_descriptionController = TextEditingController(
|
|
||||||
text: widget.myProfile?.description ?? '',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _saveProfile() {
|
Future<void> _initializeProfileData() async {
|
||||||
|
await _profileManager.initialize();
|
||||||
|
|
||||||
|
_actualProfile = await _profileManager.getActualProfile(widget.myProfile);
|
||||||
|
|
||||||
|
_firstNameController = TextEditingController(
|
||||||
|
text: _actualProfile?.firstName ?? '',
|
||||||
|
);
|
||||||
|
_lastNameController = TextEditingController(
|
||||||
|
text: _actualProfile?.lastName ?? '',
|
||||||
|
);
|
||||||
|
_descriptionController = TextEditingController(
|
||||||
|
text: _actualProfile?.description ?? '',
|
||||||
|
);
|
||||||
|
final localPath = await _profileManager.getLocalAvatarPath();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = localPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveProfile() async {
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiService.instance.updateProfileText(
|
setState(() {
|
||||||
_firstNameController.text.trim(),
|
_isLoading = true;
|
||||||
_lastNameController.text.trim(),
|
});
|
||||||
_descriptionController.text.trim(),
|
|
||||||
|
try {
|
||||||
|
final firstName = _firstNameController.text.trim();
|
||||||
|
final lastName = _lastNameController.text.trim();
|
||||||
|
final description = _descriptionController.text.trim();
|
||||||
|
|
||||||
|
final userId = _actualProfile?.id ?? widget.myProfile?.id ?? 0;
|
||||||
|
final photoBaseUrl =
|
||||||
|
_actualProfile?.photoBaseUrl ?? widget.myProfile?.photoBaseUrl;
|
||||||
|
final photoId = _actualProfile?.photoId ?? widget.myProfile?.photoId ?? 0;
|
||||||
|
|
||||||
|
await _profileCache.saveProfileData(
|
||||||
|
userId: userId,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
description: description.isEmpty ? null : description,
|
||||||
|
photoBaseUrl: photoBaseUrl,
|
||||||
|
photoId: photoId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_actualProfile = await _profileManager.getActualProfile(widget.myProfile);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text("Профиль успешно сохранен"),
|
content: Text("Профиль сохранен локально"),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
duration: Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Ошибка сохранения: $e"),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _logout() async {
|
void _logout() async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
@@ -102,28 +161,63 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pickAndUpdateProfilePhoto() async {
|
Future<void> _pickAndUpdateProfilePhoto() async {
|
||||||
|
try {
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
|
||||||
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
if (image == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
if (image != null) {
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
File imageFile = File(image.path);
|
File imageFile = File(image.path);
|
||||||
|
|
||||||
|
final userId = _actualProfile?.id ?? widget.myProfile?.id ?? 0;
|
||||||
|
if (userId != 0) {
|
||||||
|
final localPath = await _profileCache.saveAvatar(imageFile, userId);
|
||||||
|
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = localPath;
|
||||||
|
});
|
||||||
|
_actualProfile = await _profileManager.getActualProfile(
|
||||||
|
widget.myProfile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text("Фотография профиля обновляется..."),
|
content: Text("Фотография профиля сохранена"),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Ошибка загрузки фото: $e"),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -155,7 +249,6 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
_buildAvatarSection(theme),
|
_buildAvatarSection(theme),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -199,7 +292,6 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -234,7 +326,6 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
|
||||||
if (widget.myProfile != null)
|
if (widget.myProfile != null)
|
||||||
Card(
|
Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@@ -281,22 +372,32 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildAvatarSection(ThemeData theme) {
|
Widget _buildAvatarSection(ThemeData theme) {
|
||||||
|
ImageProvider? avatarImage;
|
||||||
|
|
||||||
|
if (_localAvatarPath != null) {
|
||||||
|
avatarImage = FileImage(File(_localAvatarPath!));
|
||||||
|
} else if (_actualProfile?.photoBaseUrl != null) {
|
||||||
|
if (_actualProfile!.photoBaseUrl!.startsWith('file://')) {
|
||||||
|
final path = _actualProfile!.photoBaseUrl!.replaceFirst('file://', '');
|
||||||
|
avatarImage = FileImage(File(path));
|
||||||
|
} else {
|
||||||
|
avatarImage = NetworkImage(_actualProfile!.photoBaseUrl!);
|
||||||
|
}
|
||||||
|
} else if (widget.myProfile?.photoBaseUrl != null) {
|
||||||
|
avatarImage = NetworkImage(widget.myProfile!.photoBaseUrl!);
|
||||||
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
onTap: _pickAndUpdateProfilePhoto,
|
||||||
onTap: _pickAndUpdateProfilePhoto, // 2. Вызываем метод при нажатии
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 60,
|
radius: 60,
|
||||||
backgroundColor: theme.colorScheme.secondaryContainer,
|
backgroundColor: theme.colorScheme.secondaryContainer,
|
||||||
backgroundImage: widget.myProfile?.photoBaseUrl != null
|
backgroundImage: avatarImage,
|
||||||
? NetworkImage(widget.myProfile!.photoBaseUrl!)
|
child: avatarImage == null
|
||||||
: null,
|
|
||||||
child: widget.myProfile?.photoBaseUrl == null
|
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
size: 60,
|
size: 60,
|
||||||
@@ -304,6 +405,21 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
if (_isLoading)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
@@ -329,7 +445,6 @@ class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
|||||||
IconData icon, {
|
IconData icon, {
|
||||||
bool alignLabel = false,
|
bool alignLabel = false,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
final prefixIcon = (label == "О себе")
|
final prefixIcon = (label == "О себе")
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 60), // Смещаем иконку вверх
|
padding: const EdgeInsets.only(bottom: 60), // Смещаем иконку вверх
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/otp_screen.dart';
|
import 'package:gwid/otp_screen.dart';
|
||||||
import 'package:gwid/proxy_service.dart';
|
import 'package:gwid/proxy_service.dart';
|
||||||
import 'package:gwid/screens/settings/proxy_settings_screen.dart';
|
import 'package:gwid/screens/settings/auth_settings_screen.dart';
|
||||||
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
|
||||||
import 'package:gwid/token_auth_screen.dart';
|
import 'package:gwid/token_auth_screen.dart';
|
||||||
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
|
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
|
||||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||||
@@ -219,10 +218,6 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshProxySettings() {
|
|
||||||
_checkProxySettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _requestOtp() async {
|
void _requestOtp() async {
|
||||||
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
@@ -429,9 +424,14 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
_AnonymityCard(isConfigured: _hasCustomAnonymity),
|
_SettingsButton(
|
||||||
const SizedBox(height: 16),
|
hasCustomAnonymity: _hasCustomAnonymity,
|
||||||
_ProxyCard(isConfigured: _hasProxyConfigured),
|
hasProxyConfigured: _hasProxyConfigured,
|
||||||
|
onRefresh: () {
|
||||||
|
_checkAnonymitySettings();
|
||||||
|
_checkProxySettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text.rich(
|
Text.rich(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -600,172 +600,159 @@ class _CountryPicker extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AnonymityCard extends StatelessWidget {
|
class _SettingsButton extends StatelessWidget {
|
||||||
final bool isConfigured;
|
final bool hasCustomAnonymity;
|
||||||
const _AnonymityCard({required this.isConfigured});
|
final bool hasProxyConfigured;
|
||||||
|
final VoidCallback onRefresh;
|
||||||
|
|
||||||
|
const _SettingsButton({
|
||||||
|
required this.hasCustomAnonymity,
|
||||||
|
required this.hasProxyConfigured,
|
||||||
|
required this.onRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
final Color cardColor = isConfigured
|
final hasAnySettings = hasCustomAnonymity || hasProxyConfigured;
|
||||||
? colors.secondaryContainer
|
|
||||||
: colors.surfaceContainerHighest.withOpacity(0.5);
|
|
||||||
final Color onCardColor = isConfigured
|
|
||||||
? colors.onSecondaryContainer
|
|
||||||
: colors.onSurfaceVariant;
|
|
||||||
final IconData icon = isConfigured
|
|
||||||
? Icons.verified_user_outlined
|
|
||||||
: Icons.visibility_outlined;
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
color: cardColor,
|
color: hasAnySettings
|
||||||
|
? colors.primaryContainer.withOpacity(0.3)
|
||||||
|
: colors.surfaceContainerHighest.withOpacity(0.5),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
side: BorderSide(color: colors.outline.withOpacity(0.5)),
|
side: BorderSide(
|
||||||
),
|
color: hasAnySettings
|
||||||
child: Padding(
|
? colors.primary.withOpacity(0.3)
|
||||||
padding: const EdgeInsets.all(16.0),
|
: colors.outline.withOpacity(0.3),
|
||||||
child: Column(
|
width: hasAnySettings ? 2 : 1,
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: onCardColor, size: 20),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
isConfigured
|
|
||||||
? 'Активны кастомные настройки анонимности'
|
|
||||||
: 'Настройте анонимность для скрытия данных',
|
|
||||||
style: GoogleFonts.manrope(
|
|
||||||
textStyle: textTheme.bodyMedium,
|
|
||||||
color: onCardColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: InkWell(
|
||||||
],
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
onTap: () async {
|
||||||
const SizedBox(height: 12),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: isConfigured
|
|
||||||
? FilledButton.tonalIcon(
|
|
||||||
onPressed: _navigateToSpoofingScreen(context),
|
|
||||||
icon: const Icon(Icons.settings, size: 18),
|
|
||||||
label: Text(
|
|
||||||
'Изменить настройки',
|
|
||||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: FilledButton.icon(
|
|
||||||
onPressed: _navigateToSpoofingScreen(context),
|
|
||||||
icon: const Icon(Icons.visibility_off, size: 18),
|
|
||||||
label: Text(
|
|
||||||
'Настроить анонимность',
|
|
||||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VoidCallback _navigateToSpoofingScreen(BuildContext context) {
|
|
||||||
return () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(builder: (context) => const SessionSpoofingScreen()),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProxyCard extends StatelessWidget {
|
|
||||||
final bool isConfigured;
|
|
||||||
const _ProxyCard({required this.isConfigured});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final colors = Theme.of(context).colorScheme;
|
|
||||||
final textTheme = Theme.of(context).textTheme;
|
|
||||||
|
|
||||||
final Color cardColor = isConfigured
|
|
||||||
? colors.secondaryContainer
|
|
||||||
: colors.surfaceContainerHighest.withOpacity(0.5);
|
|
||||||
final Color onCardColor = isConfigured
|
|
||||||
? colors.onSecondaryContainer
|
|
||||||
: colors.onSurfaceVariant;
|
|
||||||
final IconData icon = isConfigured ? Icons.vpn_key : Icons.vpn_key_outlined;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: cardColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
side: BorderSide(color: colors.outline.withOpacity(0.5)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: onCardColor, size: 20),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
isConfigured
|
|
||||||
? 'Прокси-сервер настроен и активен'
|
|
||||||
: 'Настройте прокси-сервер для подключения',
|
|
||||||
style: GoogleFonts.manrope(
|
|
||||||
textStyle: textTheme.bodyMedium,
|
|
||||||
color: onCardColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: isConfigured
|
|
||||||
? FilledButton.tonalIcon(
|
|
||||||
onPressed: _navigateToProxyScreen(context),
|
|
||||||
icon: const Icon(Icons.settings, size: 18),
|
|
||||||
label: Text(
|
|
||||||
'Изменить настройки',
|
|
||||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: FilledButton.icon(
|
|
||||||
onPressed: _navigateToProxyScreen(context),
|
|
||||||
icon: const Icon(Icons.vpn_key, size: 18),
|
|
||||||
label: Text(
|
|
||||||
'Настроить прокси',
|
|
||||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VoidCallback _navigateToProxyScreen(BuildContext context) {
|
|
||||||
return () async {
|
|
||||||
await Navigator.of(context).push(
|
await Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (context) => const ProxySettingsScreen()),
|
MaterialPageRoute(builder: (context) => const AuthSettingsScreen()),
|
||||||
|
);
|
||||||
|
onRefresh();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hasAnySettings
|
||||||
|
? colors.primary.withOpacity(0.15)
|
||||||
|
: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.tune_outlined,
|
||||||
|
color: hasAnySettings
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Настройки',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.titleMedium,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
hasAnySettings
|
||||||
|
? 'Настроены дополнительные параметры'
|
||||||
|
: 'Прокси и анонимность',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.bodySmall,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (hasAnySettings) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (hasCustomAnonymity) ...[
|
||||||
|
Icon(
|
||||||
|
Icons.verified_user,
|
||||||
|
size: 16,
|
||||||
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Анонимность',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.labelSmall,
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (hasCustomAnonymity && hasProxyConfigured) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
],
|
||||||
|
if (hasProxyConfigured) ...[
|
||||||
|
Icon(Icons.vpn_key, size: 16, color: colors.primary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Прокси',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.labelSmall,
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (context.mounted) {
|
|
||||||
final state = context.findAncestorStateOfType<_PhoneEntryScreenState>();
|
|
||||||
state?.refreshProxySettings();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
610
lib/screens/edit_contact_screen.dart
Normal file
610
lib/screens/edit_contact_screen.dart
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:gwid/services/contact_local_names_service.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class EditContactScreen extends StatefulWidget {
|
||||||
|
final int contactId;
|
||||||
|
final String? originalFirstName;
|
||||||
|
final String? originalLastName;
|
||||||
|
final String? originalDescription;
|
||||||
|
final String? originalAvatarUrl;
|
||||||
|
|
||||||
|
const EditContactScreen({
|
||||||
|
super.key,
|
||||||
|
required this.contactId,
|
||||||
|
this.originalFirstName,
|
||||||
|
this.originalLastName,
|
||||||
|
this.originalDescription,
|
||||||
|
this.originalAvatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditContactScreen> createState() => _EditContactScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditContactScreenState extends State<EditContactScreen> {
|
||||||
|
late final TextEditingController _firstNameController;
|
||||||
|
late final TextEditingController _lastNameController;
|
||||||
|
late final TextEditingController _notesController;
|
||||||
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||||
|
bool _isLoading = true;
|
||||||
|
String? _localAvatarPath;
|
||||||
|
bool _isLoadingAvatar = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_firstNameController = TextEditingController(
|
||||||
|
text: widget.originalFirstName ?? '',
|
||||||
|
);
|
||||||
|
_lastNameController = TextEditingController(
|
||||||
|
text: widget.originalLastName ?? '',
|
||||||
|
);
|
||||||
|
_notesController = TextEditingController();
|
||||||
|
|
||||||
|
_loadContactData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContactData() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'contact_${widget.contactId}';
|
||||||
|
final savedData = prefs.getString(key);
|
||||||
|
|
||||||
|
if (savedData != null) {
|
||||||
|
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
_firstNameController.text =
|
||||||
|
data['firstName'] ?? widget.originalFirstName ?? '';
|
||||||
|
_lastNameController.text =
|
||||||
|
data['lastName'] ?? widget.originalLastName ?? '';
|
||||||
|
_notesController.text = data['notes'] ?? '';
|
||||||
|
|
||||||
|
final avatarPath = data['avatarPath'] as String?;
|
||||||
|
if (avatarPath != null) {
|
||||||
|
final file = File(avatarPath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = avatarPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_localAvatarPath == null && mounted) {
|
||||||
|
final cachedPath = ContactLocalNamesService().getContactAvatarPath(
|
||||||
|
widget.contactId,
|
||||||
|
);
|
||||||
|
if (cachedPath != null) {
|
||||||
|
final file = File(cachedPath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = cachedPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Ошибка загрузки локальных данных контакта: $e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveContactData() async {
|
||||||
|
if (_isLoading || !_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final data = {
|
||||||
|
'firstName': _firstNameController.text.trim(),
|
||||||
|
'lastName': _lastNameController.text.trim(),
|
||||||
|
'notes': _notesController.text.trim(),
|
||||||
|
'updatedAt': DateTime.now().toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_localAvatarPath != null) {
|
||||||
|
data['avatarPath'] = _localAvatarPath!;
|
||||||
|
}
|
||||||
|
await ContactLocalNamesService().saveContactData(widget.contactId, data);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Данные контакта сохранены'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка сохранения: $e'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearContactData() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Очистить данные?'),
|
||||||
|
content: const Text(
|
||||||
|
'Будут восстановлены оригинальные данные контакта с сервера.',
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red.shade400,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Очистить'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await ContactLocalNamesService().clearContactData(widget.contactId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Данные контакта очищены'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка: $e'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider? _getAvatarImage() {
|
||||||
|
if (_localAvatarPath != null) {
|
||||||
|
return FileImage(File(_localAvatarPath!));
|
||||||
|
} else if (widget.originalAvatarUrl != null) {
|
||||||
|
return NetworkImage(widget.originalAvatarUrl!);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickAvatar() async {
|
||||||
|
try {
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoadingAvatar = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
File imageFile = File(image.path);
|
||||||
|
|
||||||
|
final localPath = await ContactLocalNamesService().saveContactAvatar(
|
||||||
|
imageFile,
|
||||||
|
widget.contactId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localPath != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = localPath;
|
||||||
|
_isLoadingAvatar = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Аватар сохранен'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingAvatar = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingAvatar = false;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка загрузки аватара: $e'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeAvatar() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Удалить аватар?'),
|
||||||
|
content: const Text(
|
||||||
|
'Локальный аватар будет удален, будет показан оригинальный.',
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red.shade400,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Удалить'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
await ContactLocalNamesService().removeContactAvatar(widget.contactId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Аватар удален'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Ошибка удаления: $e'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Редактировать контакт'),
|
||||||
|
centerTitle: true,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _saveContactData,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'Сохранить',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 20,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Эти данные сохраняются только локально',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Аватар',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _pickAvatar,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 60,
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme.secondaryContainer,
|
||||||
|
backgroundImage: _getAvatarImage(),
|
||||||
|
child: _getAvatarImage() == null
|
||||||
|
? Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 60,
|
||||||
|
color: theme
|
||||||
|
.colorScheme
|
||||||
|
.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (_isLoadingAvatar)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 4,
|
||||||
|
right: 4,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.camera_alt,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_localAvatarPath != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _removeAvatar,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: const Text('Удалить аватар'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
side: BorderSide(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Локальное имя',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _firstNameController,
|
||||||
|
maxLength: 60,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Имя',
|
||||||
|
hintText: widget.originalFirstName ?? 'Имя',
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
counterText: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _lastNameController,
|
||||||
|
maxLength: 60,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Фамилия',
|
||||||
|
hintText: widget.originalLastName ?? 'Фамилия',
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
counterText: '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Заметки',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
maxLines: 4,
|
||||||
|
maxLength: 400,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Заметки о контакте',
|
||||||
|
hintText: 'Добавьте заметки...',
|
||||||
|
prefixIcon: const Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 60),
|
||||||
|
child: Icon(Icons.note_outlined),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _clearContactData,
|
||||||
|
icon: const Icon(Icons.restore),
|
||||||
|
label: const Text('Восстановить оригинальные данные'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.error,
|
||||||
|
side: BorderSide(color: theme.colorScheme.error),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_firstNameController.dispose();
|
||||||
|
_lastNameController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContactLocalDataHelper {
|
||||||
|
static Future<Map<String, dynamic>?> getContactData(int contactId) async {
|
||||||
|
return ContactLocalNamesService().getContactData(contactId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> clearContactData(int contactId) async {
|
||||||
|
await ContactLocalNamesService().clearContactData(contactId);
|
||||||
|
}
|
||||||
|
}
|
||||||
410
lib/screens/settings/auth_settings_screen.dart
Normal file
410
lib/screens/settings/auth_settings_screen.dart
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:gwid/proxy_service.dart';
|
||||||
|
import 'package:gwid/screens/settings/proxy_settings_screen.dart';
|
||||||
|
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AuthSettingsScreen extends StatefulWidget {
|
||||||
|
const AuthSettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuthSettingsScreen> createState() => _AuthSettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthSettingsScreenState extends State<AuthSettingsScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
bool _hasCustomAnonymity = false;
|
||||||
|
bool _hasProxyConfigured = false;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimation =
|
||||||
|
Tween<Offset>(begin: const Offset(0, 0.1), end: Offset.zero).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _animationController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_checkSettings();
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkSettings() async {
|
||||||
|
await Future.wait([_checkAnonymitySettings(), _checkProxySettings()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkAnonymitySettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final anonymityEnabled = prefs.getBool('anonymity_enabled') ?? false;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _hasCustomAnonymity = anonymityEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkProxySettings() async {
|
||||||
|
final settings = await ProxyService.instance.loadProxySettings();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_hasProxyConfigured = settings.isEnabled && settings.host.isNotEmpty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToAnonymitySettings() async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => const SessionSpoofingScreen()),
|
||||||
|
);
|
||||||
|
_checkAnonymitySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToProxySettings() async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => const ProxySettingsScreen()),
|
||||||
|
);
|
||||||
|
_checkProxySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color.lerp(colors.surface, colors.primary, 0.05)!,
|
||||||
|
colors.surface,
|
||||||
|
Color.lerp(colors.surface, colors.tertiary, 0.05)!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Настройки',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.headlineSmall,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Безопасность и конфиденциальность',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.bodyMedium,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _fadeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
children: [
|
||||||
|
_SettingsCard(
|
||||||
|
icon: _hasCustomAnonymity
|
||||||
|
? Icons.verified_user
|
||||||
|
: Icons.visibility_outlined,
|
||||||
|
title: 'Настройки анонимности',
|
||||||
|
description: _hasCustomAnonymity
|
||||||
|
? 'Активны кастомные настройки анонимности'
|
||||||
|
: 'Настройте анонимность для скрытия данных устройства',
|
||||||
|
isConfigured: _hasCustomAnonymity,
|
||||||
|
onTap: _navigateToAnonymitySettings,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: _hasCustomAnonymity
|
||||||
|
? [
|
||||||
|
Color.lerp(
|
||||||
|
colors.primaryContainer,
|
||||||
|
colors.primary,
|
||||||
|
0.2,
|
||||||
|
)!,
|
||||||
|
colors.primaryContainer,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
colors.surfaceContainerHighest,
|
||||||
|
colors.surfaceContainer,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_SettingsCard(
|
||||||
|
icon: _hasProxyConfigured
|
||||||
|
? Icons.vpn_key
|
||||||
|
: Icons.vpn_key_outlined,
|
||||||
|
title: 'Настройки прокси',
|
||||||
|
description: _hasProxyConfigured
|
||||||
|
? 'Прокси-сервер настроен и активен'
|
||||||
|
: 'Настройте прокси-сервер для безопасного подключения',
|
||||||
|
isConfigured: _hasProxyConfigured,
|
||||||
|
onTap: _navigateToProxySettings,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: _hasProxyConfigured
|
||||||
|
? [
|
||||||
|
Color.lerp(
|
||||||
|
colors.tertiaryContainer,
|
||||||
|
colors.tertiary,
|
||||||
|
0.2,
|
||||||
|
)!,
|
||||||
|
colors.tertiaryContainer,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
colors.surfaceContainerHighest,
|
||||||
|
colors.surfaceContainer,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest.withOpacity(
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: colors.outline.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: colors.primary,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Эти настройки помогут вам безопасно и анонимно использовать приложение',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.bodyMedium,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final bool isConfigured;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Gradient gradient;
|
||||||
|
|
||||||
|
const _SettingsCard({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.isConfigured,
|
||||||
|
required this.onTap,
|
||||||
|
required this.gradient,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: gradient,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isConfigured
|
||||||
|
? colors.primary.withOpacity(0.3)
|
||||||
|
: colors.outline.withOpacity(0.2),
|
||||||
|
width: isConfigured ? 2 : 1,
|
||||||
|
),
|
||||||
|
boxShadow: isConfigured
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: colors.primary.withOpacity(0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isConfigured
|
||||||
|
? colors.primary.withOpacity(0.15)
|
||||||
|
: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: isConfigured
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isConfigured)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.primary.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: colors.primary,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Активно',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.labelSmall,
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.titleLarge,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.bodyMedium,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isConfigured ? 'Изменить настройки' : 'Настроить',
|
||||||
|
style: GoogleFonts.manrope(
|
||||||
|
textStyle: textTheme.labelLarge,
|
||||||
|
color: isConfigured
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward,
|
||||||
|
color: isConfigured
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
243
lib/services/contact_local_names_service.dart
Normal file
243
lib/services/contact_local_names_service.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class ContactLocalNamesService {
|
||||||
|
static final ContactLocalNamesService _instance =
|
||||||
|
ContactLocalNamesService._internal();
|
||||||
|
factory ContactLocalNamesService() => _instance;
|
||||||
|
ContactLocalNamesService._internal();
|
||||||
|
final Map<int, Map<String, dynamic>> _cache = {};
|
||||||
|
|
||||||
|
final _changesController = StreamController<int>.broadcast();
|
||||||
|
Stream<int> get changes => _changesController.stream;
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_initialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final keys = prefs.getKeys();
|
||||||
|
|
||||||
|
for (final key in keys) {
|
||||||
|
if (key.startsWith('contact_')) {
|
||||||
|
final contactIdStr = key.replaceFirst('contact_', '');
|
||||||
|
final contactId = int.tryParse(contactIdStr);
|
||||||
|
|
||||||
|
if (contactId != null) {
|
||||||
|
final data = prefs.getString(key);
|
||||||
|
if (data != null) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(data) as Map<String, dynamic>;
|
||||||
|
final avatarPath = decoded['avatarPath'] as String?;
|
||||||
|
if (avatarPath != null) {
|
||||||
|
final file = File(avatarPath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
decoded.remove('avatarPath');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_cache[contactId] = decoded;
|
||||||
|
} catch (e) {
|
||||||
|
print(
|
||||||
|
'Ошибка парсинга локальных данных для контакта $contactId: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
print(
|
||||||
|
'✅ ContactLocalNamesService: загружено ${_cache.length} локальных имен',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка инициализации ContactLocalNamesService: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? getContactData(int contactId) {
|
||||||
|
return _cache[contactId];
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDisplayName({
|
||||||
|
required int contactId,
|
||||||
|
String? originalName,
|
||||||
|
String? originalFirstName,
|
||||||
|
String? originalLastName,
|
||||||
|
}) {
|
||||||
|
final localData = _cache[contactId];
|
||||||
|
|
||||||
|
if (localData != null) {
|
||||||
|
final firstName = localData['firstName'] as String?;
|
||||||
|
final lastName = localData['lastName'] as String?;
|
||||||
|
|
||||||
|
if (firstName != null && firstName.isNotEmpty ||
|
||||||
|
lastName != null && lastName.isNotEmpty) {
|
||||||
|
final fullName = '${firstName ?? ''} ${lastName ?? ''}'.trim();
|
||||||
|
if (fullName.isNotEmpty) {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalFirstName != null || originalLastName != null) {
|
||||||
|
final fullName = '${originalFirstName ?? ''} ${originalLastName ?? ''}'
|
||||||
|
.trim();
|
||||||
|
if (fullName.isNotEmpty) {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalName ?? 'ID $contactId';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getDisplayDescription({
|
||||||
|
required int contactId,
|
||||||
|
String? originalDescription,
|
||||||
|
}) {
|
||||||
|
final localData = _cache[contactId];
|
||||||
|
|
||||||
|
if (localData != null) {
|
||||||
|
final notes = localData['notes'] as String?;
|
||||||
|
if (notes != null && notes.isNotEmpty) {
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> saveContactAvatar(File imageFile, int contactId) async {
|
||||||
|
try {
|
||||||
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final avatarDir = Directory('${directory.path}/contact_avatars');
|
||||||
|
|
||||||
|
if (!await avatarDir.exists()) {
|
||||||
|
await avatarDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileName = 'contact_$contactId.jpg';
|
||||||
|
final savePath = '${avatarDir.path}/$fileName';
|
||||||
|
|
||||||
|
await imageFile.copy(savePath);
|
||||||
|
|
||||||
|
final localData = _cache[contactId] ?? {};
|
||||||
|
localData['avatarPath'] = savePath;
|
||||||
|
_cache[contactId] = localData;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'contact_$contactId';
|
||||||
|
await prefs.setString(key, jsonEncode(localData));
|
||||||
|
|
||||||
|
_changesController.add(contactId);
|
||||||
|
|
||||||
|
print('✅ Локальный аватар контакта сохранен: $savePath');
|
||||||
|
return savePath;
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка сохранения локального аватара контакта: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getContactAvatarPath(int contactId) {
|
||||||
|
final localData = _cache[contactId];
|
||||||
|
if (localData != null) {
|
||||||
|
return localData['avatarPath'] as String?;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getDisplayAvatar({
|
||||||
|
required int contactId,
|
||||||
|
String? originalAvatarUrl,
|
||||||
|
}) {
|
||||||
|
final localAvatarPath = getContactAvatarPath(contactId);
|
||||||
|
if (localAvatarPath != null) {
|
||||||
|
final file = File(localAvatarPath);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return 'file://$localAvatarPath';
|
||||||
|
} else {
|
||||||
|
final localData = _cache[contactId];
|
||||||
|
if (localData != null) {
|
||||||
|
localData.remove('avatarPath');
|
||||||
|
_cache[contactId] = localData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeContactAvatar(int contactId) async {
|
||||||
|
try {
|
||||||
|
final localData = _cache[contactId];
|
||||||
|
if (localData != null) {
|
||||||
|
final avatarPath = localData['avatarPath'] as String?;
|
||||||
|
if (avatarPath != null) {
|
||||||
|
final file = File(avatarPath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localData.remove('avatarPath');
|
||||||
|
_cache[contactId] = localData;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'contact_$contactId';
|
||||||
|
await prefs.setString(key, jsonEncode(localData));
|
||||||
|
|
||||||
|
_changesController.add(contactId);
|
||||||
|
|
||||||
|
print('✅ Локальный аватар контакта удален');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка удаления локального аватара контакта: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveContactData(int contactId, Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'contact_$contactId';
|
||||||
|
await prefs.setString(key, jsonEncode(data));
|
||||||
|
|
||||||
|
_cache[contactId] = data;
|
||||||
|
|
||||||
|
_changesController.add(contactId);
|
||||||
|
|
||||||
|
print('✅ Сохранены локальные данные для контакта $contactId');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка сохранения локальных данных контакта: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearContactData(int contactId) async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final key = 'contact_$contactId';
|
||||||
|
await prefs.remove(key);
|
||||||
|
|
||||||
|
_cache.remove(contactId);
|
||||||
|
|
||||||
|
_changesController.add(contactId);
|
||||||
|
|
||||||
|
print('✅ Очищены локальные данные для контакта $contactId');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка очистки локальных данных контакта: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCache() {
|
||||||
|
_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_changesController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/services/local_profile_manager.dart
Normal file
57
lib/services/local_profile_manager.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:gwid/models/profile.dart';
|
||||||
|
import 'package:gwid/services/profile_cache_service.dart';
|
||||||
|
|
||||||
|
class LocalProfileManager {
|
||||||
|
static final LocalProfileManager _instance = LocalProfileManager._internal();
|
||||||
|
factory LocalProfileManager() => _instance;
|
||||||
|
LocalProfileManager._internal();
|
||||||
|
|
||||||
|
final ProfileCacheService _profileCache = ProfileCacheService();
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_initialized) return;
|
||||||
|
await _profileCache.initialize();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Profile?> getActualProfile(Profile? serverProfile) async {
|
||||||
|
await initialize();
|
||||||
|
|
||||||
|
final localAvatarPath = await _profileCache.getLocalAvatarPath();
|
||||||
|
final mergedProfile = await _profileCache.getMergedProfile(serverProfile);
|
||||||
|
|
||||||
|
if (mergedProfile != null && localAvatarPath != null) {
|
||||||
|
return Profile(
|
||||||
|
id: mergedProfile.id,
|
||||||
|
phone: mergedProfile.phone,
|
||||||
|
firstName: mergedProfile.firstName,
|
||||||
|
lastName: mergedProfile.lastName,
|
||||||
|
description: mergedProfile.description,
|
||||||
|
photoBaseUrl: 'file://$localAvatarPath',
|
||||||
|
photoId: mergedProfile.photoId,
|
||||||
|
updateTime: mergedProfile.updateTime,
|
||||||
|
options: mergedProfile.options,
|
||||||
|
accountStatus: mergedProfile.accountStatus,
|
||||||
|
profileOptions: mergedProfile.profileOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLocalAvatarPath() async {
|
||||||
|
await initialize();
|
||||||
|
return await _profileCache.getLocalAvatarPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasLocalChanges() async {
|
||||||
|
await initialize();
|
||||||
|
return await _profileCache.hasLocalChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearLocalChanges() async {
|
||||||
|
await initialize();
|
||||||
|
await _profileCache.clearProfileCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
241
lib/services/profile_cache_service.dart
Normal file
241
lib/services/profile_cache_service.dart
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:gwid/services/cache_service.dart';
|
||||||
|
import 'package:gwid/models/profile.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class ProfileCacheService {
|
||||||
|
static final ProfileCacheService _instance = ProfileCacheService._internal();
|
||||||
|
factory ProfileCacheService() => _instance;
|
||||||
|
ProfileCacheService._internal();
|
||||||
|
|
||||||
|
final CacheService _cacheService = CacheService();
|
||||||
|
|
||||||
|
static const String _profileKey = 'my_profile_data';
|
||||||
|
static const String _profileAvatarKey = 'my_profile_avatar';
|
||||||
|
static const Duration _profileTTL = Duration(days: 30);
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_initialized) return;
|
||||||
|
await _cacheService.initialize();
|
||||||
|
_initialized = true;
|
||||||
|
print('✅ ProfileCacheService инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveProfileData({
|
||||||
|
required int userId,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
String? description,
|
||||||
|
String? photoBaseUrl,
|
||||||
|
int? photoId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final profileData = {
|
||||||
|
'userId': userId,
|
||||||
|
'firstName': firstName,
|
||||||
|
'lastName': lastName,
|
||||||
|
'description': description,
|
||||||
|
'photoBaseUrl': photoBaseUrl,
|
||||||
|
'photoId': photoId,
|
||||||
|
'updatedAt': DateTime.now().toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await _cacheService.set(_profileKey, profileData, ttl: _profileTTL);
|
||||||
|
print('✅ Данные профиля сохранены в кэш: $firstName $lastName');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка сохранения профиля в кэш: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getProfileData() async {
|
||||||
|
try {
|
||||||
|
final cached = await _cacheService.get<Map<String, dynamic>>(
|
||||||
|
_profileKey,
|
||||||
|
ttl: _profileTTL,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cached != null) {
|
||||||
|
print('✅ Данные профиля загружены из кэша');
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка загрузки профиля из кэша: $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> saveAvatar(File imageFile, int userId) async {
|
||||||
|
try {
|
||||||
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final avatarDir = Directory('${directory.path}/avatars');
|
||||||
|
|
||||||
|
if (!await avatarDir.exists()) {
|
||||||
|
await avatarDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileName = 'profile_$userId.jpg';
|
||||||
|
final savePath = '${avatarDir.path}/$fileName';
|
||||||
|
|
||||||
|
await imageFile.copy(savePath);
|
||||||
|
|
||||||
|
await _cacheService.set(_profileAvatarKey, savePath, ttl: _profileTTL);
|
||||||
|
|
||||||
|
print('✅ Аватар сохранен локально: $savePath');
|
||||||
|
return savePath;
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка сохранения аватара: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getLocalAvatarPath() async {
|
||||||
|
try {
|
||||||
|
final path = await _cacheService.get<String>(
|
||||||
|
_profileAvatarKey,
|
||||||
|
ttl: _profileTTL,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (path != null) {
|
||||||
|
final file = File(path);
|
||||||
|
if (await file.exists()) {
|
||||||
|
print('✅ Локальный аватар найден: $path');
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
await _cacheService.remove(_profileAvatarKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка загрузки локального аватара: $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProfileFields({
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? description,
|
||||||
|
String? photoBaseUrl,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final currentData = await getProfileData();
|
||||||
|
if (currentData == null) {
|
||||||
|
print('⚠️ Нет сохраненных данных профиля для обновления');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstName != null) currentData['firstName'] = firstName;
|
||||||
|
if (lastName != null) currentData['lastName'] = lastName;
|
||||||
|
if (description != null) currentData['description'] = description;
|
||||||
|
if (photoBaseUrl != null) currentData['photoBaseUrl'] = photoBaseUrl;
|
||||||
|
|
||||||
|
currentData['updatedAt'] = DateTime.now().toIso8601String();
|
||||||
|
|
||||||
|
await _cacheService.set(_profileKey, currentData, ttl: _profileTTL);
|
||||||
|
print('✅ Поля профиля обновлены в кэше');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка обновления полей профиля: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearProfileCache() async {
|
||||||
|
try {
|
||||||
|
await _cacheService.remove(_profileKey);
|
||||||
|
await _cacheService.remove(_profileAvatarKey);
|
||||||
|
|
||||||
|
final avatarPath = await getLocalAvatarPath();
|
||||||
|
if (avatarPath != null) {
|
||||||
|
final file = File(avatarPath);
|
||||||
|
if (await file.exists()) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('✅ Кэш профиля очищен');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка очистки кэша профиля: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> syncWithServerProfile(Profile serverProfile) async {
|
||||||
|
try {
|
||||||
|
final cachedData = await getProfileData();
|
||||||
|
|
||||||
|
if (cachedData != null) {
|
||||||
|
print(
|
||||||
|
'⚠️ Локальные данные профиля уже существуют, пропускаем синхронизацию',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveProfileData(
|
||||||
|
userId: serverProfile.id,
|
||||||
|
firstName: serverProfile.firstName,
|
||||||
|
lastName: serverProfile.lastName,
|
||||||
|
description: serverProfile.description,
|
||||||
|
photoBaseUrl: serverProfile.photoBaseUrl,
|
||||||
|
photoId: serverProfile.photoId,
|
||||||
|
);
|
||||||
|
print('✅ Профиль инициализирован с сервера');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка синхронизации профиля: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Profile?> getMergedProfile(Profile? serverProfile) async {
|
||||||
|
try {
|
||||||
|
final cachedData = await getProfileData();
|
||||||
|
|
||||||
|
if (cachedData == null && serverProfile == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedData == null && serverProfile != null) {
|
||||||
|
return serverProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedData != null && serverProfile == null) {
|
||||||
|
return Profile(
|
||||||
|
id: cachedData['userId'] ?? 0,
|
||||||
|
phone: '',
|
||||||
|
firstName: cachedData['firstName'] ?? '',
|
||||||
|
lastName: cachedData['lastName'] ?? '',
|
||||||
|
description: cachedData['description'],
|
||||||
|
photoBaseUrl: cachedData['photoBaseUrl'],
|
||||||
|
photoId: cachedData['photoId'] ?? 0,
|
||||||
|
updateTime: 0,
|
||||||
|
options: [],
|
||||||
|
accountStatus: 0,
|
||||||
|
profileOptions: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Profile(
|
||||||
|
id: serverProfile!.id,
|
||||||
|
phone: serverProfile.phone,
|
||||||
|
firstName: cachedData!['firstName'] ?? serverProfile.firstName,
|
||||||
|
lastName: cachedData['lastName'] ?? serverProfile.lastName,
|
||||||
|
description: cachedData['description'] ?? serverProfile.description,
|
||||||
|
photoBaseUrl: cachedData['photoBaseUrl'] ?? serverProfile.photoBaseUrl,
|
||||||
|
photoId: cachedData['photoId'] ?? serverProfile.photoId,
|
||||||
|
updateTime: serverProfile.updateTime,
|
||||||
|
options: serverProfile.options,
|
||||||
|
accountStatus: serverProfile.accountStatus,
|
||||||
|
profileOptions: serverProfile.profileOptions,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка получения объединенного профиля: $e');
|
||||||
|
return serverProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasLocalChanges() async {
|
||||||
|
try {
|
||||||
|
final cachedData = await getProfileData();
|
||||||
|
return cachedData != null;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/widgets/contact_avatar_widget.dart
Normal file
139
lib/widgets/contact_avatar_widget.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gwid/services/contact_local_names_service.dart';
|
||||||
|
|
||||||
|
class ContactAvatarWidget extends StatefulWidget {
|
||||||
|
final int contactId;
|
||||||
|
final String? originalAvatarUrl;
|
||||||
|
final double radius;
|
||||||
|
final String? fallbackText;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? textColor;
|
||||||
|
|
||||||
|
const ContactAvatarWidget({
|
||||||
|
super.key,
|
||||||
|
required this.contactId,
|
||||||
|
this.originalAvatarUrl,
|
||||||
|
this.radius = 24,
|
||||||
|
this.fallbackText,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.textColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContactAvatarWidget> createState() => _ContactAvatarWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactAvatarWidgetState extends State<ContactAvatarWidget> {
|
||||||
|
String? _localAvatarPath;
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadLocalAvatar();
|
||||||
|
|
||||||
|
_subscription = ContactLocalNamesService().changes.listen((contactId) {
|
||||||
|
if (contactId == widget.contactId && mounted) {
|
||||||
|
_loadLocalAvatar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ContactAvatarWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.contactId != widget.contactId ||
|
||||||
|
oldWidget.originalAvatarUrl != widget.originalAvatarUrl) {
|
||||||
|
_loadLocalAvatar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadLocalAvatar() {
|
||||||
|
final localPath = ContactLocalNamesService().getContactAvatarPath(
|
||||||
|
widget.contactId,
|
||||||
|
);
|
||||||
|
if (localPath != null) {
|
||||||
|
final file = File(localPath);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = localPath;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localAvatarPath = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider? _getAvatarImage() {
|
||||||
|
if (_localAvatarPath != null) {
|
||||||
|
return FileImage(File(_localAvatarPath!));
|
||||||
|
} else if (widget.originalAvatarUrl != null) {
|
||||||
|
if (widget.originalAvatarUrl!.startsWith('file://')) {
|
||||||
|
final path = widget.originalAvatarUrl!.replaceFirst('file://', '');
|
||||||
|
return FileImage(File(path));
|
||||||
|
}
|
||||||
|
return NetworkImage(widget.originalAvatarUrl!);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final avatarImage = _getAvatarImage();
|
||||||
|
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: widget.radius,
|
||||||
|
backgroundColor:
|
||||||
|
widget.backgroundColor ?? theme.colorScheme.secondaryContainer,
|
||||||
|
backgroundImage: avatarImage,
|
||||||
|
child: avatarImage == null
|
||||||
|
? Text(
|
||||||
|
widget.fallbackText ?? '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
widget.textColor ?? theme.colorScheme.onSecondaryContainer,
|
||||||
|
fontSize: widget.radius * 0.8,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider? getContactAvatarImage({
|
||||||
|
required int contactId,
|
||||||
|
String? originalAvatarUrl,
|
||||||
|
}) {
|
||||||
|
final localPath = ContactLocalNamesService().getContactAvatarPath(contactId);
|
||||||
|
|
||||||
|
if (localPath != null) {
|
||||||
|
final file = File(localPath);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return FileImage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalAvatarUrl != null) {
|
||||||
|
if (originalAvatarUrl.startsWith('file://')) {
|
||||||
|
final path = originalAvatarUrl.replaceFirst('file://', '');
|
||||||
|
return FileImage(File(path));
|
||||||
|
}
|
||||||
|
return NetworkImage(originalAvatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
95
lib/widgets/contact_display_name.dart
Normal file
95
lib/widgets/contact_display_name.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gwid/screens/edit_contact_screen.dart';
|
||||||
|
|
||||||
|
class ContactDisplayName extends StatefulWidget {
|
||||||
|
final int contactId;
|
||||||
|
final String? originalFirstName;
|
||||||
|
final String? originalLastName;
|
||||||
|
final String? fallbackName;
|
||||||
|
final TextStyle? style;
|
||||||
|
final int? maxLines;
|
||||||
|
final TextOverflow? overflow;
|
||||||
|
|
||||||
|
const ContactDisplayName({
|
||||||
|
super.key,
|
||||||
|
required this.contactId,
|
||||||
|
this.originalFirstName,
|
||||||
|
this.originalLastName,
|
||||||
|
this.fallbackName,
|
||||||
|
this.style,
|
||||||
|
this.maxLines,
|
||||||
|
this.overflow,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContactDisplayName> createState() => _ContactDisplayNameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactDisplayNameState extends State<ContactDisplayName> {
|
||||||
|
String? _localFirstName;
|
||||||
|
String? _localLastName;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadLocalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ContactDisplayName oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.contactId != widget.contactId) {
|
||||||
|
_loadLocalData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLocalData() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final localData = await ContactLocalDataHelper.getContactData(
|
||||||
|
widget.contactId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localFirstName = localData?['firstName'] as String?;
|
||||||
|
_localLastName = localData?['lastName'] as String?;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _displayName {
|
||||||
|
final firstName = _localFirstName ?? widget.originalFirstName ?? '';
|
||||||
|
final lastName = _localLastName ?? widget.originalLastName ?? '';
|
||||||
|
final fullName = '$firstName $lastName'.trim();
|
||||||
|
|
||||||
|
if (fullName.isNotEmpty) {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.fallbackName ?? 'ID ${widget.contactId}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return Text(
|
||||||
|
widget.fallbackName ?? '...',
|
||||||
|
style: widget.style,
|
||||||
|
maxLines: widget.maxLines,
|
||||||
|
overflow: widget.overflow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
_displayName,
|
||||||
|
style: widget.style,
|
||||||
|
maxLines: widget.maxLines,
|
||||||
|
overflow: widget.overflow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
lib/widgets/contact_name_widget.dart
Normal file
96
lib/widgets/contact_name_widget.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gwid/services/contact_local_names_service.dart';
|
||||||
|
|
||||||
|
class ContactNameWidget extends StatefulWidget {
|
||||||
|
final int contactId;
|
||||||
|
final String? originalName;
|
||||||
|
final String? originalFirstName;
|
||||||
|
final String? originalLastName;
|
||||||
|
final TextStyle? style;
|
||||||
|
final int? maxLines;
|
||||||
|
final TextOverflow? overflow;
|
||||||
|
|
||||||
|
const ContactNameWidget({
|
||||||
|
super.key,
|
||||||
|
required this.contactId,
|
||||||
|
this.originalName,
|
||||||
|
this.originalFirstName,
|
||||||
|
this.originalLastName,
|
||||||
|
this.style,
|
||||||
|
this.maxLines,
|
||||||
|
this.overflow,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContactNameWidget> createState() => _ContactNameWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactNameWidgetState extends State<ContactNameWidget> {
|
||||||
|
late String _displayName;
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateDisplayName();
|
||||||
|
|
||||||
|
_subscription = ContactLocalNamesService().changes.listen((contactId) {
|
||||||
|
if (contactId == widget.contactId && mounted) {
|
||||||
|
_updateDisplayName();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ContactNameWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.contactId != widget.contactId ||
|
||||||
|
oldWidget.originalName != widget.originalName ||
|
||||||
|
oldWidget.originalFirstName != widget.originalFirstName ||
|
||||||
|
oldWidget.originalLastName != widget.originalLastName) {
|
||||||
|
_updateDisplayName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateDisplayName() {
|
||||||
|
setState(() {
|
||||||
|
_displayName = ContactLocalNamesService().getDisplayName(
|
||||||
|
contactId: widget.contactId,
|
||||||
|
originalName: widget.originalName,
|
||||||
|
originalFirstName: widget.originalFirstName,
|
||||||
|
originalLastName: widget.originalLastName,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
_displayName,
|
||||||
|
style: widget.style,
|
||||||
|
maxLines: widget.maxLines,
|
||||||
|
overflow: widget.overflow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getContactDisplayName({
|
||||||
|
required int contactId,
|
||||||
|
String? originalName,
|
||||||
|
String? originalFirstName,
|
||||||
|
String? originalLastName,
|
||||||
|
}) {
|
||||||
|
return ContactLocalNamesService().getDisplayName(
|
||||||
|
contactId: contactId,
|
||||||
|
originalName: originalName,
|
||||||
|
originalFirstName: originalFirstName,
|
||||||
|
originalLastName: originalLastName,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:gwid/models/contact.dart';
|
|||||||
import 'package:gwid/models/profile.dart';
|
import 'package:gwid/models/profile.dart';
|
||||||
import 'package:gwid/api/api_service.dart';
|
import 'package:gwid/api/api_service.dart';
|
||||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||||
|
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||||
import 'package:gwid/chat_screen.dart';
|
import 'package:gwid/chat_screen.dart';
|
||||||
|
|
||||||
class ControlMessageChip extends StatelessWidget {
|
class ControlMessageChip extends StatelessWidget {
|
||||||
@@ -26,8 +27,15 @@ class ControlMessageChip extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final eventType = controlAttach['event'];
|
final eventType = controlAttach['event'];
|
||||||
final senderName =
|
final senderContact = contacts[message.senderId];
|
||||||
contacts[message.senderId]?.name ?? 'ID ${message.senderId}';
|
final senderName = senderContact != null
|
||||||
|
? getContactDisplayName(
|
||||||
|
contactId: senderContact.id,
|
||||||
|
originalName: senderContact.name,
|
||||||
|
originalFirstName: senderContact.firstName,
|
||||||
|
originalLastName: senderContact.lastName,
|
||||||
|
)
|
||||||
|
: 'ID ${message.senderId}';
|
||||||
final isMe = message.senderId == myId;
|
final isMe = message.senderId == myId;
|
||||||
final senderDisplayName = isMe ? 'Вы' : senderName;
|
final senderDisplayName = isMe ? 'Вы' : senderName;
|
||||||
|
|
||||||
@@ -40,7 +48,16 @@ class ControlMessageChip extends StatelessWidget {
|
|||||||
if (id == myId) {
|
if (id == myId) {
|
||||||
return 'Вы';
|
return 'Вы';
|
||||||
}
|
}
|
||||||
return contacts[id]?.name ?? 'участник с ID $id';
|
final contact = contacts[id];
|
||||||
|
if (contact != null) {
|
||||||
|
return getContactDisplayName(
|
||||||
|
contactId: contact.id,
|
||||||
|
originalName: contact.name,
|
||||||
|
originalFirstName: contact.firstName,
|
||||||
|
originalLastName: contact.lastName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 'участник с ID $id';
|
||||||
})
|
})
|
||||||
.where((name) => name.isNotEmpty)
|
.where((name) => name.isNotEmpty)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gwid/services/avatar_cache_service.dart';
|
import 'package:gwid/services/avatar_cache_service.dart';
|
||||||
|
import 'package:gwid/widgets/contact_name_widget.dart';
|
||||||
|
import 'package:gwid/widgets/contact_avatar_widget.dart';
|
||||||
|
import 'package:gwid/services/contact_local_names_service.dart';
|
||||||
|
|
||||||
class UserProfilePanel extends StatefulWidget {
|
class UserProfilePanel extends StatefulWidget {
|
||||||
final int userId;
|
final int userId;
|
||||||
@@ -33,27 +37,62 @@ class UserProfilePanel extends StatefulWidget {
|
|||||||
|
|
||||||
class _UserProfilePanelState extends State<UserProfilePanel> {
|
class _UserProfilePanelState extends State<UserProfilePanel> {
|
||||||
final ScrollController _nameScrollController = ScrollController();
|
final ScrollController _nameScrollController = ScrollController();
|
||||||
|
String? _localDescription;
|
||||||
|
StreamSubscription? _changesSubscription;
|
||||||
|
|
||||||
String get _displayName {
|
String get _displayName {
|
||||||
if (widget.firstName != null || widget.lastName != null) {
|
final displayName = getContactDisplayName(
|
||||||
final firstName = widget.firstName ?? '';
|
contactId: widget.userId,
|
||||||
final lastName = widget.lastName ?? '';
|
originalName: widget.name,
|
||||||
final fullName = '$firstName $lastName'.trim();
|
originalFirstName: widget.firstName,
|
||||||
return fullName.isNotEmpty
|
originalLastName: widget.lastName,
|
||||||
? fullName
|
);
|
||||||
: (widget.name ?? 'ID ${widget.userId}');
|
return displayName;
|
||||||
}
|
}
|
||||||
return widget.name ?? 'ID ${widget.userId}';
|
|
||||||
|
String? get _displayDescription {
|
||||||
|
if (_localDescription != null && _localDescription!.isNotEmpty) {
|
||||||
|
return _localDescription;
|
||||||
|
}
|
||||||
|
return widget.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_loadLocalDescription();
|
||||||
|
|
||||||
|
_changesSubscription = ContactLocalNamesService().changes.listen((
|
||||||
|
contactId,
|
||||||
|
) {
|
||||||
|
if (contactId == widget.userId && mounted) {
|
||||||
|
_loadLocalDescription();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkNameLength();
|
_checkNameLength();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLocalDescription() async {
|
||||||
|
final localData = await ContactLocalNamesService().getContactData(
|
||||||
|
widget.userId,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_localDescription = localData?['notes'] as String?;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_changesSubscription?.cancel();
|
||||||
|
_nameScrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _checkNameLength() {
|
void _checkNameLength() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_nameScrollController.hasClients) {
|
if (_nameScrollController.hasClients) {
|
||||||
@@ -99,12 +138,6 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameScrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
@@ -130,11 +163,13 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
|||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
AvatarCacheService().getAvatarWidget(
|
ContactAvatarWidget(
|
||||||
widget.avatarUrl,
|
contactId: widget.userId,
|
||||||
userId: widget.userId,
|
originalAvatarUrl: widget.avatarUrl,
|
||||||
size: 80,
|
radius: 40,
|
||||||
fallbackText: _displayName,
|
fallbackText: _displayName.isNotEmpty
|
||||||
|
? _displayName[0].toUpperCase()
|
||||||
|
: '?',
|
||||||
backgroundColor: colors.primaryContainer,
|
backgroundColor: colors.primaryContainer,
|
||||||
textColor: colors.onPrimaryContainer,
|
textColor: colors.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
@@ -213,11 +248,11 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.description != null &&
|
if (_displayDescription != null &&
|
||||||
widget.description!.isNotEmpty) ...[
|
_displayDescription!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
widget.description!,
|
_displayDescription!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colors.onSurfaceVariant,
|
color: colors.onSurfaceVariant,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|||||||
Reference in New Issue
Block a user