import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; class JoinGroupScreen extends StatefulWidget { const JoinGroupScreen({super.key}); @override State createState() => _JoinGroupScreenState(); } class _JoinGroupScreenState extends State { final TextEditingController _linkController = TextEditingController(); StreamSubscription? _apiSubscription; bool _isLoading = false; @override void initState() { super.initState(); _listenToApiMessages(); } @override void dispose() { _linkController.dispose(); _apiSubscription?.cancel(); super.dispose(); } void _listenToApiMessages() { _apiSubscription = ApiService.instance.messages.listen((message) { if (!mounted) return; if (message['type'] == 'group_join_success') { setState(() { _isLoading = false; }); final payload = message['payload']; final chat = payload['chat']; final chatTitle = chat?['title'] ?? 'Группа'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Успешно присоединились к группе "$chatTitle"!'), backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); Navigator.of(context).pop(); } if (message['cmd'] == 3 && message['opcode'] == 57) { setState(() { _isLoading = false; }); final errorPayload = message['payload']; String errorMessage = 'Неизвестная ошибка'; if (errorPayload != null) { if (errorPayload['localizedMessage'] != null) { errorMessage = errorPayload['localizedMessage']; } else if (errorPayload['message'] != null) { errorMessage = errorPayload['message']; } } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: Theme.of(context).colorScheme.error, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } }); } String _normalizeLink(String inputLink) { String link = inputLink.trim(); // Поддержка формата @https://max.ru/... if (link.startsWith('@')) { link = link.substring(1).trim(); } return link; } String _extractJoinLink(String inputLink) { final link = _normalizeLink(inputLink); if (link.startsWith('join/')) { print('Ссылка уже в правильном формате: $link'); return link; } final joinIndex = link.indexOf('join/'); if (joinIndex != -1) { final extractedLink = link.substring(joinIndex); print('Извлечена ссылка из полной ссылки: $link -> $extractedLink'); return extractedLink; } print('Не найдено "join/" в ссылке: $link'); return link; } bool _isChannelLink(String inputLink) { final link = _normalizeLink(inputLink); try { final uri = Uri.parse(link); if (uri.host == 'max.ru' && uri.pathSegments.isNotEmpty && uri.pathSegments.first.startsWith('id')) { return true; } } catch (_) {} return false; } void _joinGroup() async { final rawInput = _linkController.text.trim(); final inputLink = _normalizeLink(rawInput); if (inputLink.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Введите ссылку'), backgroundColor: Colors.orange, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); return; } // Сначала пытаемся распознать ссылку на канал (https://max.ru/id...) if (_isChannelLink(inputLink)) { setState(() { _isLoading = true; }); try { final chatInfo = await ApiService.instance.getChatInfoByLink(inputLink); if (!mounted) return; setState(() { _isLoading = false; }); // Показываем диалог подписки на канал _showChannelSubscribeDialog(chatInfo, inputLink); } catch (e) { if (!mounted) return; setState(() { _isLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка: ${e.toString()}'), backgroundColor: Theme.of(context).colorScheme.error, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } return; } // Иначе считаем, что это ссылка на группу с "join/" final processedLink = _extractJoinLink(inputLink); if (!processedLink.contains('join/')) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Неверный формат ссылки. Для группы ссылка должна содержать "join/"', ), backgroundColor: Colors.orange, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); return; } setState(() { _isLoading = true; }); try { await ApiService.instance.joinGroupByLink(processedLink); } catch (e) { setState(() { _isLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка присоединения: ${e.toString()}'), backgroundColor: Theme.of(context).colorScheme.error, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: const Text('Присоединиться по ссылке'), backgroundColor: colors.surface, foregroundColor: colors.onSurface, ), body: Stack( children: [ SingleChildScrollView( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.primaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.link, color: colors.primary), const SizedBox(width: 8), Text( 'Присоединение по ссылке', style: TextStyle( fontWeight: FontWeight.bold, color: colors.primary, ), ), ], ), const SizedBox(height: 8), Text( 'Введите ссылку на группу или канал, чтобы присоединиться. ' 'Для групп можно вводить полную (https://max.ru/join/...) ' 'или короткую (join/...) ссылку, для каналов — ссылку вида ' 'https://max.ru/idXXXXXXXX.', style: TextStyle(color: colors.onSurfaceVariant), ), ], ), ), const SizedBox(height: 24), Text( 'Ссылка', style: Theme.of( context, ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), TextField( controller: _linkController, decoration: InputDecoration( labelText: 'Ссылка на группу или канал', hintText: 'https://max.ru/join/ABC123DEF456GHI789JKL или https://max.ru/id7452017130_gos', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), prefixIcon: const Icon(Icons.link), ), maxLines: 3, ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), border: Border.all(color: colors.outline.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.info_outline, size: 16, color: colors.primary, ), const SizedBox(width: 8), Text( 'Формат ссылки:', style: TextStyle( fontWeight: FontWeight.w600, color: colors.primary, fontSize: 14, ), ), ], ), const SizedBox(height: 8), Text( '• Для групп: ссылка должна содержать "join/"\n' '• После "join/" должен идти уникальный идентификатор группы\n' '• Примеры групп:\n' ' - https://max.ru/join/ABC123DEF456GHI789JKL\n' ' - join/ABC123DEF456GHI789JKL\n' '• Для каналов: ссылка вида https://max.ru/idXXXXXXXX', style: TextStyle( color: colors.onSurfaceVariant, fontSize: 13, height: 1.4, ), ), ], ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isLoading ? null : _joinGroup, icon: _isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( colors.onPrimary, ), ), ) : const Icon(Icons.link), label: Text( _isLoading ? 'Присоединение...' : 'Присоединиться по ссылке', ), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ), if (_isLoading) Container( color: Colors.black.withOpacity(0.5), child: const Center(child: CircularProgressIndicator()), ), ], ), ); } void _showChannelSubscribeDialog( Map chatInfo, String linkToJoin, ) { final String title = chatInfo['title'] ?? 'Канал'; final String? iconUrl = chatInfo['baseIconUrl'] ?? chatInfo['baseUrl'] ?? chatInfo['iconUrl']; int subscribeState = 0; String? errorMessage; showDialog( context: context, barrierDismissible: false, builder: (dialogContext) { return StatefulBuilder( builder: (context, setState) { Widget content; List actions = []; if (subscribeState == 1) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const CircularProgressIndicator(), const SizedBox(height: 24), Text( 'Подписка...', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = []; } else if (subscribeState == 2) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const Icon( Icons.check_circle_outline, color: Colors.green, size: 60, ), const SizedBox(height: 24), Text( 'Вы подписались на канал!', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = [ FilledButton( child: const Text('Отлично'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else if (subscribeState == 3) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), Icon( Icons.error_outline, color: Theme.of(context).colorScheme.error, size: 60, ), const SizedBox(height: 24), Text( 'Ошибка', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( errorMessage ?? 'Не удалось подписаться на канал.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 32), ], ); actions = [ TextButton( child: const Text('Закрыть'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else { content = Column( mainAxisSize: MainAxisSize.min, children: [ if (iconUrl != null && iconUrl.isNotEmpty) CircleAvatar( radius: 60, backgroundImage: NetworkImage(iconUrl), onBackgroundImageError: (e, s) { print("Ошибка загрузки аватара канала: $e"); }, backgroundColor: Colors.grey.shade300, ) else CircleAvatar( radius: 60, backgroundColor: Colors.grey.shade300, child: const Icon( Icons.campaign, size: 60, color: Colors.white, ), ), const SizedBox(height: 24), Text( title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), const Text( 'Вы действительно хотите подписаться на этот канал?', textAlign: TextAlign.center, ), ], ); actions = [ TextButton( child: const Text('Отмена'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), FilledButton( child: const Text('Подписаться'), onPressed: () async { setState(() { subscribeState = 1; }); try { await ApiService.instance.subscribeToChannel(linkToJoin); setState(() { subscribeState = 2; }); await Future.delayed(const Duration(seconds: 2)); if (mounted) { Navigator.of(dialogContext).pop(); } } catch (e) { setState(() { subscribeState = 3; errorMessage = e.toString().replaceFirst( "Exception: ", "", ); }); } }, ), ]; } return AlertDialog( title: subscribeState == 0 ? const Text('Подписаться на канал?') : null, content: AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), transitionBuilder: (Widget child, Animation animation) { final slideAnimation = Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutQuart, ), ); return FadeTransition( opacity: animation, child: SlideTransition( position: slideAnimation, child: child, ), ); }, child: Container( key: ValueKey(subscribeState), child: content, ), ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), actionsAlignment: MainAxisAlignment.center, actions: actions, ); }, ); }, ); } }