стикер в пустых чатах как в тг
This commit is contained in:
@@ -59,6 +59,115 @@ class DateSeparatorItem extends ChatItem {
|
||||
DateSeparatorItem(this.date);
|
||||
}
|
||||
|
||||
class _EmptyChatWidget extends StatelessWidget {
|
||||
final Map<String, dynamic>? sticker;
|
||||
final VoidCallback? onStickerTap;
|
||||
|
||||
const _EmptyChatWidget({this.sticker, this.onStickerTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
print('🎨 _EmptyChatWidget.build: sticker=${sticker != null ? "есть" : "null"}');
|
||||
if (sticker != null) {
|
||||
print('🎨 Стикер данные: ${sticker}');
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (sticker != null) ...[
|
||||
// Показываем стикер (LOTTIE или обычное изображение) с возможностью нажатия
|
||||
GestureDetector(
|
||||
onTap: onStickerTap,
|
||||
child: _buildSticker(sticker!),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
] else ...[
|
||||
// Показываем индикатор загрузки, пока стикер не загружен
|
||||
const SizedBox(
|
||||
width: 170,
|
||||
height: 170,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
Text(
|
||||
'Сообщений пока нет, напишите первым или отправьте этот стикер',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: colors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSticker(Map<String, dynamic> sticker) {
|
||||
final url = sticker['url'] as String?;
|
||||
final lottieUrl = sticker['lottieUrl'] as String?;
|
||||
final width = (sticker['width'] as num?)?.toDouble() ?? 170.0;
|
||||
final height = (sticker['height'] as num?)?.toDouble() ?? 170.0;
|
||||
|
||||
print('🎨 _buildSticker: url=$url, lottieUrl=$lottieUrl, width=$width, height=$height');
|
||||
|
||||
// Для отображения используем обычный url (статичное изображение)
|
||||
// lottieUrl - это для анимации, но пока используем статичное изображение
|
||||
// Если есть url, используем его, иначе пробуем lottieUrl
|
||||
final imageUrl = url ?? lottieUrl;
|
||||
|
||||
print('🎨 Используемый URL для стикера: $imageUrl');
|
||||
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
print('✅ Стикер успешно загружен');
|
||||
return child;
|
||||
}
|
||||
print('⏳ Загрузка стикера: ${loadingProgress.cumulativeBytesLoaded}/${loadingProgress.expectedTotalBytes}');
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
print('❌ Ошибка загрузки стикера: $error');
|
||||
print('❌ StackTrace: $stackTrace');
|
||||
return Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: width,
|
||||
color: Colors.grey,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
print('❌ URL стикера пустой или null');
|
||||
return Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: width,
|
||||
color: Colors.grey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final int chatId;
|
||||
final Contact contact;
|
||||
@@ -97,6 +206,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final Set<String> _animatedMessageIds = {};
|
||||
|
||||
bool _isLoadingHistory = true;
|
||||
Map<String, dynamic>? _emptyChatSticker;
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final FocusNode _textFocusNode = FocusNode();
|
||||
StreamSubscription? _apiSubscription;
|
||||
@@ -872,6 +982,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_isLoadingHistory = false;
|
||||
});
|
||||
_updatePinnedMessage();
|
||||
|
||||
// Если чат пустой, загружаем стикер для пустого состояния
|
||||
if (_messages.isEmpty && !widget.isChannel) {
|
||||
_loadEmptyChatSticker();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -957,6 +1072,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_isLoadingHistory = false;
|
||||
});
|
||||
_updatePinnedMessage();
|
||||
|
||||
// Если чат пустой, загружаем стикер для пустого состояния
|
||||
if (_messages.isEmpty && !widget.isChannel) {
|
||||
_loadEmptyChatSticker();
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Ошибка при загрузке с сервера: $e");
|
||||
if (mounted) {
|
||||
@@ -1148,6 +1268,66 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadEmptyChatSticker() async {
|
||||
try {
|
||||
// Список доступных ID стикеров для пустого чата
|
||||
final availableStickerIds = [272821, 295349, 13571];
|
||||
// Выбираем случайный ID
|
||||
final random = DateTime.now().millisecondsSinceEpoch % availableStickerIds.length;
|
||||
final selectedStickerId = availableStickerIds[random];
|
||||
|
||||
print('🎨 Загружаем стикер для пустого чата (ID: $selectedStickerId)...');
|
||||
final seq = ApiService.instance.sendRawRequest(28, {
|
||||
"type": "STICKER",
|
||||
"ids": [selectedStickerId],
|
||||
});
|
||||
|
||||
if (seq == -1) {
|
||||
print('❌ Не удалось отправить запрос на получение стикера');
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await ApiService.instance.messages
|
||||
.firstWhere(
|
||||
(msg) => msg['seq'] == seq && msg['opcode'] == 28,
|
||||
orElse: () => <String, dynamic>{},
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => throw TimeoutException(
|
||||
'Превышено время ожидания ответа от сервера',
|
||||
),
|
||||
);
|
||||
|
||||
if (response.isEmpty || response['payload'] == null) {
|
||||
print('❌ Не получен ответ от сервера для стикера');
|
||||
return;
|
||||
}
|
||||
|
||||
final stickers = response['payload']['stickers'] as List?;
|
||||
print('🎨 Получен ответ со стикерами: ${stickers?.length ?? 0}');
|
||||
if (stickers != null && stickers.isNotEmpty) {
|
||||
final sticker = stickers.first as Map<String, dynamic>;
|
||||
// Сохраняем также stickerId для отправки
|
||||
final stickerId = sticker['id'] as int?;
|
||||
print('🎨 Данные стикера: id=$stickerId, url=${sticker['url']}, lottieUrl=${sticker['lottieUrl']}, width=${sticker['width']}, height=${sticker['height']}');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_emptyChatSticker = {
|
||||
...sticker,
|
||||
'stickerId': stickerId, // Сохраняем ID для отправки
|
||||
};
|
||||
});
|
||||
print('✅ Стикер для пустого чата загружен и сохранен (ID: $stickerId)');
|
||||
}
|
||||
} else {
|
||||
print('❌ Стикеры не найдены в ответе');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Ошибка при загрузке стикера для пустого чата: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePinnedMessage() {
|
||||
Message? latestPinned;
|
||||
for (int i = _messages.length - 1; i >= 0; i--) {
|
||||
@@ -1468,6 +1648,51 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendEmptyChatSticker() async {
|
||||
if (_emptyChatSticker == null) {
|
||||
print('❌ Стикер не загружен, невозможно отправить');
|
||||
return;
|
||||
}
|
||||
|
||||
final stickerId = _emptyChatSticker!['stickerId'] as int?;
|
||||
if (stickerId == null) {
|
||||
print('❌ ID стикера не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
print('🎨 Отправляем стикер (ID: $stickerId) в чат ${widget.chatId}');
|
||||
final cid = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final payload = {
|
||||
"chatId": widget.chatId,
|
||||
"message": {
|
||||
"cid": cid,
|
||||
"attaches": [
|
||||
{
|
||||
"_type": "STICKER",
|
||||
"stickerId": stickerId,
|
||||
}
|
||||
],
|
||||
},
|
||||
"notify": true,
|
||||
};
|
||||
|
||||
ApiService.instance.sendRawRequest(64, payload);
|
||||
print('✅ Стикер отправлен (opcode 64, cid: $cid)');
|
||||
} catch (e) {
|
||||
print('❌ Ошибка при отправке стикера: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка при отправке стикера: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final originalText = _textController.text.trim();
|
||||
if (originalText.isNotEmpty) {
|
||||
@@ -2499,16 +2724,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
key: ValueKey('loading'),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: AnimatedPadding(
|
||||
key: const ValueKey('chat_list'),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOutCubic,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(
|
||||
context,
|
||||
).viewInsets.bottom,
|
||||
),
|
||||
child: ScrollablePositionedList.builder(
|
||||
: _messages.isEmpty && !widget.isChannel
|
||||
? _EmptyChatWidget(
|
||||
sticker: _emptyChatSticker,
|
||||
onStickerTap: _sendEmptyChatSticker,
|
||||
)
|
||||
: AnimatedPadding(
|
||||
key: const ValueKey('chat_list'),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOutCubic,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(
|
||||
context,
|
||||
).viewInsets.bottom,
|
||||
),
|
||||
child: ScrollablePositionedList.builder(
|
||||
itemScrollController: _itemScrollController,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
reverse: true,
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:gwid/utils/download_path_helper.dart';
|
||||
|
||||
class StorageScreen extends StatefulWidget {
|
||||
final bool isModal;
|
||||
@@ -35,6 +37,9 @@ class _StorageScreenState extends State<StorageScreen>
|
||||
_buildStorageDetails(colors),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildDownloadFolderSetting(colors),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildActionButtons(colors),
|
||||
],
|
||||
),
|
||||
@@ -294,6 +299,10 @@ class _StorageScreenState extends State<StorageScreen>
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildDownloadFolderSetting(colors),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildActionButtons(colors),
|
||||
],
|
||||
),
|
||||
@@ -380,6 +389,9 @@ class _StorageScreenState extends State<StorageScreen>
|
||||
_buildStorageDetails(colors),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildDownloadFolderSetting(colors),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildActionButtons(colors),
|
||||
],
|
||||
),
|
||||
@@ -654,6 +666,202 @@ class _StorageScreenState extends State<StorageScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _selectDownloadFolder() async {
|
||||
try {
|
||||
String? selectedDirectory;
|
||||
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
// На десктопе используем getDirectoryPath
|
||||
selectedDirectory = await FilePicker.platform.getDirectoryPath();
|
||||
} else {
|
||||
// На мобильных платформах file_picker может не поддерживать выбор папки
|
||||
// Используем диалог с текстовым вводом или просто показываем сообщение
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Выбор папки доступен только на десктопных платформах'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDirectory != null && selectedDirectory.isNotEmpty) {
|
||||
await DownloadPathHelper.setDownloadDirectory(selectedDirectory);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Папка загрузки установлена: $selectedDirectory'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
setState(() {}); // Обновляем UI
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка при выборе папки: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resetDownloadFolder() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Сбросить папку загрузки'),
|
||||
content: const Text(
|
||||
'Вернуть папку загрузки к значению по умолчанию?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Сбросить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
await DownloadPathHelper.setDownloadDirectory(null);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Папка загрузки сброшена к значению по умолчанию'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
setState(() {}); // Обновляем UI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDownloadFolderSetting(ColorScheme colors) {
|
||||
return FutureBuilder<String>(
|
||||
future: DownloadPathHelper.getDisplayPath(),
|
||||
builder: (context, snapshot) {
|
||||
final currentPath = snapshot.data ?? 'Загрузка...';
|
||||
final isCustom = snapshot.hasData &&
|
||||
currentPath != 'Не указано' &&
|
||||
!currentPath.contains('Downloads') &&
|
||||
!currentPath.contains('Download');
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.outline.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_outlined,
|
||||
color: colors.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Папка загрузки файлов',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Текущая папка:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
currentPath,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCustom)
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: colors.primary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _selectDownloadFolder,
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: const Text('Выбрать папку'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isCustom) ...[
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(
|
||||
onPressed: _resetDownloadFolder,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
),
|
||||
child: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(ColorScheme colors) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
Reference in New Issue
Block a user