Вход в кАналы по ссылке, возможность на них подписаться, отписаться пока нельзя sosi

This commit is contained in:
jganenok
2025-11-27 15:12:30 +07:00
parent 1007a053fd
commit 8991cd5bb8
4 changed files with 584 additions and 58 deletions

View File

@@ -259,8 +259,26 @@ class _HomeScreenState extends State<HomeScreen> {
} }
void _handleJoinLink(Uri uri) { void _handleJoinLink(Uri uri) {
if (uri.host == 'max.ru' && uri.path.startsWith('/join/')) { if (uri.host != 'max.ru') return;
final String fullLink = uri.toString();
String fullLink = uri.toString();
// На всякий случай убираем возможный префикс '@' перед ссылкой
if (fullLink.startsWith('@')) {
fullLink = fullLink.substring(1);
}
final bool isGroupLink = uri.path.startsWith('/join/');
final bool isChannelLink =
!isGroupLink &&
uri.pathSegments.isNotEmpty &&
uri.pathSegments.first.startsWith('id');
if (!isGroupLink && !isChannelLink) {
return;
}
if (isGroupLink) {
final String processedLink = _extractJoinLink(fullLink); final String processedLink = _extractJoinLink(fullLink);
if (!processedLink.contains('join/')) { if (!processedLink.contains('join/')) {
@@ -309,6 +327,39 @@ class _HomeScreenState extends State<HomeScreen> {
} }
}); });
}); });
} else if (isChannelLink) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Загрузка информации о канале...'),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(10),
duration: Duration(seconds: 10),
),
);
ApiService.instance.waitUntilOnline().then((_) {
ApiService.instance
.getChatInfoByLink(fullLink)
.then((chatInfo) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
if (mounted) {
_showChannelSubscribeDialog(chatInfo, fullLink);
}
})
.catchError((error) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка: ${error.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
}
});
});
} }
} }
@@ -530,6 +581,227 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
void _showChannelSubscribeDialog(
Map<String, dynamic> 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<Widget> 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;
});
ApiService.instance.clearChatsCache();
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<double> animation) {
final slideAnimation =
Tween<Offset>(
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<int>(subscribeState),
child: content,
),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
actionsAlignment: MainAxisAlignment.center,
actions: actions,
);
},
);
},
);
}
String _extractJoinLink(String inputLink) { String _extractJoinLink(String inputLink) {
final link = inputLink.trim(); final link = inputLink.trim();

View File

@@ -1,5 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gwid/api/api_service.dart'; import 'package:gwid/api/api_service.dart';
@@ -33,7 +31,6 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
_apiSubscription = ApiService.instance.messages.listen((message) { _apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return; if (!mounted) return;
if (message['type'] == 'group_join_success') { if (message['type'] == 'group_join_success') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -55,11 +52,9 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
), ),
); );
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
if (message['cmd'] == 3 && message['opcode'] == 57) { if (message['cmd'] == 3 && message['opcode'] == 57) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -90,17 +85,25 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
}); });
} }
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) { String _extractJoinLink(String inputLink) {
final link = inputLink.trim(); final link = _normalizeLink(inputLink);
if (link.startsWith('join/')) { if (link.startsWith('join/')) {
print('Ссылка уже в правильном формате: $link'); print('Ссылка уже в правильном формате: $link');
return link; return link;
} }
final joinIndex = link.indexOf('join/'); final joinIndex = link.indexOf('join/');
if (joinIndex != -1) { if (joinIndex != -1) {
final extractedLink = link.substring(joinIndex); final extractedLink = link.substring(joinIndex);
@@ -108,18 +111,33 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
return extractedLink; return extractedLink;
} }
print('Не найдено "join/" в ссылке: $link'); print('Не найдено "join/" в ссылке: $link');
return 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 { void _joinGroup() async {
final inputLink = _linkController.text.trim(); final rawInput = _linkController.text.trim();
final inputLink = _normalizeLink(rawInput);
if (inputLink.isEmpty) { if (inputLink.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Введите ссылку на группу'), content: const Text('Введите ссылку'),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -131,15 +149,49 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
return; 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); final processedLink = _extractJoinLink(inputLink);
if (!processedLink.contains('join/')) { if (!processedLink.contains('join/')) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text( content: const Text(
'Неверный формат ссылки. Ссылка должна содержать "join/"', 'Неверный формат ссылки. Для группы ссылка должна содержать "join/"',
), ),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -164,7 +216,7 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
}); });
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Ошибка присоединения к группе: ${e.toString()}'), content: Text('Ошибка присоединения: ${e.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -182,7 +234,7 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Присоединиться к группе'), title: const Text('Присоединиться по ссылке'),
backgroundColor: colors.surface, backgroundColor: colors.surface,
foregroundColor: colors.onSurface, foregroundColor: colors.onSurface,
), ),
@@ -193,7 +245,6 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -205,10 +256,10 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
children: [ children: [
Row( Row(
children: [ children: [
Icon(Icons.group_add, color: colors.primary), Icon(Icons.link, color: colors.primary),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Присоединение к группе', 'Присоединение по ссылке',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colors.primary, color: colors.primary,
@@ -218,9 +269,10 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Введите ссылку на группу, чтобы присоединиться к ней. ' 'Введите ссылку на группу или канал, чтобы присоединиться. '
'Можно вводить как полную ссылку (https://max.ru/join/...), ' 'Для групп можно вводить полную (https://max.ru/join/...) '
'так и короткую (join/...).', 'или короткую (join/...) ссылку, для каналов — ссылку вида '
'https://max.ru/idXXXXXXXX.',
style: TextStyle(color: colors.onSurfaceVariant), style: TextStyle(color: colors.onSurfaceVariant),
), ),
], ],
@@ -229,9 +281,8 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Ссылка на группу', 'Ссылка',
style: Theme.of( style: Theme.of(
context, context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
@@ -241,8 +292,9 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
TextField( TextField(
controller: _linkController, controller: _linkController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Ссылка на группу', labelText: 'Ссылка на группу или канал',
hintText: 'https://max.ru/join/ABC123DEF456GHI789JKL', hintText:
'https://max.ru/join/ABC123DEF456GHI789JKL или https://max.ru/id7452017130_gos',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -281,11 +333,12 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Ссылка должна содержать "join/"\n' 'Для групп: ссылка должна содержать "join/"\n'
'• После "join/" должен идти уникальный идентификатор группы\n' '• После "join/" должен идти уникальный идентификатор группы\n'
'• Примеры:\n' '• Примеры групп:\n'
' - https://max.ru/join/ABC123DEF456GHI789JKL\n' ' - https://max.ru/join/ABC123DEF456GHI789JKL\n'
' - join/ABC123DEF456GHI789JKL', ' - join/ABC123DEF456GHI789JKL\n'
'• Для каналов: ссылка вида https://max.ru/idXXXXXXXX',
style: TextStyle( style: TextStyle(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
fontSize: 13, fontSize: 13,
@@ -312,11 +365,11 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
), ),
), ),
) )
: const Icon(Icons.group_add), : const Icon(Icons.link),
label: Text( label: Text(
_isLoading _isLoading
? 'Присоединение...' ? 'Присоединение...'
: 'Присоединиться к группе', : 'Присоединиться по ссылке',
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50), minimumSize: const Size(double.infinity, 50),
@@ -338,4 +391,223 @@ class _JoinGroupScreenState extends State<JoinGroupScreen> {
), ),
); );
} }
void _showChannelSubscribeDialog(
Map<String, dynamic> 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<Widget> 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<double> animation) {
final slideAnimation =
Tween<Offset>(
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<int>(subscribeState),
child: content,
),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
actionsAlignment: MainAxisAlignment.center,
actions: actions,
);
},
);
},
);
}
} }

