Вход в кАналы по ссылке, возможность на них подписаться, отписаться пока нельзя sosi
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user