import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/models/channel.dart'; class SearchChannelsScreen extends StatefulWidget { const SearchChannelsScreen({super.key}); @override State createState() => _SearchChannelsScreenState(); } class _SearchChannelsScreenState extends State { final TextEditingController _searchController = TextEditingController(); StreamSubscription? _apiSubscription; bool _isLoading = false; List _foundChannels = []; String? _errorMessage; @override void initState() { super.initState(); _listenToApiMessages(); } @override void dispose() { _searchController.dispose(); _apiSubscription?.cancel(); super.dispose(); } void _listenToApiMessages() { _apiSubscription = ApiService.instance.messages.listen((message) { if (!mounted) return; if (message['type'] == 'channels_found') { setState(() { _isLoading = false; _errorMessage = null; }); final payload = message['payload']; final channelsData = payload['contacts'] as List?; if (channelsData != null) { _foundChannels = channelsData .map((channelJson) => Channel.fromJson(channelJson)) .toList(); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Найдено каналов: ${_foundChannels.length}'), backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } if (message['type'] == 'channels_not_found') { setState(() { _isLoading = false; _foundChannels.clear(); }); final payload = message['payload']; String errorMessage = 'Каналы не найдены'; if (payload != null) { if (payload['localizedMessage'] != null) { errorMessage = payload['localizedMessage']; } else if (payload['message'] != null) { errorMessage = payload['message']; } } setState(() { _errorMessage = errorMessage; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: Colors.orange, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } }); } void _searchChannels() async { final searchQuery = _searchController.text.trim(); if (searchQuery.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; } setState(() { _isLoading = true; _foundChannels.clear(); _errorMessage = null; }); try { await ApiService.instance.searchChannels(searchQuery); } 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), ), ); } } void _viewChannel(Channel channel) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => ChannelDetailsScreen(channel: channel), ), ); } @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.broadcast_on_personal, color: colors.primary, ), const SizedBox(width: 8), Text( 'Поиск каналов', style: TextStyle( fontWeight: FontWeight.bold, color: colors.primary, ), ), ], ), const SizedBox(height: 8), Text( 'Найдите интересные каналы по названию или описанию. ' 'Вы можете просматривать каналы и подписываться на них.', 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: _searchController, decoration: InputDecoration( labelText: 'Название или описание канала', hintText: 'Например: новости, технологии, развлечения', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), prefixIcon: const Icon(Icons.search), ), ), 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( '• Используйте ключевые слова\n' '• Поиск по названию или описанию\n' '• Попробуйте разные варианты написания', style: TextStyle( color: colors.onSurfaceVariant, fontSize: 13, height: 1.4, ), ), ], ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isLoading ? null : _searchChannels, icon: _isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( colors.onPrimary, ), ), ) : const Icon(Icons.search), label: Text(_isLoading ? 'Поиск...' : 'Найти каналы'), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), if (_foundChannels.isNotEmpty) ...[ const SizedBox(height: 24), Text( 'Найденные каналы (${_foundChannels.length})', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), ..._foundChannels.map( (channel) => _buildChannelCard(channel), ), ], if (_errorMessage != null) ...[ const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange.withOpacity(0.3)), ), child: Row( children: [ Icon(Icons.warning, color: Colors.orange), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: TextStyle( color: Colors.orange.shade800, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ], ), ), if (_isLoading) Container( color: Colors.black.withOpacity(0.5), child: const Center(child: CircularProgressIndicator()), ), ], ), ); } Widget _buildChannelCard(Channel channel) { final colors = Theme.of(context).colorScheme; return Card( margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: const EdgeInsets.all(16), leading: CircleAvatar( radius: 24, backgroundImage: channel.photoBaseUrl != null ? NetworkImage(channel.photoBaseUrl!) : null, child: channel.photoBaseUrl == null ? Text( channel.name.isNotEmpty ? channel.name[0].toUpperCase() : '?', style: TextStyle( color: colors.onSurface, fontWeight: FontWeight.w600, ), ) : null, ), title: Text( channel.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (channel.description?.isNotEmpty == true) ...[ const SizedBox(height: 4), Text( channel.description!, style: TextStyle(color: colors.onSurfaceVariant, fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 8), Row( children: [ if (channel.options.contains('BOT')) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: colors.primaryContainer, borderRadius: BorderRadius.circular(12), ), child: Text( 'Бот', style: TextStyle( color: colors.onPrimaryContainer, fontSize: 12, fontWeight: FontWeight.w500, ), ), ), if (channel.options.contains('HAS_WEBAPP')) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: colors.secondaryContainer, borderRadius: BorderRadius.circular(12), ), child: Text( 'Веб-приложение', style: TextStyle( color: colors.onSecondaryContainer, fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ], ], ), ], ), trailing: Icon( Icons.arrow_forward_ios, size: 16, color: colors.onSurfaceVariant, ), onTap: () => _viewChannel(channel), ), ); } } class ChannelDetailsScreen extends StatefulWidget { final Channel channel; const ChannelDetailsScreen({super.key, required this.channel}); @override State createState() => _ChannelDetailsScreenState(); } class _ChannelDetailsScreenState extends State { StreamSubscription? _apiSubscription; bool _isLoading = false; String? _webAppUrl; String? _errorMessage; @override void initState() { super.initState(); _listenToApiMessages(); } @override void dispose() { _apiSubscription?.cancel(); super.dispose(); } void _listenToApiMessages() { _apiSubscription = ApiService.instance.messages.listen((message) { if (!mounted) return; if (message['type'] == 'channel_entered') { setState(() { _isLoading = false; _errorMessage = null; }); final payload = message['payload']; _webAppUrl = payload['url'] as String?; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Канал открыт'), backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } if (message['type'] == 'channel_subscribed') { setState(() { _isLoading = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Подписка на канал успешна!'), backgroundColor: Colors.green, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } if (message['type'] == 'channel_error') { setState(() { _isLoading = false; }); final payload = message['payload']; String errorMessage = 'Произошла ошибка'; if (payload != null) { if (payload['localizedMessage'] != null) { errorMessage = payload['localizedMessage']; } else if (payload['message'] != null) { errorMessage = payload['message']; } } setState(() { _errorMessage = errorMessage; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: Colors.red, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } }); } String _extractChannelLink(String inputLink) { final link = inputLink.trim(); if (link.startsWith('https://max.ru/') || link.startsWith('max.ru/')) { return link; } return link; } void _enterChannel() async { if (widget.channel.link == null) { 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; } setState(() { _isLoading = true; _errorMessage = null; }); try { final processedLink = _extractChannelLink(widget.channel.link!); await ApiService.instance.enterChannel(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), ), ); } } void _subscribeToChannel() async { if (widget.channel.link == null) { 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; } setState(() { _isLoading = true; _errorMessage = null; }); try { final processedLink = _extractChannelLink(widget.channel.link!); await ApiService.instance.subscribeToChannel(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: [ Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ CircleAvatar( radius: 40, backgroundImage: widget.channel.photoBaseUrl != null ? NetworkImage(widget.channel.photoBaseUrl!) : null, child: widget.channel.photoBaseUrl == null ? Text( widget.channel.name.isNotEmpty ? widget.channel.name[0].toUpperCase() : '?', style: TextStyle( color: colors.onSurface, fontWeight: FontWeight.w600, fontSize: 24, ), ) : null, ), const SizedBox(height: 16), Text( widget.channel.name, style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), if (widget.channel.description?.isNotEmpty == true) ...[ const SizedBox(height: 8), Text( widget.channel.description!, style: TextStyle( color: colors.onSurfaceVariant, fontSize: 16, ), textAlign: TextAlign.center, ), ], const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: [ if (widget.channel.options.contains('BOT')) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: colors.primaryContainer, borderRadius: BorderRadius.circular(16), ), child: Text( 'Бот', style: TextStyle( color: colors.onPrimaryContainer, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), if (widget.channel.options.contains('HAS_WEBAPP')) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: colors.secondaryContainer, borderRadius: BorderRadius.circular(16), ), child: Text( 'Веб-приложение', style: TextStyle( color: colors.onSecondaryContainer, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), ], ), ), ), const SizedBox(height: 24), Column( children: [ SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _isLoading ? null : _enterChannel, icon: _isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( colors.onPrimary, ), ), ) : const Icon(Icons.visibility), label: Text( _isLoading ? 'Загрузка...' : 'Просмотреть канал', ), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _isLoading ? null : _subscribeToChannel, icon: const Icon(Icons.subscriptions), label: const Text('Подписаться на канал'), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), if (_webAppUrl != null) ...[ const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colors.primaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(12), border: Border.all( color: colors.primary.withOpacity(0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.web, color: colors.primary), const SizedBox(width: 8), Text( 'Веб-приложение канала', style: TextStyle( fontWeight: FontWeight.bold, color: colors.primary, ), ), ], ), const SizedBox(height: 8), Text( 'Канал имеет веб-приложение. Вы можете открыть его в браузере.', style: TextStyle(color: colors.onSurfaceVariant), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Открытие веб-приложения...', ), backgroundColor: Colors.blue, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); }, icon: const Icon(Icons.open_in_browser), label: const Text('Открыть веб-приложение'), style: ElevatedButton.styleFrom( backgroundColor: colors.primary, foregroundColor: colors.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ), ], if (_errorMessage != null) ...[ const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.withOpacity(0.3)), ), child: Row( children: [ Icon(Icons.error, color: Colors.red), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: TextStyle( color: Colors.red.shade800, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ], ), ), if (_isLoading) Container( color: Colors.black.withOpacity(0.5), child: const Center(child: CircularProgressIndicator()), ), ], ), ); } }