feat: поддержка медиа-вложений — опросы, видео, файлы и просмотр фото

Опросы (новый тип сообщения):
- модель Poll/PollAnswer и PollAttachment (_type: POLL)
- PollsModule: загрузка через opcode 306 (GET_POLL_UPDATES) с кэшем
- рендер опроса в баблах (вопрос, варианты, прогресс-бары, голоса)

Видео:
- getVideoUrl через opcode 83 (VIDEO_PLAY): выбор MP4_*/HLS из ответа
- полноэкранный плеер (video_player) с play/pause и перемоткой
- тап по видео в чате запускает воспроизведение

Файлы:
- getFileUrl через opcode 88 (FILE_DOWNLOAD) с корректным форматом
  {messageId, chatId, fileId} → url (раньше слался неверный {url, token})
- скачивание во временную папку и открытие системным приложением
  (path_provider, open_filex)

Фото:
- полноэкранный просмотрщик с зумом (был TODO-заглушкой)
- фикс «сжатости»: декодирование с учётом devicePixelRatio вместо ×2
This commit is contained in:
klockky
2026-06-02 20:06:46 +00:00
parent 5ff914b0dc
commit 1a3570f942
13 changed files with 787 additions and 33 deletions
+53
View File
@@ -0,0 +1,53 @@
import 'dart:io';
import 'package:open_filex/open_filex.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
class FileDownloadResult {
final bool ok;
final String? path;
final String? error;
const FileDownloadResult({required this.ok, this.path, this.error});
}
/// Скачивает файл по [url] во временную папку под именем [fileName]
/// и открывает его системным приложением.
Future<FileDownloadResult> downloadAndOpenFile(
String url,
String fileName,
) async {
try {
final dir = await getTemporaryDirectory();
final safeName = _sanitize(fileName);
final file = File(p.join(dir.path, safeName));
final client = HttpClient();
try {
final request = await client.getUrl(Uri.parse(url));
final response = await request.close();
if (response.statusCode != 200) {
return FileDownloadResult(ok: false, error: 'HTTP ${response.statusCode}');
}
final sink = file.openWrite();
await response.pipe(sink);
} finally {
client.close();
}
final opened = await OpenFilex.open(file.path);
return FileDownloadResult(
ok: opened.type == ResultType.done,
path: file.path,
error: opened.type == ResultType.done ? null : opened.message,
);
} catch (e) {
return FileDownloadResult(ok: false, error: e.toString());
}
}
String _sanitize(String name) {
final cleaned = name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
return cleaned.isEmpty ? 'file' : cleaned;
}