View File

@@ -1,5 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gwid/api/api_service.dart'; import 'package:gwid/api/api_service.dart';
@@ -36,7 +34,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
_apiSubscription = ApiService.instance.messages.listen((message) { _apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return; if (!mounted) return;
if (message['type'] == 'channels_found') { if (message['type'] == 'channels_found') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -65,7 +62,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
); );
} }
if (message['type'] == 'channels_not_found') { if (message['type'] == 'channels_not_found') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -171,7 +167,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -209,7 +204,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Поисковый запрос', 'Поисковый запрос',
style: Theme.of( style: Theme.of(
@@ -300,7 +294,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
), ),
), ),
if (_foundChannels.isNotEmpty) ...[ if (_foundChannels.isNotEmpty) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@@ -315,7 +308,6 @@ class _SearchChannelsScreenState extends State<SearchChannelsScreen> {
), ),
], ],
if (_errorMessage != null) ...[ if (_errorMessage != null) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Container( Container(
@@ -483,7 +475,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
_apiSubscription = ApiService.instance.messages.listen((message) { _apiSubscription = ApiService.instance.messages.listen((message) {
if (!mounted) return; if (!mounted) return;
if (message['type'] == 'channel_entered') { if (message['type'] == 'channel_entered') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -506,7 +497,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
); );
} }
if (message['type'] == 'channel_subscribed') { if (message['type'] == 'channel_subscribed') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -525,7 +515,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
); );
} }
if (message['type'] == 'channel_error') { if (message['type'] == 'channel_error') {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
@@ -561,16 +550,14 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
}); });
} }
String _extractChannelLink(String inputLink) { String _extractChannelLink(String inputLink) {
final link = inputLink.trim(); String link = inputLink.trim();
// Поддержка формата @https://max.ru/...
if (link.startsWith('https://max.ru/') || link.startsWith('max.ru/')) { if (link.startsWith('@')) {
return link; link = link.substring(1).trim();
} }
return link; return link;
} }
@@ -675,7 +662,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Card( Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -772,7 +758,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Column( Column(
children: [ children: [
SizedBox( SizedBox(
@@ -820,7 +805,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
], ],
), ),
if (_webAppUrl != null) ...[ if (_webAppUrl != null) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Container( Container(
@@ -858,7 +842,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text( content: const Text(
@@ -889,7 +872,6 @@ class _ChannelDetailsScreenState extends State<ChannelDetailsScreen> {
), ),
], ],
if (_errorMessage != null) ...[ if (_errorMessage != null) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Container( Container(

View File

@@ -809,10 +809,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -1318,10 +1318,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.7"
timezone: timezone:
dependency: "direct main" dependency: "direct main"
description: description: