Files
fuckKomet/lib/screens/search_channels_screen.dart

914 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<SearchChannelsScreen> createState() => _SearchChannelsScreenState();
}
class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
final TextEditingController _searchController = TextEditingController();
StreamSubscription? _apiSubscription;
bool _isLoading = false;
List<Channel> _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<dynamic>?;
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<Color>(
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<ChannelDetailsScreen> createState() => _ChannelDetailsScreenState();
}
class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
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) {
String link = inputLink.trim();
// Поддержка формата @https://max.ru/...
if (link.startsWith('@')) {
link = link.substring(1).trim();
}
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<Color>(
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()),
),
],
),
);
}
}