753 lines
26 KiB
Dart
753 lines
26 KiB
Dart
import 'dart:convert';
|
||
import 'package:chepuhagram/core/constants.dart';
|
||
import 'package:chepuhagram/domain/services/aPI_service.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import '../widgets/contact_tile.dart';
|
||
import '../screens/settings_screen.dart';
|
||
import '../screens/new_chat_screen.dart';
|
||
import '../screens/chat_screen.dart';
|
||
import '/logic/contact_provider.dart';
|
||
import 'package:cached_network_image/cached_network_image.dart';
|
||
import '/logic/auth_provider.dart';
|
||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'package:chepuhagram/main.dart';
|
||
import 'dart:async';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'dart:io';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:open_filex/open_filex.dart';
|
||
import '/data/datasources/ws_client.dart';
|
||
|
||
class ContactsScreen extends StatefulWidget {
|
||
final int? targetChatId;
|
||
|
||
const ContactsScreen({super.key, this.targetChatId});
|
||
|
||
@override
|
||
State<ContactsScreen> createState() => _ContactsScreenState();
|
||
}
|
||
|
||
class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||
static const String _notificationLaunchKey = 'notification_launch_data';
|
||
StreamSubscription<dynamic>? _socketSubscription;
|
||
bool _isDownloading = false;
|
||
double _downloadProgress = 0.0;
|
||
int _downloadedBytes = 0;
|
||
int _downloadTotalBytes = 0;
|
||
int _apkFileSizeBytes = 0;
|
||
CancelToken? _cancelToken = CancelToken();
|
||
String? _latestApkUrl;
|
||
bool _showUpdateBanner = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
print('ContactsScreen initState, targetChatId: ${widget.targetChatId}');
|
||
_setupPushNotifications();
|
||
final socketService = Provider.of<SocketService>(context, listen: false);
|
||
_socketSubscription = socketService.messages.listen(_handleIncomingMessage);
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
final authProvider = context.read<AuthProvider>();
|
||
final contactProvider = context.read<ContactProvider>();
|
||
|
||
// Установить текущего пользователя и загрузить контакты с сообщениями
|
||
print(
|
||
'Setting current user ID in ContactProvider: ${authProvider.currentUserId}',
|
||
);
|
||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||
_initContacts();
|
||
});
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_checkAppUpdate();
|
||
});
|
||
}
|
||
|
||
Future<void> _initContacts() async {
|
||
final contactProvider = context.read<ContactProvider>();
|
||
// Ждем завершения загрузки контактов
|
||
await contactProvider.loadContacts();
|
||
|
||
print('Contacts loaded, checking targetChatId: ${widget.targetChatId}');
|
||
|
||
// Дальнейшая логика выполнится только после того, как loadContacts завершится
|
||
if (widget.targetChatId != null) {
|
||
_navigateToTargetChat();
|
||
} else {
|
||
_checkSavedNotificationTarget();
|
||
}
|
||
}
|
||
|
||
@override
|
||
void didChangeDependencies() {
|
||
super.didChangeDependencies();
|
||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||
}
|
||
|
||
@override
|
||
void didPopNext() {
|
||
print("Пользователь вернулся на этот экран!");
|
||
_refreshData();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
routeObserver.unsubscribe(this);
|
||
_socketSubscription?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
void _refreshData() {
|
||
print("Обновляем данные контактов и сообщений...");
|
||
final contactProvider = context.read<ContactProvider>();
|
||
|
||
contactProvider.loadContacts();
|
||
}
|
||
|
||
Future<void> _checkSavedNotificationTarget() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final savedData = prefs.getString(_notificationLaunchKey);
|
||
if (savedData == null) {
|
||
print('No saved notification data found in SharedPreferences');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
||
print('Recovered saved notification data: $data');
|
||
final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
|
||
final type = data['type']?.toString();
|
||
|
||
// Поддерживаем старый payload (только sender_id) и новый (type+sender_id)
|
||
if (senderId != null && (type == null || type == 'enc_message')) {
|
||
print('Recovered targetChatId from saved data: $senderId');
|
||
|
||
await prefs.remove(_notificationLaunchKey);
|
||
_navigateToTargetChatWithId(senderId);
|
||
return;
|
||
}
|
||
|
||
print('Saved notification data is not a valid payload: $data');
|
||
await prefs.remove(_notificationLaunchKey);
|
||
} catch (e) {
|
||
print('Error parsing saved notification data: $e');
|
||
await prefs.remove(_notificationLaunchKey);
|
||
}
|
||
}
|
||
|
||
void _navigateToTargetChat() {
|
||
if (widget.targetChatId == null) return;
|
||
|
||
_navigateToTargetChatWithId(widget.targetChatId!);
|
||
}
|
||
|
||
void _navigateToTargetChatWithId(int targetChatId) async {
|
||
print('_navigateToTargetChat called with targetChatId: $targetChatId');
|
||
final contactProvider = context.read<ContactProvider>();
|
||
try {
|
||
final contact = contactProvider.contacts.firstWhere(
|
||
(c) => c.id == targetChatId,
|
||
);
|
||
print('Auto-navigating to chat with contact: ${contact.username}');
|
||
currentActiveChatContactId = targetChatId; // Устанавливаем активный чат
|
||
final result = await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||
);
|
||
if (result != null) {
|
||
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
|
||
}
|
||
} catch (e) {
|
||
print('Target contact with id $targetChatId not found: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _checkAppUpdate() async {
|
||
print('Проверка обновлений');
|
||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||
try {
|
||
// 1. Запрос к вашему FastAPI
|
||
final response = await http.get(
|
||
Uri.parse('${AppConstants.baseUrl}/check-update'),
|
||
);
|
||
if (response.statusCode == 200) {
|
||
final data = jsonDecode(response.body);
|
||
final String latestVersion = data['latest_version'];
|
||
print('444444');
|
||
print(latestVersion);
|
||
print(packageInfo.version);
|
||
// Сравнение версий (предположим, у вас есть способ получить текущую версию)
|
||
if (latestVersion != packageInfo.version) {
|
||
setState(() {
|
||
_showUpdateBanner = true;
|
||
_latestApkUrl = data['apk_url'];
|
||
});
|
||
if (_latestApkUrl != null) {
|
||
final size = await _fetchApkSize(_latestApkUrl!);
|
||
if (mounted) {
|
||
setState(() {
|
||
_apkFileSizeBytes = size;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка проверки обновлений: $e");
|
||
}
|
||
}
|
||
|
||
Future<void> _setupPushNotifications() async {
|
||
// Request permissions
|
||
await FirebaseMessaging.instance.requestPermission();
|
||
|
||
String? token = await FirebaseMessaging.instance.getToken();
|
||
if (token != null) {
|
||
ApiService apiService = ApiService();
|
||
print(token);
|
||
await apiService.updateFcmToken(token);
|
||
}
|
||
|
||
// Listen for token refresh
|
||
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
|
||
ApiService apiService = ApiService();
|
||
apiService.updateFcmToken(newToken);
|
||
print('FCM Token refreshed: $newToken');
|
||
});
|
||
|
||
// Listen for foreground messages
|
||
FirebaseMessaging.onMessage.listen(_handleIncomingMessage);
|
||
|
||
// Handle notification tap when app was terminated/backgrounded
|
||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||
print('Notification tapped, app opened: ${message.data}');
|
||
if (message.data['type'] == 'enc_message') {
|
||
final senderId = int.tryParse(
|
||
message.data['sender_id']?.toString() ?? '',
|
||
);
|
||
if (senderId != null) {
|
||
_navigateToChatFromNotification(senderId);
|
||
} else {
|
||
print(
|
||
'Notification tap contains invalid sender_id: ${message.data['sender_id']}',
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _navigateToChatFromNotification(int senderId) async {
|
||
final contactProvider = context.read<ContactProvider>();
|
||
print('Navigate to chat from notification with senderId: $senderId');
|
||
|
||
// Если контакты еще не загружены, ждем их загрузки
|
||
if (contactProvider.contacts.isEmpty) {
|
||
print('Contacts not loaded yet, waiting...');
|
||
// Ждем немного и пробуем снова
|
||
Future.delayed(const Duration(milliseconds: 500), () {
|
||
if (mounted) {
|
||
_navigateToChatFromNotification(senderId);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final contact = contactProvider.contacts.firstWhere(
|
||
(c) => c.id == senderId,
|
||
);
|
||
print('Navigating to chat from notification: ${contact.username}');
|
||
currentActiveChatContactId = senderId; // Устанавливаем активный чат
|
||
final result = await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||
);
|
||
if (result != null) {
|
||
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
|
||
}
|
||
} catch (e) {
|
||
// Contact not found, stay on contacts screen
|
||
print('Contact not found for notification: $senderId');
|
||
}
|
||
}
|
||
|
||
Future<void> _handleIncomingMessage(dynamic data) async {
|
||
if (data is RemoteMessage) {
|
||
// FCM message
|
||
await _handleFCMMessage(data);
|
||
} else if (data is Map<String, dynamic>) {
|
||
// WebSocket message
|
||
print('WebSocket message received: $data');
|
||
if (data['type'] == 'user_updated') {
|
||
final userId = int.tryParse(data['user_id']?.toString() ?? '');
|
||
if (userId != null) {
|
||
final contactProvider = context.read<ContactProvider>();
|
||
contactProvider.updateContact(userId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _handleFCMMessage(RemoteMessage message) async {
|
||
try {
|
||
// Проверяем, не находимся ли мы уже в чате с отправителем
|
||
final senderId = int.tryParse(
|
||
message.data['sender_id']?.toString() ?? '',
|
||
);
|
||
if (senderId != null && currentActiveChatContactId == senderId) {
|
||
print('Already in chat with sender $senderId, skipping notification');
|
||
return;
|
||
}
|
||
|
||
// Ensure notification channel exists
|
||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||
'Messages',
|
||
'Новые сообщения',
|
||
description: 'Chat messages notifications',
|
||
importance: Importance.high,
|
||
);
|
||
|
||
await flutterLocalNotificationsPlugin
|
||
.resolvePlatformSpecificImplementation<
|
||
AndroidFlutterLocalNotificationsPlugin
|
||
>()
|
||
?.createNotificationChannel(channel);
|
||
|
||
final crypto = CryptoService();
|
||
final myPrivKey = await crypto.getPrivateKey();
|
||
if (myPrivKey == null) {
|
||
print('Private key not found, cannot decrypt message');
|
||
return;
|
||
}
|
||
|
||
final sharedSecret = await crypto.deriveSharedSecret(
|
||
myPrivKey,
|
||
message.data['public_key'],
|
||
);
|
||
final decryptedText = await crypto.decryptMessage(
|
||
message.data['content'],
|
||
sharedSecret,
|
||
);
|
||
|
||
if (senderId == null) return;
|
||
final String groupKey = 'ru.chepuhagram.app.$senderId';
|
||
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final String? firstName = prefs.getString(
|
||
'firstname_${message.data['sender_id']}',
|
||
);
|
||
final String? lastName = prefs.getString(
|
||
'lastname_${message.data['sender_id']}',
|
||
);
|
||
final String localFullName = '${firstName ?? ''} ${lastName ?? ''}'
|
||
.trim();
|
||
|
||
final String title = localFullName.isNotEmpty
|
||
? localFullName
|
||
: (message.data['username'] ?? 'Unknown');
|
||
// Show local notification
|
||
|
||
await flutterLocalNotificationsPlugin.show(
|
||
senderId,
|
||
'',
|
||
'',
|
||
NotificationDetails(
|
||
android: AndroidNotificationDetails(
|
||
'Messages',
|
||
'Новые сообщения',
|
||
groupKey: groupKey,
|
||
setAsGroupSummary: true,
|
||
importance: Importance.high,
|
||
priority: Priority.high,
|
||
groupAlertBehavior: GroupAlertBehavior.all,
|
||
),
|
||
),
|
||
);
|
||
await flutterLocalNotificationsPlugin.show(
|
||
message.hashCode,
|
||
title,
|
||
decryptedText,
|
||
NotificationDetails(
|
||
android: AndroidNotificationDetails(
|
||
'Messages',
|
||
'Новые сообщения',
|
||
groupKey: groupKey,
|
||
importance: Importance.high,
|
||
priority: Priority.high,
|
||
showWhen: true,
|
||
),
|
||
),
|
||
payload: jsonEncode({
|
||
'type': 'enc_message',
|
||
'sender_id': message.data['sender_id'],
|
||
'timestamp':
|
||
message.data['timestamp'] ?? DateTime.now().toIso8601String(),
|
||
}),
|
||
);
|
||
|
||
if (message.data['type'] == 'enc_message') {
|
||
final contactProvider = context.read<ContactProvider>();
|
||
contactProvider.loadContacts();
|
||
}
|
||
} catch (e) {
|
||
print('Error processing foreground FCM message: $e');
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final double fabBottomPadding = _showUpdateBanner ? 120.0 : 16.0;
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text(
|
||
"Chepuhagram",
|
||
style: TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
centerTitle: false,
|
||
elevation: 0,
|
||
actions: [IconButton(icon: const Icon(Icons.search), onPressed: () {})],
|
||
),
|
||
body: Stack(
|
||
children: [
|
||
Consumer<ContactProvider>(
|
||
builder: (context, contactProvider, child) {
|
||
if (contactProvider.isLoading) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
if (contactProvider.error != null) {
|
||
return Center(child: Text('Error: ${contactProvider.error}'));
|
||
}
|
||
return ListView.separated(
|
||
itemCount: contactProvider.contacts.length,
|
||
separatorBuilder: (context, index) => Divider(
|
||
height: 1,
|
||
indent: 80,
|
||
color: Theme.of(context).colorScheme.primaryContainer,
|
||
),
|
||
itemBuilder: (context, index) {
|
||
final contact = contactProvider.contacts[index];
|
||
return ContactTile(
|
||
contact: contact,
|
||
onTap: () async {
|
||
final result = await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (_) => ChatScreen(contact: contact),
|
||
),
|
||
);
|
||
if (result != null) {
|
||
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
|
||
}
|
||
},
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
if (_showUpdateBanner)
|
||
Positioned(
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 40,
|
||
child: _buildUpdateBanner(),
|
||
),
|
||
],
|
||
),
|
||
floatingActionButton: AnimatedPadding(
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeInOut,
|
||
padding: EdgeInsets.only(bottom: fabBottomPadding),
|
||
child: FloatingActionButton(
|
||
onPressed: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const NewChatScreen()),
|
||
);
|
||
},
|
||
child: const Icon(Icons.edit),
|
||
),
|
||
),
|
||
drawer: Drawer(
|
||
child: ListView(
|
||
padding: EdgeInsets.zero,
|
||
children: [
|
||
// Шапка меню с данными юзера
|
||
Consumer<AuthProvider>(
|
||
builder: (context, authProvider, _) {
|
||
final username = authProvider.username;
|
||
final displayName = authProvider.displayName;
|
||
final initials =
|
||
(displayName.isNotEmpty ? displayName : (username ?? 'U'))
|
||
.trim()
|
||
.split(RegExp(r'\s+'))
|
||
.where((p) => p.isNotEmpty)
|
||
.take(2)
|
||
.map((p) => p[0].toUpperCase())
|
||
.join();
|
||
|
||
return UserAccountsDrawerHeader(
|
||
accountName: Text(
|
||
displayName,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurface,
|
||
),
|
||
),
|
||
accountEmail: Text(
|
||
username == null || username.isEmpty ? '' : '@$username',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurface,
|
||
),
|
||
),
|
||
currentAccountPicture: CircleAvatar(
|
||
backgroundColor:
|
||
authProvider.avatarUrl == null &&
|
||
authProvider.avatarPath == null
|
||
? Theme.of(context).colorScheme.onSurface
|
||
: null,
|
||
backgroundImage: authProvider.avatarUrl != null
|
||
? CachedNetworkImageProvider(authProvider.avatarUrl!)
|
||
: authProvider.avatarPath != null
|
||
? FileImage(File(authProvider.avatarPath!))
|
||
: null,
|
||
child:
|
||
(authProvider.avatarUrl == null &&
|
||
authProvider.avatarPath == null)
|
||
? Text(
|
||
initials.isEmpty ? 'U' : initials,
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: Theme.of(
|
||
context,
|
||
).colorScheme.primaryContainer,
|
||
),
|
||
)
|
||
: null,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.inversePrimary,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.settings),
|
||
title: const Text("Настройки"),
|
||
onTap: () {
|
||
// Закрываем Drawer и переходим на экран настроек
|
||
Navigator.pop(context);
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => SettingsScreen()),
|
||
);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.info_outline),
|
||
title: const Text("О приложении"),
|
||
onTap: () {
|
||
/* ... */
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _startDownload() async {
|
||
if (_latestApkUrl == null) return;
|
||
|
||
// Показываем индикатор
|
||
setState(() => _isDownloading = true);
|
||
|
||
final dir = await getExternalStorageDirectory();
|
||
final path = '${dir!.path}/update.apk';
|
||
final file = File(path);
|
||
|
||
// Удаляем старый файл, если он есть, чтобы гарантировать чистоту
|
||
if (await file.exists()) {
|
||
await file.delete();
|
||
}
|
||
|
||
try {
|
||
setState(() {
|
||
_downloadProgress = 0.0;
|
||
_downloadedBytes = 0;
|
||
_downloadTotalBytes = 0;
|
||
});
|
||
|
||
// Скачиваем файл «в лоб»
|
||
await Dio().download(
|
||
_latestApkUrl!,
|
||
path,
|
||
cancelToken: _cancelToken,
|
||
onReceiveProgress: (rec, total) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_downloadedBytes = rec;
|
||
_downloadTotalBytes = total > 0 ? total : 0;
|
||
_downloadProgress = total > 0 ? rec / total : 0.0;
|
||
});
|
||
}
|
||
},
|
||
);
|
||
|
||
// После успешного скачивания — установка
|
||
final result = await OpenFilex.open(path);
|
||
if (result.type != ResultType.done) {
|
||
print("Ошибка при установке: ${result.message}");
|
||
}
|
||
} on DioException catch (e) {
|
||
if (e.type != DioExceptionType.cancel) {
|
||
print("Ошибка скачивания: $e");
|
||
}
|
||
} catch (e) {
|
||
print("Ошибка: $e");
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isDownloading = false;
|
||
_downloadProgress = 0.0;
|
||
_downloadedBytes = 0;
|
||
_downloadTotalBytes = 0;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<int> _fetchApkSize(String url) async {
|
||
try {
|
||
final response = await http.head(Uri.parse(url));
|
||
final lengthHeader = response.headers['content-length'];
|
||
if (lengthHeader == null) return 0;
|
||
return int.tryParse(lengthHeader) ?? 0;
|
||
} catch (_) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
String _formatBytes(int bytes) {
|
||
if (bytes <= 0) return '0 B';
|
||
const kb = 1024;
|
||
const mb = kb * 1024;
|
||
if (bytes < kb) return '$bytes B';
|
||
if (bytes < mb) return '${(bytes / kb).toStringAsFixed(1)} KB';
|
||
return '${(bytes / mb).toStringAsFixed(1)} MB';
|
||
}
|
||
|
||
Widget _buildUpdateBanner() {
|
||
return Container(
|
||
margin: const EdgeInsets.fromLTRB(
|
||
12,
|
||
0,
|
||
12,
|
||
16,
|
||
), // Отступы от краев и снизу
|
||
child: Material(
|
||
elevation: 6,
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [Colors.orange.shade600, Colors.deepOrange.shade400],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(
|
||
Icons.system_update_alt,
|
||
color: Colors.white,
|
||
size: 28,
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Text(
|
||
_isDownloading
|
||
? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%'
|
||
: _apkFileSizeBytes > 0
|
||
? 'Доступно новое обновление: ${_formatBytes(_apkFileSizeBytes)}'
|
||
: 'Доступно новое обновление!',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
),
|
||
TextButton(
|
||
onPressed: () async {
|
||
if (_isDownloading) {
|
||
// Если уже качаем — отменяем
|
||
_cancelToken?.cancel("Пользователь отменил загрузку");
|
||
setState(() {
|
||
_isDownloading = false;
|
||
_cancelToken = null; // Обязательно обнуляем токен!
|
||
_downloadProgress = 0.0;
|
||
_downloadedBytes = 0;
|
||
_downloadTotalBytes = 0;
|
||
});
|
||
} else {
|
||
// Если не качаем — запускаем
|
||
setState(() {
|
||
_isDownloading = true;
|
||
_cancelToken =
|
||
CancelToken(); // Создаем новый токен перед началом
|
||
});
|
||
|
||
// ВАЖНО: вызываем саму функцию скачивания
|
||
await _startDownload();
|
||
}
|
||
},
|
||
style: TextButton.styleFrom(
|
||
backgroundColor: Colors.white24,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
child: Text(
|
||
_isDownloading ? "Отмена" : "Обновить",
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (_isDownloading) ...[
|
||
const SizedBox(height: 12),
|
||
LinearProgressIndicator(
|
||
value: _downloadProgress,
|
||
color: Colors.white,
|
||
backgroundColor: Colors.white24,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
'${_formatBytes(_downloadedBytes)} из ${_formatBytes(_downloadTotalBytes)}',
|
||
style: const TextStyle(color: Colors.white70, fontSize: 14),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|