From 04c3772bb6c7d4f9b23fdc2143e96f1661a1be3a Mon Sep 17 00:00:00 2001 From: Artur Date: Mon, 22 Jun 2026 10:13:45 +0500 Subject: [PATCH] 22-06-2026+10-13 --- android/app/src/main/AndroidManifest.xml | 1 + lib/data/models/contact_model.dart | 5 + lib/logic/auth_provider.dart | 20 +- lib/logic/contact_provider.dart | 7 + .../screens/account_settings_screen.dart | 74 ++- .../screens/account_setup_screen.dart | 54 +- lib/presentation/screens/chat_screen.dart | 47 +- lib/presentation/screens/contacts_screen.dart | 94 +++- lib/presentation/screens/new_chat_screen.dart | 513 ++++++++++++++++-- .../screens/privacy_settings_screen.dart | 46 +- .../screens/security_settings_screen.dart | 2 +- .../screens/user_profile_screen.dart | 44 +- lib/presentation/widgets/message_bubble.dart | 12 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 28 +- pubspec.yaml | 2 + srv/app/api/endpoints/users.py | 15 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 19 files changed, 836 insertions(+), 134 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7bb8dc9..f526cf4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + diff --git a/lib/data/models/contact_model.dart b/lib/data/models/contact_model.dart index c9722e1..36fe140 100644 --- a/lib/data/models/contact_model.dart +++ b/lib/data/models/contact_model.dart @@ -17,6 +17,7 @@ class Contact { final int? lastMessageId; final MessageType? lastMessageType; int? firstUnreadMessageId; + final String? phone; String? get effectiveAvatarUrl { if (avatarFileId != null && avatarFileId!.isNotEmpty) { @@ -42,6 +43,7 @@ class Contact { this.lastMessageId, this.lastMessageType, this.firstUnreadMessageId, + this.phone, }); Contact copyWith({ @@ -60,6 +62,7 @@ class Contact { int? lastMessageId, MessageType? lastMessageType, int? firstUnreadMessageId, + String? phone, }) { return Contact( id: id ?? this.id, @@ -77,6 +80,7 @@ class Contact { lastMessageId: lastMessageId ?? this.lastMessageId, lastMessageType: lastMessageType ?? this.lastMessageType, firstUnreadMessageId: firstUnreadMessageId ?? this.firstUnreadMessageId, + phone: phone ?? this.phone, ); } @@ -104,6 +108,7 @@ class Contact { lastMessageId: int.tryParse((json['last_message_id'] ?? json['lastMessageId'] ?? 0).toString()) ?? 0, lastMessageType: MessageModel.parseMessageType(json['last_message_type'] ?? json['lastMessageType'] ?? 'text'), firstUnreadMessageId: int.tryParse((json['first_unread_message_id'] ?? json['firstUnreadMessageId'] ?? 0).toString()) ?? 0, + phone: json['phone'] ?? json['phone_number'], ); } } diff --git a/lib/logic/auth_provider.dart b/lib/logic/auth_provider.dart index 8f33ed1..31a065c 100644 --- a/lib/logic/auth_provider.dart +++ b/lib/logic/auth_provider.dart @@ -451,8 +451,12 @@ class AuthProvider extends ChangeNotifier { Future setupAccount( String firstName, String? lastName, - String masterPassword, - ) async { + String masterPassword, { + String? phone, + String? email, + String? about, + }) async { + _isLoading = true; notifyListeners(); try { @@ -476,7 +480,18 @@ class AuthProvider extends ChangeNotifier { ); if (response.statusCode == 200) { + final String currentUsername = _username ?? ''; + await _apiService.updateMe( + username: currentUsername, + firstName: firstName, + lastName: lastName ?? '', + phone: phone, + email: email, + about: about, + ); + _needsSetup = false; + await _checkAccountStatus(); notifyListeners(); return true; } else { @@ -487,6 +502,7 @@ class AuthProvider extends ChangeNotifier { print("Ошибка сети: $e"); return false; } finally { + _isLoading = false; notifyListeners(); } } diff --git a/lib/logic/contact_provider.dart b/lib/logic/contact_provider.dart index e65b118..980152e 100644 --- a/lib/logic/contact_provider.dart +++ b/lib/logic/contact_provider.dart @@ -243,6 +243,13 @@ class ContactProvider extends ChangeNotifier { try { final index = _contacts.indexWhere((c) => c.id == contactId); if (index != -1) { + if (lastMessage == null && lastMessageTime == null && lastMessageId == null) { + _contacts.removeAt(index); + print("Контакт $contactId удален из списка чатов, так как сообщений не осталось."); + _sortContacts(); + notifyListeners(); + return; + } final existing = _contacts[index]; String displayMessage; if (isEdited) { diff --git a/lib/presentation/screens/account_settings_screen.dart b/lib/presentation/screens/account_settings_screen.dart index 93c20ea..5404f6f 100644 --- a/lib/presentation/screens/account_settings_screen.dart +++ b/lib/presentation/screens/account_settings_screen.dart @@ -43,8 +43,8 @@ class _AccountSettingsScreenState extends State { super.dispose(); } - Future _save() async { - if (!_formKey.currentState!.validate()) return; + Future _save() async { + if (!_formKey.currentState!.validate()) return false; setState(() => _isSaving = true); try { final api = ApiService(); @@ -57,19 +57,16 @@ class _AccountSettingsScreenState extends State { about: _aboutController.text, ); - if (!mounted) return; + if (!mounted) return true; await context.read().refreshMe(); - - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Сохранено'), behavior: SnackBarBehavior.floating), - ); - Navigator.of(context).pop(); + return true; } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating), + ); + } + return false; } finally { if (mounted) setState(() => _isSaving = false); } @@ -79,32 +76,19 @@ class _AccountSettingsScreenState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colorScheme.background, - appBar: AppBar( - title: Text('Редактировать аккаунт', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)), - elevation: 0, - backgroundColor: Colors.transparent, - iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Center( - child: _isSaving - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2.5, color: colorScheme.primary), - ) - : TextButton.icon( - onPressed: _save, - icon: const Icon(Icons.done_rounded, size: 18), - label: const Text('Готово', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - ), - ], - ), + return WillPopScope( + onWillPop: () async { + final success = await _save(); + return success; + }, + child: Scaffold( + backgroundColor: colorScheme.background, + appBar: AppBar( + title: Text('Редактировать аккаунт', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)), + elevation: 0, + backgroundColor: Colors.transparent, + iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), + ), body: Form( key: _formKey, child: ListView( @@ -159,20 +143,23 @@ class _AccountSettingsScreenState extends State { label: 'О себе', hint: 'Расскажите немного о себе', icon: Icons.short_text_rounded, - maxLines: 4, + minLines: 1, + maxLines: 50, ), ], ), ), - ); - } + ), + ); +} Widget _buildInputField({ required TextEditingController controller, required String label, required String hint, required IconData icon, - int maxLines = 1, + int? minLines, + int? maxLines = 1, TextInputType? keyboardType, String? Function(String?)? validator, }) { @@ -188,6 +175,7 @@ class _AccountSettingsScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: TextFormField( controller: controller, + minLines: minLines, maxLines: maxLines, keyboardType: keyboardType, validator: validator, diff --git a/lib/presentation/screens/account_setup_screen.dart b/lib/presentation/screens/account_setup_screen.dart index cf0d798..3f55a81 100644 --- a/lib/presentation/screens/account_setup_screen.dart +++ b/lib/presentation/screens/account_setup_screen.dart @@ -18,6 +18,9 @@ class _AccountSetupScreenState extends State final _formKey = GlobalKey(); final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _aboutController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); @@ -46,6 +49,14 @@ class _AccountSetupScreenState extends State ); _animationController.forward(); + + // Заполняем поля данными, если они уже есть на сервере + final auth = context.read(); + _firstNameController.text = auth.firstName ?? ''; + _lastNameController.text = auth.lastName ?? ''; + _phoneController.text = auth.phone ?? ''; + _emailController.text = auth.email ?? ''; + _aboutController.text = auth.about ?? ''; } @override @@ -91,7 +102,7 @@ class _AccountSetupScreenState extends State ), const SizedBox(height: 12), Text( - "Укажите ваше имя и создайте мастер-пароль", + "Укажите ваши данные и создайте мастер-пароль", textAlign: TextAlign.center, style: TextStyle( fontSize: 16, @@ -102,7 +113,7 @@ class _AccountSetupScreenState extends State ), ), ), - const SizedBox(height: 48), + const SizedBox(height: 36), // Поле Имя _buildTextField( @@ -120,6 +131,33 @@ class _AccountSetupScreenState extends State label: "Фамилия", icon: Icons.person_outline, ), + const SizedBox(height: 16), + + // Поле Телефон + _buildTextField( + controller: _phoneController, + label: "Телефон", + icon: Icons.phone_android_outlined, + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 16), + + // Поле Почта + _buildTextField( + controller: _emailController, + label: "Электронная почта", + icon: Icons.mail_outline_rounded, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + + // Поле О себе + _buildTextField( + controller: _aboutController, + label: "О себе", + icon: Icons.info_outline_rounded, + maxLines: 3, + ), const SizedBox(height: 24), // Поле Мастер-пароль @@ -153,7 +191,7 @@ class _AccountSetupScreenState extends State return null; }, ), - const SizedBox(height: 40), + const SizedBox(height: 32), // Кнопка Продолжить ElevatedButton( @@ -201,6 +239,8 @@ class _AccountSetupScreenState extends State required IconData icon, String? Function(String?)? validator, bool obscureText = false, + TextInputType? keyboardType, + int maxLines = 1, }) { final colorScheme = Theme.of(context).colorScheme; @@ -221,6 +261,8 @@ class _AccountSetupScreenState extends State controller: controller, validator: validator, obscureText: obscureText, + keyboardType: keyboardType, + maxLines: maxLines, decoration: InputDecoration( labelText: label, labelStyle: TextStyle(color: colorScheme.outline), @@ -247,6 +289,9 @@ class _AccountSetupScreenState extends State _firstNameController.text.trim(), _lastNameController.text.trim(), _passwordController.text, + phone: _phoneController.text.trim(), + email: _emailController.text.trim(), + about: _aboutController.text.trim(), ); if (mounted) { @@ -272,6 +317,9 @@ class _AccountSetupScreenState extends State void dispose() { _firstNameController.dispose(); _lastNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _aboutController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); _animationController.dispose(); diff --git a/lib/presentation/screens/chat_screen.dart b/lib/presentation/screens/chat_screen.dart index 870b64b..466229f 100644 --- a/lib/presentation/screens/chat_screen.dart +++ b/lib/presentation/screens/chat_screen.dart @@ -41,6 +41,7 @@ import 'package:drift/drift.dart' as drift; import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:flutter_contacts/flutter_contacts.dart' as fc; class ChatScreen extends StatefulWidget { final Contact contact; @@ -230,14 +231,48 @@ class _ChatScreenState extends State with RouteAware { final String? savedSurname = prefs.getString( 'lastname_${_currentContact.id}', ); - print('Загружены имя $savedName, $savedSurname'); + + String? phoneBookName; + if ((savedName == null && savedSurname == null) && + _currentContact.phone != null && + (Platform.isAndroid || Platform.isIOS)) { + try { + final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true); + if (hasPermission) { + final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true); + String normalizePhone(String p) { + final digits = p.replaceAll(RegExp(r'\D'), ''); + if (digits.length >= 10) { + return digits.substring(digits.length - 10); + } + return digits; + } + final normTarget = normalizePhone(_currentContact.phone!); + if (normTarget.isNotEmpty) { + for (var dc in deviceContacts) { + for (var phoneObj in dc.phones) { + if (normalizePhone(phoneObj.number) == normTarget) { + phoneBookName = dc.displayName; + break; + } + } + if (phoneBookName != null) break; + } + } + } + } catch (e) { + print("Ошибка получения имени контакта из телефонной книги в чате: $e"); + } + } + if (mounted) { setState(() { - if (savedName != null) { - _currentContact.name = savedName; - } - if (savedSurname != null) { - _currentContact.surname = savedSurname; + if (savedName != null || savedSurname != null) { + _currentContact.name = savedName ?? ''; + _currentContact.surname = savedSurname ?? ''; + } else if (phoneBookName != null && phoneBookName!.isNotEmpty) { + _currentContact.name = phoneBookName!; + _currentContact.surname = ''; } }); } diff --git a/lib/presentation/screens/contacts_screen.dart b/lib/presentation/screens/contacts_screen.dart index 817f241..b8e18bd 100644 --- a/lib/presentation/screens/contacts_screen.dart +++ b/lib/presentation/screens/contacts_screen.dart @@ -31,6 +31,7 @@ import 'admin_broadcast_screen.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_contacts/flutter_contacts.dart' as fc; class ContactsScreen extends StatefulWidget { final int? targetChatId; @@ -71,6 +72,7 @@ class _ContactsScreenState extends State with RouteAware { // Хранилище стабильно загруженных локальных имён Map _localFullNames = {}; + Map _phoneBookNames = {}; final Map _pendingUnreadCounters = {}; @@ -128,11 +130,64 @@ class _ContactsScreenState extends State with RouteAware { }); } + Future _loadPhoneBookNames() async { + if (!Platform.isAndroid && !Platform.isIOS) return; + try { + final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true); + if (!hasPermission) return; + + final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true); + final contactProvider = context.read(); + + final Map matchedNames = {}; + + String normalizePhone(String p) { + final digits = p.replaceAll(RegExp(r'\D'), ''); + if (digits.length >= 10) { + return digits.substring(digits.length - 10); + } + return digits; + } + + final Map phoneToContactId = {}; + for (var contact in contactProvider.contacts) { + if (contact.phone != null) { + final norm = normalizePhone(contact.phone!); + if (norm.isNotEmpty) { + phoneToContactId[norm] = contact.id; + } + } + } + + for (var dc in deviceContacts) { + for (var phoneObj in dc.phones) { + final norm = normalizePhone(phoneObj.number); + if (norm.isNotEmpty && phoneToContactId.containsKey(norm)) { + final contactId = phoneToContactId[norm]!; + if (dc.displayName.isNotEmpty) { + matchedNames[contactId] = dc.displayName; + } + break; + } + } + } + + if (mounted) { + setState(() { + _phoneBookNames = matchedNames; + }); + } + } catch (e) { + print("Ошибка загрузки имен из телефонной книги: $e"); + } + } + Future _initContacts() async { if (_contactsLoaded) return; final contactProvider = context.read(); await contactProvider.loadContacts(); - await _loadLocalNames(); // Гарантированный вызов после загрузки контактов + await _loadLocalNames(); + await _loadPhoneBookNames(); print('Contacts loaded, checking targetChatId: ${widget.targetChatId}'); @@ -404,10 +459,13 @@ class _ContactsScreenState extends State with RouteAware { Widget _buildSearchResultTile(Contact contact, {required bool isChat}) { final localName = _localFullNames[contact.id]; + final phoneBookName = _phoneBookNames[contact.id]; final displayName = (localName != null && localName.isNotEmpty) ? localName - : '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}' - .trim(); + : (phoneBookName != null && phoneBookName.isNotEmpty) + ? phoneBookName + : '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}' + .trim(); final title = displayName.isNotEmpty ? displayName : contact.username; final subtitle = contact.username.isNotEmpty ? '@${contact.username}' @@ -560,10 +618,13 @@ class _ContactsScreenState extends State with RouteAware { final isSelected = _selectedContact?.id == contact.id; final localName = _localFullNames[contact.id]; + final phoneBookName = _phoneBookNames[contact.id]; final displayName = (localName != null && localName.isNotEmpty) ? localName - : '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}' - .trim(); + : (phoneBookName != null && phoneBookName.isNotEmpty) + ? phoneBookName + : '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}' + .trim(); final contactInitials = displayName.isNotEmpty ? displayName @@ -952,10 +1013,8 @@ class _ContactsScreenState extends State with RouteAware { SafeArea(top: false, child: _buildUpdateBanner(isPhone)), ], ), - floatingActionButton: null, - /* (isCollapsed || (isPhone && _currentIndex != 0)) - ? null - : AnimatedPadding( + floatingActionButton: (Platform.isAndroid && isPhone && _currentIndex == 0) + ? AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeInOut, padding: EdgeInsets.only( @@ -972,7 +1031,8 @@ class _ContactsScreenState extends State with RouteAware { ), child: const Icon(Icons.edit_note_rounded), ), - ),*/ + ) + : null, bottomNavigationBar: isPhone ? BottomNavigationBar( currentIndex: _currentIndex, @@ -1825,6 +1885,20 @@ class _ContactsScreenState extends State with RouteAware { } } + if (data['type'] == 'message_deleted') { + final messageId = int.tryParse(data['message_id']?.toString() ?? ''); + if (messageId != null) { + final contactIndex = contactProvider.contacts.indexWhere( + (c) => c.lastMessageId == messageId, + ); + if (contactIndex != -1) { + final contactId = contactProvider.contacts[contactIndex].id; + await contactProvider.refreshContactLastMessage(contactId); + } + } + return; + } + if (data['type'] == 'user_updated') { final userId = int.tryParse(data['user_id']?.toString() ?? ''); if (userId != null) { diff --git a/lib/presentation/screens/new_chat_screen.dart b/lib/presentation/screens/new_chat_screen.dart index d12f136..5814fdc 100644 --- a/lib/presentation/screens/new_chat_screen.dart +++ b/lib/presentation/screens/new_chat_screen.dart @@ -1,8 +1,14 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart' as fc; +import 'package:url_launcher/url_launcher.dart'; import 'package:provider/provider.dart'; -import '/logic/contact_provider.dart'; +import 'package:share_plus/share_plus.dart'; import '/logic/auth_provider.dart'; +import '/data/models/contact_model.dart'; +import '/data/repositories/contact_repository.dart'; import 'chat_screen.dart'; +import 'package:cached_network_image/cached_network_image.dart'; class NewChatScreen extends StatefulWidget { const NewChatScreen({super.key}); @@ -12,52 +18,493 @@ class NewChatScreen extends StatefulWidget { } class _NewChatScreenState extends State { + bool _isLoading = true; + bool _permissionDenied = false; + String? _error; + + List> _registeredMatches = []; + List _unregisteredMatches = []; + @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - final authProvider = context.read(); - final contactProvider = context.read(); - - // Установить текущего пользователя и загрузить все контакты - contactProvider.setCurrentUserId(authProvider.currentUserId); - contactProvider.loadAllContactsForNewChat(); + _requestPermissionAndLoad(); + } + + String _normalizePhone(String p) { + final digits = p.replaceAll(RegExp(r'\D'), ''); + if (digits.length >= 10) { + return digits.substring(digits.length - 10); + } + return digits; + } + + Future _requestPermissionAndLoad() async { + setState(() { + _isLoading = true; + _permissionDenied = false; + _error = null; }); + try { + // 1. Проверяем разрешение на доступ к контактам (запрашиваем при необходимости) + bool permission = await fc.FlutterContacts.requestPermission(readonly: true); + if (!permission) { + setState(() { + _permissionDenied = true; + _isLoading = false; + }); + return; + } + + // 2. Разрешение получено, загружаем контакты устройства + final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true); + + // 3. Загружаем список зарегистрированных пользователей из нашего репозитория + final contactRepository = ContactRepository(); + final registeredUsers = await contactRepository.fetchAllUsers(); + + // Диагностика для отладки сопоставления контактов + debugPrint('--- NEW CHAT CONTACT MATCHING DEBUG ---'); + debugPrint('Total registered users from server: ${registeredUsers.length}'); + for (var u in registeredUsers) { + debugPrint(' Server User: ID=${u.id}, Username=${u.username}, Phone="${u.phone}"'); + } + debugPrint('Total device contacts: ${deviceContacts.length}'); + for (var dc in deviceContacts) { + final phonesStr = dc.phones.map((p) => '${p.number} (norm: ${_normalizePhone(p.number)})').join(', '); + debugPrint(' Device Contact: Name="${dc.displayName}", Phones=[$phonesStr]'); + } + debugPrint('---------------------------------------'); + + // 4. Группируем и сопоставляем контакты по последним 10 цифрам номера телефона + final Map registeredMap = {}; + for (var u in registeredUsers) { + if (u.phone != null) { + final norm = _normalizePhone(u.phone!); + if (norm.isNotEmpty) { + registeredMap[norm] = u; + } + } + } + + final List> regMatches = []; + final List unregMatches = []; + + for (var dc in deviceContacts) { + bool matched = false; + for (var phoneObj in dc.phones) { + final norm = _normalizePhone(phoneObj.number); + if (norm.isNotEmpty && registeredMap.containsKey(norm)) { + final regUser = registeredMap[norm]!; + regMatches.add({ + 'registered': regUser, + 'device': dc, + }); + matched = true; + break; + } + } + if (!matched && dc.phones.isNotEmpty) { + unregMatches.add(dc); + } + } + + // Сортировка по имени в алфавитном порядке + regMatches.sort((a, b) => (a['device'] as fc.Contact).displayName.toLowerCase().compareTo((b['device'] as fc.Contact).displayName.toLowerCase())); + unregMatches.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); + + setState(() { + _registeredMatches = regMatches; + _unregisteredMatches = unregMatches; + _isLoading = false; + }); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + Future _sendInvitation(String phoneNumber) async { + final message = 'Привет! Присоединяйся к бета-тесту мессенджера Чепухаграм. Напиши разработчику в Telegram @ArturKarasevich для получения доступа!'; + final uri = Uri.parse('sms:$phoneNumber?body=${Uri.encodeComponent(message)}'); + try { + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw Exception('Не удалось открыть SMS приложение'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка отправки: $e')), + ); + } + } + } + + Future _sendNativeShare(String name) async { + final message = 'Привет! Присоединяйся к бета-тесту мессенджера Чепухаграм. Напиши разработчику в Telegram @ArturKarasevich для получения доступа!'; + try { + await Share.share( + message, + subject: 'Приглашение в Чепухаграм', + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка отправки: $e')), + ); + } + } + } + + Future _showInviteDialog(String name, String phone) async { + final colorScheme = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + title: Text('Пригласить $name', style: const TextStyle(fontWeight: FontWeight.bold)), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Этого человека ещё нет в Чепухаграм. Выберите способ приглашения:', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + const SizedBox(height: 20), + ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(Icons.sms_rounded, color: colorScheme.primary, size: 20), + ), + title: const Text('Отправить SMS', style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: Text(phone, style: const TextStyle(fontSize: 12)), + onTap: () { + Navigator.pop(context); + _sendInvitation(phone); + }, + ), + const SizedBox(height: 8), + ListTile( + contentPadding: EdgeInsets.zero, + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(Icons.share_rounded, color: colorScheme.primary, size: 20), + ), + title: const Text('Другие мессенджеры', style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: const Text('Нативное меню выбора (Telegram, WhatsApp и др.)', style: TextStyle(fontSize: 12)), + onTap: () { + Navigator.pop(context); + _sendNativeShare(name); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Отмена'), + ), + ], + ), + ); + } + + Widget _buildHeader(String title, ColorScheme colorScheme) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + title.toUpperCase(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: colorScheme.primary, + letterSpacing: 1.0, + ), + ), + ); + } + + Widget _buildList(ColorScheme colorScheme) { + if (_registeredMatches.isEmpty && _unregisteredMatches.isEmpty) { + return const Center(child: Text('Контакты в телефонной книге не найдены.')); + } + + final List listItems = []; + + if (_registeredMatches.isNotEmpty) { + listItems.add(_buildHeader('Контакты в Чепухаграм', colorScheme)); + for (int i = 0; i < _registeredMatches.length; i++) { + final m = _registeredMatches[i]; + final Contact regUser = m['registered']; + final fc.Contact devContact = m['device']; + + final String dispName = devContact.displayName.isNotEmpty + ? devContact.displayName + : regUser.name; + + final initials = dispName.isNotEmpty + ? dispName + .trim() + .split(RegExp(r'\s+')) + .take(2) + .map((e) => e.isNotEmpty ? e[0].toUpperCase() : '') + .join() + : '?'; + + listItems.add( + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorScheme.primaryContainer, + ), + child: ClipOval( + child: Stack( + alignment: Alignment.center, + children: [ + Text( + initials, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onPrimaryContainer, + ), + ), + if (regUser.effectiveAvatarUrl != null && regUser.effectiveAvatarUrl!.isNotEmpty) + CachedNetworkImage( + imageUrl: regUser.effectiveAvatarUrl!, + fit: BoxFit.cover, + width: 40, + height: 40, + placeholder: (context, url) => const SizedBox.shrink(), + errorWidget: (context, url, error) => const SizedBox.shrink(), + ), + ], + ), + ), + ), + title: Text( + dispName, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), + ), + subtitle: Text( + '@${regUser.username} • ${regUser.phone ?? ''}', + style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 13), + ), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'В Чепухаграм', + style: TextStyle(color: Colors.green, fontSize: 11, fontWeight: FontWeight.bold), + ), + ), + onTap: () { + // Открываем чат с этим контактом + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ChatScreen(contact: regUser), + ), + ); + }, + ), + ); + if (i < _registeredMatches.length - 1) { + listItems.add( + Divider( + height: 1, + indent: 72, + endIndent: 16, + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ); + } + } + listItems.add(const SizedBox(height: 16)); + } + + if (_unregisteredMatches.isNotEmpty) { + listItems.add(_buildHeader('Пригласить в Чепухаграм', colorScheme)); + for (int i = 0; i < _unregisteredMatches.length; i++) { + final dc = _unregisteredMatches[i]; + final phoneNum = dc.phones.isNotEmpty ? dc.phones.first.number : ''; + + final initials = dc.displayName.isNotEmpty + ? dc.displayName + .trim() + .split(RegExp(r'\s+')) + .take(2) + .map((e) => e.isNotEmpty ? e[0].toUpperCase() : '') + .join() + : '?'; + + listItems.add( + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: CircleAvatar( + backgroundColor: colorScheme.surfaceVariant.withOpacity(0.4), + foregroundColor: colorScheme.outline, + child: Text( + initials, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + title: Text( + dc.displayName, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), + ), + subtitle: Text( + phoneNum, + style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 13), + ), + trailing: OutlinedButton( + onPressed: () => _showInviteDialog(dc.displayName, phoneNum), + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.primary, + side: BorderSide(color: colorScheme.primary.withOpacity(0.5)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('Пригласить', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), + ), + onTap: () => _showInviteDialog(dc.displayName, phoneNum), + ), + ); + if (i < _unregisteredMatches.length - 1) { + listItems.add( + Divider( + height: 1, + indent: 72, + endIndent: 16, + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ); + } + } + } + + return ListView( + physics: const BouncingScrollPhysics(), + children: listItems, + ); } @override Widget build(BuildContext context) { - final contactProvider = context.watch(); + final colorScheme = Theme.of(context).colorScheme; return Scaffold( + backgroundColor: colorScheme.background, appBar: AppBar( - title: const Text('Новый чат'), + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + iconTheme: IconThemeData(color: colorScheme.onBackground), + title: Text( + 'Новый чат', + style: TextStyle( + color: colorScheme.onBackground, + fontWeight: FontWeight.w800, + fontSize: 24, + letterSpacing: -0.5, + ), + ), + centerTitle: false, ), - body: contactProvider.isLoading + body: _isLoading ? const Center(child: CircularProgressIndicator()) - : contactProvider.error != null - ? Center(child: Text('Error: ${contactProvider.error}')) - : ListView.builder( - itemCount: contactProvider.allContacts.length, - itemBuilder: (context, index) { - final contact = contactProvider.allContacts[index]; - return ListTile( - leading: CircleAvatar( - child: Text(contact.name[0]), - ), - title: Text(contact.name), - onTap: () { - // Создать чат с этим контактом - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ChatScreen(contact: contact), + : _permissionDenied + ? Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.contacts_rounded, + size: 80, + color: colorScheme.outline.withOpacity(0.4), + ), + const SizedBox(height: 24), + const Text( + 'Для поиска контактов необходимо разрешение', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + const Text( + 'Чепухаграм сопоставит номера из вашей телефонной книги, чтобы вы могли общаться с друзьями.', + style: TextStyle(color: Colors.grey, fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _requestPermissionAndLoad, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), - ); - }, - ); - }, - ), + child: const Text('Предоставить доступ', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ), + ), + ) + : _error != null + ? Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline_rounded, size: 60, color: colorScheme.error), + const SizedBox(height: 16), + Text('Ошибка: $_error', textAlign: TextAlign.center), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _requestPermissionAndLoad, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + child: const Text('Повторить попытку', style: TextStyle(fontWeight: FontWeight.bold)), + ), + ], + ), + ), + ) + : _buildList(colorScheme), ); } } \ No newline at end of file diff --git a/lib/presentation/screens/privacy_settings_screen.dart b/lib/presentation/screens/privacy_settings_screen.dart index c147e19..d616603 100644 --- a/lib/presentation/screens/privacy_settings_screen.dart +++ b/lib/presentation/screens/privacy_settings_screen.dart @@ -87,12 +87,6 @@ class _PrivacySettingsScreenState extends State { await _savePreference(_showAvatarKey, _showAvatar); await _savePreference(_showAboutKey, _showAbout); await _savePreference(_showLastOnlineKey, _showLastOnline); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Настройки видимости сохранены'), behavior: SnackBarBehavior.floating), - ); - } } } catch (e) { if (mounted) { @@ -109,28 +103,19 @@ class _PrivacySettingsScreenState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colorScheme.background, - appBar: AppBar( - title: Text('Видимость данных', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)), - elevation: 0, - backgroundColor: Colors.transparent, - iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Center( - child: _isSaving - ? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary)) - : TextButton.icon( - onPressed: _saveToServer, - icon: const Icon(Icons.save_rounded, size: 18), - label: const Text('Сохранить'), - ), - ), - ), - ], - ), + return WillPopScope( + onWillPop: () async { + await _saveToServer(); + return true; + }, + child: Scaffold( + backgroundColor: colorScheme.background, + appBar: AppBar( + title: Text('Видимость данных', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)), + elevation: 0, + backgroundColor: Colors.transparent, + iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), + ), body: ListView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), @@ -182,8 +167,9 @@ class _PrivacySettingsScreenState extends State { ), ], ), - ); - } + ), + ); +} Widget _buildSwitchTile(String title, bool value, ValueChanged onChanged) { return SwitchListTile( diff --git a/lib/presentation/screens/security_settings_screen.dart b/lib/presentation/screens/security_settings_screen.dart index fc4f225..79a906d 100644 --- a/lib/presentation/screens/security_settings_screen.dart +++ b/lib/presentation/screens/security_settings_screen.dart @@ -556,7 +556,7 @@ class _SecuritySettingsScreenState extends State { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Обновить ключ шифрования'), + : const Text('Обновить пароль шифрования'), ), ), ], diff --git a/lib/presentation/screens/user_profile_screen.dart b/lib/presentation/screens/user_profile_screen.dart index 7790a98..7513301 100644 --- a/lib/presentation/screens/user_profile_screen.dart +++ b/lib/presentation/screens/user_profile_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '/core/constants.dart'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_contacts/flutter_contacts.dart' as fc; class UserProfileScreen extends StatefulWidget { final int userId; @@ -35,6 +36,7 @@ class _UserProfileScreenState extends State { Timer? _onlineTimer; String? firstName; String? lastName; + String? _phoneBookName; bool _isAvatarExpanded = false; @override @@ -62,9 +64,45 @@ class _UserProfileScreenState extends State { final prefs = await SharedPreferences.getInstance(); firstName = prefs.getString('firstname_${widget.userId}'); lastName = prefs.getString('lastname_${widget.userId}'); + + String? phoneBookName; + final phone = data['phone']?.toString(); + if ((firstName == null && lastName == null) && + phone != null && + (Platform.isAndroid || Platform.isIOS)) { + try { + final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true); + if (hasPermission) { + final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true); + String normalizePhone(String p) { + final digits = p.replaceAll(RegExp(r'\D'), ''); + if (digits.length >= 10) { + return digits.substring(digits.length - 10); + } + return digits; + } + final normTarget = normalizePhone(phone); + if (normTarget.isNotEmpty) { + for (var dc in deviceContacts) { + for (var phoneObj in dc.phones) { + if (normalizePhone(phoneObj.number) == normTarget) { + phoneBookName = dc.displayName; + break; + } + } + if (phoneBookName != null) break; + } + } + } + } catch (e) { + print("Ошибка получения имени контакта из телефонной книги в профиле: $e"); + } + } + if (mounted) { setState(() { _userData = data; + _phoneBookName = phoneBookName; _isLoading = false; }); } @@ -211,8 +249,10 @@ class _UserProfileScreenState extends State { if (_userData == null) return const SizedBox.shrink(); final colorScheme = Theme.of(context).colorScheme; - final String displayFN = firstName ?? _userData?['first_name'] ?? ''; - final String displayLN = lastName ?? _userData?['last_name'] ?? ''; + final String displayFN = firstName ?? _phoneBookName ?? _userData?['first_name'] ?? ''; + final String displayLN = (firstName == null && _phoneBookName != null) + ? '' + : (lastName ?? _userData?['last_name'] ?? ''); final String combinedName = '$displayFN $displayLN'.trim(); final String username = _userData?['username'] ?? ''; final rawAvatarUrl = _userData?['avatar_url']?.toString(); diff --git a/lib/presentation/widgets/message_bubble.dart b/lib/presentation/widgets/message_bubble.dart index 4f5b106..2283b48 100644 --- a/lib/presentation/widgets/message_bubble.dart +++ b/lib/presentation/widgets/message_bubble.dart @@ -74,6 +74,7 @@ class _MessageBubbleState extends State { Future? _delayFuture; TextSelection? _currentSelection; + TapDownDetails? _lastTapDownDetails; final MediaCacheManager _mediaCache = MediaCacheManager(); @@ -737,7 +738,16 @@ class _MessageBubbleState extends State { child: Material( color: Colors.transparent, child: InkWell( - onTap: widget.onTap, + onTapDown: (details) { + _lastTapDownDetails = details; + }, + onTap: () { + if (_lastTapDownDetails != null) { + _showContextMenu(_lastTapDownDetails!); + } else { + widget.onTap?.call(); + } + }, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c7352d6..5c3d3b2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -25,6 +25,7 @@ import package_info_plus import path_provider_foundation import photo_manager import record_macos +import share_plus import shared_preferences_foundation import sqflite_darwin import sqlite3_flutter_libs @@ -53,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 58e7175..f271bf9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -638,6 +638,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_contacts: + dependency: "direct main" + description: + name: flutter_contacts + sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" flutter_image_compress: dependency: "direct main" description: @@ -1164,10 +1172,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" mobile_scanner: dependency: "direct main" description: @@ -1520,6 +1528,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + url: "https://pub.dev" + source: hosted + version: "3.4.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4ebc3eb..9c33099 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,8 @@ dependencies: scrollable_positioned_list: ^0.3.8 qr_flutter: ^4.1.0 mobile_scanner: ^7.2.0 + flutter_contacts: ^1.1.6 + share_plus: ^7.2.1 dev_dependencies: flutter_test: diff --git a/srv/app/api/endpoints/users.py b/srv/app/api/endpoints/users.py index 6af3aba..07f16ff 100644 --- a/srv/app/api/endpoints/users.py +++ b/srv/app/api/endpoints/users.py @@ -216,6 +216,7 @@ async def get_privacy_settings(current_user: models.User = Depends(get_current_u @usersRouter.get("/all") async def read_users_all( + request: Request, current_user: models.User = Depends(get_current_user), db: AsyncSession = Depends(get_db), query: Optional[str] = None, @@ -233,7 +234,18 @@ async def read_users_all( users_for_return.append(user) else: users_for_return = users - return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users_for_return] + return [ + { + "id": user.id, + "username": user.username, + "name": f"{user.first_name} {user.last_name or ''}".strip(), + "public_key": user.public_key, + "phone": user.phone, + "avatar_file_id": user.avatar_file_id if (user.show_avatar or current_user.id == 1) else None, + "avatar_url": str(request.url_for("get_file", file_id=user.avatar_file_id)) if (user.show_avatar or current_user.id == 1) and user.avatar_file_id else None, + } + for user in users_for_return + ] @usersRouter.get("/chats") async def read_users_chats( @@ -296,6 +308,7 @@ async def read_users_chats( "online": str(user.id) in connection_manager.manager.online_users, "last_message_id": last_msg.id if last_msg else None, "last_message_type": last_msg.message_type if last_msg else None, + "phone": user.phone, } ) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 776055f..59e1977 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index dd3990e..f729dae 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST local_auth_windows permission_handler_windows record_windows + share_plus sqlite3_flutter_libs url_launcher_windows video_player_win