Files
fuckKomet/lib/home_screen.dart
2025-11-19 18:45:44 +03:00

981 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/chats_screen.dart';
import 'package:gwid/phone_entry_screen.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/screens/settings/reconnection_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gwid/services/version_checker.dart';
import 'package:app_links/app_links.dart';
import 'package:gwid/models/chat.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/models/profile.dart';
import 'package:gwid/chat_screen.dart';
import 'package:provider/provider.dart';
import 'package:gwid/theme_provider.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late Future<Map<String, dynamic>> _chatsFuture;
Profile? _myProfile;
bool _isProfileLoading = true;
String? _connectionStatus;
StreamSubscription? _connectionSubscription;
StreamSubscription? _messageSubscription;
late final AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
Uri? _initialUri;
@override
void initState() {
super.initState();
_loadMyProfile();
_chatsFuture = (() async {
try {
await ApiService.instance.waitUntilOnline();
return ApiService.instance.getChatsAndContacts();
} catch (e) {
print('Ошибка получения чатов в HomeScreen: $e');
if (e.toString().contains('Auth token not found') ||
e.toString().contains('FAIL_WRONG_PASSWORD')) {}
rethrow;
}
})();
_checkVersionInBackground();
_initDeepLinking();
_showSpoofUpdateDialogIfNeeded();
_connectionSubscription = ApiService.instance.connectionStatus.listen((
status,
) {
if (mounted) {
setState(() => _connectionStatus = status);
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() => _connectionStatus = null);
}
});
}
});
_messageSubscription = ApiService.instance.messages.listen((message) {
if (message['type'] == 'session_terminated' && mounted) {
_handleSessionTerminated(message['message']);
} else if (message['type'] == 'invalid_token' && mounted) {
_handleInvalidToken(message['message']);
} else if (message['type'] == 'group_join_success' && mounted) {
_handleGroupJoinSuccess(message);
} else if (message['cmd'] == 3 && message['opcode'] == 57 && mounted) {
_handleGroupJoinError(message);
}
});
}
Future<void> _loadMyProfile() async {
if (!mounted) return;
setState(() => _isProfileLoading = true);
try {
final cachedProfile = ApiService.instance.lastChatsPayload?['profile'];
if (cachedProfile != null) {
if (mounted) {
setState(() {
_myProfile = Profile.fromJson(cachedProfile);
_isProfileLoading = false;
});
}
} else {
final result = await ApiService.instance.getChatsAndContacts(
force: false,
);
if (mounted) {
final profileJson = result['profile'];
if (profileJson != null) {
setState(() {
_myProfile = Profile.fromJson(profileJson);
_isProfileLoading = false;
});
} else {
setState(() => _isProfileLoading = false);
}
}
}
} catch (e) {
if (mounted) setState(() => _isProfileLoading = false);
print("Ошибка загрузки профиля в _HomeScreenState: $e");
}
}
Future<void> _showUpdateDialog(
BuildContext context,
String newVersion,
) async {
await showDialog(
context: context,
barrierDismissible: false, // Зачем давать им выбор оло
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Доступно обновление'),
content: Text(
'Найдена новая версия приложения: $newVersion. Рекомендуется обновить данные сессии, чтобы соответствовать последней версии.',
),
actions: <Widget>[
TextButton(
child: const Text('Отменить'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
FilledButton(
child: const Text('Обновить'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('spoof_appversion', newVersion);
try {
await ApiService.instance.performFullReconnection();
print("Переподключение выполнено успешно");
} catch (e) {
print("Ошибка переподключения: $e");
}
if (mounted) {
Navigator.of(dialogContext).pop();
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Версия сессии обновлена до $newVersion!'),
backgroundColor: Colors.green.shade700,
),
);
}
},
),
],
);
},
);
}
Future<void> _checkVersionInBackground() async {
try {
final prefs = await SharedPreferences.getInstance();
final isWebVersionCheckEnabled =
prefs.getBool('enable_web_version_check') ?? false;
if (!isWebVersionCheckEnabled) {
print("Web version checking is disabled, skipping check");
return;
}
final isAutoUpdateEnabled = prefs.getBool('auto_update_enabled') ?? false;
final showUpdateNotification =
prefs.getBool('show_update_notification') ?? true;
final currentVersion = prefs.getString('spoof_appversion') ?? '0.0.0';
final latestVersion = await VersionChecker.getLatestVersion();
if (latestVersion != currentVersion) {
if (isAutoUpdateEnabled) {
await prefs.setString('spoof_appversion', latestVersion);
print("Версия сессии автоматически обновлена до $latestVersion");
try {
await ApiService.instance.performFullReconnection();
print("Переподключение выполнено успешно");
} catch (e) {
print("Ошибка переподключения: $e");
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Спуф сессии автоматически обновлен до версии $latestVersion',
),
backgroundColor: Colors.green.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(10),
),
);
}
} else if (showUpdateNotification) {
if (mounted) {
_showUpdateDialog(context, latestVersion);
}
}
}
} catch (e) {
print("Фоновая проверка версии не удалась: $e");
}
}
Future<void> _initDeepLinking() async {
_appLinks = AppLinks();
Uri? initialUriFromLaunch;
try {
initialUriFromLaunch = await _appLinks.getInitialLink();
if (initialUriFromLaunch != null) {
print('Получена ссылка (initial): $initialUriFromLaunch');
if (mounted) {
_handleJoinLink(initialUriFromLaunch);
}
}
} catch (e) {
print('Ошибка получения initial link: $e');
}
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
print('Получена ссылка (stream): $uri');
if (uri == initialUriFromLaunch) {
print('Ссылка из stream совпадает с initial, игнорируем.');
initialUriFromLaunch = null;
return;
}
if (mounted) {
_handleJoinLink(uri);
}
});
}
void _handleJoinLink(Uri uri) {
if (uri.host == 'max.ru' && uri.path.startsWith('/join/')) {
final String fullLink = uri.toString();
final String processedLink = _extractJoinLink(fullLink);
if (!processedLink.contains('join/')) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Неверный формат ссылки. Ссылка должна содержать "join/"',
),
backgroundColor: Colors.orange,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(10),
),
);
return;
}
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(processedLink)
.then((chatInfo) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
if (mounted) {
_showJoinConfirmationDialog(chatInfo, processedLink);
}
})
.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),
),
);
}
});
});
}
}
void _showJoinConfirmationDialog(
Map<String, dynamic> chatInfo,
String linkToJoin,
) {
final String title = chatInfo['title'] ?? 'Без названия';
final String? iconUrl = chatInfo['baseIconUrl'];
int joinState = 0;
String? errorMessage;
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
Widget content;
List<Widget> actions = [];
if (joinState == 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 (joinState == 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 (joinState == 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.group,
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(() {
joinState = 1;
});
try {
await ApiService.instance.joinGroupByLink(linkToJoin);
setState(() {
joinState = 2;
});
ApiService.instance.clearChatsCache();
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
Navigator.of(dialogContext).pop();
}
} catch (e) {
setState(() {
joinState = 3;
errorMessage = e.toString().replaceFirst(
"Exception: ",
"",
);
});
}
},
),
];
}
return AlertDialog(
title: joinState == 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>(joinState),
child: content,
),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
actionsAlignment: MainAxisAlignment.center,
actions: actions,
);
},
);
},
);
}
String _extractJoinLink(String inputLink) {
final link = inputLink.trim();
if (link.startsWith('join/')) {
print('Ссылка уже в правильном формате: $link');
return link;
}
final joinIndex = link.indexOf('join/');
if (joinIndex != -1) {
final extractedLink = link.substring(joinIndex);
print('Извлечена ссылка из полной ссылки: $link -> $extractedLink');
return extractedLink;
}
print('Не найдено "join/" в ссылке: $link');
return link;
}
void _handleGroupJoinSuccess(Map<String, dynamic> message) {
final payload = message['payload'];
final chat = payload['chat'];
final chatTitle = chat?['title'] ?? 'Группа';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Успешно присоединились к группе "$chatTitle"!'),
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
}
Future<void> _showSpoofUpdateDialogIfNeeded() async {
final prefs = await SharedPreferences.getInstance();
final shouldShow = prefs.getBool('show_spoof_update_dialog') ?? true;
if (!shouldShow || !mounted) return;
Future.delayed(const Duration(milliseconds: 500), () {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
bool dontShowAgain = false;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('Проверка обновлений'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Хотите проверить обновления спуфа?'),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: dontShowAgain,
onChanged: (value) {
setState(() {
dontShowAgain = value ?? false;
});
},
),
const Expanded(child: Text('Больше не показывать')),
],
),
],
),
actions: [
TextButton(
onPressed: () async {
if (dontShowAgain) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('show_spoof_update_dialog', false);
}
Navigator.of(context).pop();
},
child: const Text('Нет'),
),
TextButton(
onPressed: () async {
if (dontShowAgain) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('show_spoof_update_dialog', false);
}
Navigator.of(context).pop();
await _checkSpoofUpdateManually();
},
child: const Text('Ок!'),
),
],
);
},
);
},
);
});
}
Future<void> _checkSpoofUpdateManually() async {
try {
final prefs = await SharedPreferences.getInstance();
final isAutoUpdateEnabled = prefs.getBool('auto_update_enabled') ?? false;
final currentVersion = prefs.getString('spoof_appversion') ?? '0.0.0';
final latestVersion = await VersionChecker.getLatestVersion();
if (latestVersion != currentVersion) {
if (isAutoUpdateEnabled) {
await prefs.setString('spoof_appversion', latestVersion);
print("Версия сессии обновлена до $latestVersion");
try {
await ApiService.instance.performFullReconnection();
print("Переподключение выполнено успешно");
} catch (e) {
print("Ошибка переподключения: $e");
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Спуф сессии обновлен до версии $latestVersion'),
backgroundColor: Colors.green.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(10),
),
);
}
} else {
if (mounted) {
_showUpdateDialog(context, latestVersion);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Версия спуфа актуальна'),
backgroundColor: Colors.blue.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(10),
),
);
}
}
} catch (e) {
print("Проверка версии спуфа не удалась: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка проверки обновлений: $e'),
backgroundColor: Colors.red.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(10),
),
);
}
}
}
void _handleGroupJoinError(Map<String, dynamic> message) {
final errorPayload = message['payload'];
String errorMessage = 'Неизвестная ошибка';
if (errorPayload != null) {
if (errorPayload['localizedMessage'] != null) {
errorMessage = errorPayload['localizedMessage'];
} else if (errorPayload['message'] != null) {
errorMessage = errorPayload['message'];
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
}
Future<void> _checkAndConnect() async {
final hasToken = await ApiService.instance.hasToken();
if (hasToken) {
print("В HomeScreen: токен найден, проверяем подключение...");
try {
await ApiService.instance.connect();
print("В HomeScreen: подключение к WebSocket успешно");
} catch (e) {
print("В HomeScreen: ошибка подключения к WebSocket: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка подключения к серверу: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
}
} else {
print("В HomeScreen: токен не найден, пользователь не авторизован");
}
}
void _handleSessionTerminated(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Theme.of(context).colorScheme.error,
duration: const Duration(seconds: 3),
),
);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const PhoneEntryScreen(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
(route) => false,
);
}
});
}
void _showReconnectionScreen() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReconnectionScreen(),
fullscreenDialog: true,
),
);
}
void _handleInvalidToken(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(10),
),
);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const PhoneEntryScreen()),
(route) => false,
);
}
@override
void dispose() {
_connectionSubscription?.cancel();
_messageSubscription?.cancel();
_linkSubscription?.cancel();
super.dispose();
}
static const double kDesktopLayoutBreakpoint = 700.0;
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return LayoutBuilder(
builder: (context, constraints) {
final shouldUseDesktopLayout =
themeProvider.useDesktopLayout &&
constraints.maxWidth >= kDesktopLayoutBreakpoint;
if (shouldUseDesktopLayout) {
return const _DesktopLayout();
} else {
return const ChatsScreen();
}
},
);
},
);
}
}
class _DesktopLayout extends StatefulWidget {
const _DesktopLayout({super.key});
@override
State<_DesktopLayout> createState() => _DesktopLayoutState();
}
class _DesktopLayoutState extends State<_DesktopLayout> {
Chat? _selectedChat;
Contact? _selectedContact;
bool _isGroupChat = false;
bool _isChannel = false;
int? _participantCount;
Profile? _myProfile;
bool _isProfileLoading = true;
final ValueNotifier<double> _leftPanelWidth = ValueNotifier(320.0);
static const double _minPanelWidth = 280.0;
static const double _maxPanelWidth = 500.0;
@override
void initState() {
super.initState();
_loadMyProfile();
}
Future<void> _loadMyProfile() async {
if (!mounted) return;
setState(() => _isProfileLoading = true);
try {
final result = await ApiService.instance.getChatsAndContacts(
force: false,
);
if (mounted) {
final profileJson = result['profile'];
if (profileJson != null) {
setState(() {
_myProfile = Profile.fromJson(profileJson);
_isProfileLoading = false;
});
}
}
} catch (e) {
if (mounted) setState(() => _isProfileLoading = false);
print("Ошибка загрузки профиля в _DesktopLayout: $e");
}
}
void _onChatSelected(
Chat chat,
Contact contact,
bool isGroup,
bool isChannel,
int? participantCount,
) {
setState(() {
_selectedChat = chat;
_selectedContact = contact;
_isGroupChat = isGroup;
_isChannel = isChannel;
_participantCount = participantCount;
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
body: Row(
children: [
ValueListenableBuilder<double>(
valueListenable: _leftPanelWidth,
builder: (context, width, child) {
return SizedBox(
width: width,
child: ChatsScreen(onChatSelected: _onChatSelected),
);
},
),
GestureDetector(
onPanUpdate: (details) {
final newWidth = _leftPanelWidth.value + details.delta.dx;
if (newWidth >= _minPanelWidth && newWidth <= _maxPanelWidth) {
_leftPanelWidth.value = newWidth;
}
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(
width: 4.0,
color: colors.outline.withOpacity(0.3),
),
),
),
Expanded(
child:
(_selectedChat == null ||
_selectedContact == null ||
_isProfileLoading)
? Center(
child: _isProfileLoading
? const CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.message,
size: 80,
color: colors.primary.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'Выберите чат, чтобы начать общение',
style: TextStyle(
fontSize: 16,
color: colors.onSurfaceVariant,
),
),
],
),
)
: ChatScreen(
key: ValueKey(_selectedChat!.id),
chatId: _selectedChat!.id,
contact: _selectedContact!,
myId: _myProfile?.id ?? 0,
isGroupChat: _isGroupChat,
isChannel: _isChannel,
participantCount: _participantCount,
isDesktopMode: true,
onChatUpdated: () {},
),
),
],
),
);
}
}