feat: кэширование файлов/видео + управление кэшем в дев-меню

This commit is contained in:
klockky
2026-06-02 20:40:03 +00:00
parent 1a3570f942
commit 4bdb0e70cf
8 changed files with 585 additions and 80 deletions
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppMediaCacheLimit {
static const prefKey = 'media_cache_limit_bytes';
static const int defaultValue = 500 * 1024 * 1024; // 500 МБ
/// Значение «без лимита» — вытеснение из кэша отключено.
static const int unlimited = 0;
/// Доступные пресеты лимита, байты (0 — без лимита).
static const List<int> presets = [
100 * 1024 * 1024,
250 * 1024 * 1024,
500 * 1024 * 1024,
1024 * 1024 * 1024,
2 * 1024 * 1024 * 1024,
unlimited,
];
static final ValueNotifier<int> current = ValueNotifier(defaultValue);
static Future<int> load() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(prefKey) ?? defaultValue;
}
static Future<void> save(int value) async {
current.value = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(prefKey, value);
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart';
/// Прогресс активных загрузок вложений, ключ — имя в кэше.
///
/// Значение: `null` — не загружается; `0..1` — доля загруженного.
class MediaDownloadProgress {
static final Map<String, ValueNotifier<double?>> _notifiers = {};
static ValueNotifier<double?> notifier(String key) =>
_notifiers.putIfAbsent(key, () => ValueNotifier<double?>(null));
static void set(String key, double? value) {
notifier(key).value = value;
}
}
+25 -28
View File
@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'media_cache.dart';
class FileDownloadResult { class FileDownloadResult {
final bool ok; final bool ok;
@@ -12,28 +10,32 @@ class FileDownloadResult {
const FileDownloadResult({required this.ok, this.path, this.error}); const FileDownloadResult({required this.ok, this.path, this.error});
} }
/// Скачивает файл по [url] во временную папку под именем [fileName] /// Открывает файл из кэша, скачивая его при отсутствии.
/// и открывает его системным приложением. ///
Future<FileDownloadResult> downloadAndOpenFile( /// [cacheName] — стабильное имя в кэше (например, `<fileId>_имя.ext`).
String url, /// [resolveUrl] вызывается лениво — только если файла ещё нет в кэше,
String fileName, /// чтобы не дёргать сервер за временной ссылкой повторно.
) async { Future<FileDownloadResult> openCachedFile(
String cacheName,
Future<String?> Function() resolveUrl, {
void Function(double progress)? onProgress,
}) async {
try { try {
final dir = await getTemporaryDirectory(); var file = await MediaCache.existing(cacheName);
final safeName = _sanitize(fileName);
final file = File(p.join(dir.path, safeName));
final client = HttpClient(); if (file == null) {
try { final url = await resolveUrl();
final request = await client.getUrl(Uri.parse(url)); if (url == null || url.isEmpty) {
final response = await request.close(); return const FileDownloadResult(ok: false, error: 'нет ссылки');
if (response.statusCode != 200) { }
return FileDownloadResult(ok: false, error: 'HTTP ${response.statusCode}'); file = await MediaCache.getOrDownload(
cacheName,
url,
onProgress: onProgress,
);
if (file == null) {
return const FileDownloadResult(ok: false, error: 'ошибка загрузки');
} }
final sink = file.openWrite();
await response.pipe(sink);
} finally {
client.close();
} }
final opened = await OpenFilex.open(file.path); final opened = await OpenFilex.open(file.path);
@@ -46,8 +48,3 @@ Future<FileDownloadResult> downloadAndOpenFile(
return FileDownloadResult(ok: false, error: e.toString()); return FileDownloadResult(ok: false, error: e.toString());
} }
} }
String _sanitize(String name) {
final cleaned = name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
return cleaned.isEmpty ? 'file' : cleaned;
}
+158
View File
@@ -0,0 +1,158 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../config/app_media_cache.dart';
/// Постоянный дисковый кэш скачанных медиа (файлы, видео).
///
/// Хранит файлы в `<appSupport>/media_cache/` под детерминированным именем
/// (обычно по id вложения), чтобы повторные открытия не качали заново.
class MediaCache {
/// Максимальный размер кэша (настраивается в дев-меню); при превышении
/// вытесняются старые файлы (LRU).
static int get maxBytes => AppMediaCacheLimit.current.value;
static Directory? _dir;
static Future<Directory> _cacheDir() async {
final cached = _dir;
if (cached != null) return cached;
final base = await getApplicationSupportDirectory();
final dir = Directory(p.join(base.path, 'media_cache'));
if (!await dir.exists()) {
await dir.create(recursive: true);
}
_dir = dir;
return dir;
}
/// Путь к кэш-файлу с именем [name] (файл может ещё не существовать).
static Future<File> fileFor(String name) async {
final dir = await _cacheDir();
return File(p.join(dir.path, _sanitize(name)));
}
/// Существует ли непустой кэш-файл [name].
///
/// При попадании обновляет mtime файла — это делает вытеснение LRU
/// (часто используемые файлы переживают очистку).
static Future<File?> existing(String name) async {
final file = await fileFor(name);
if (await file.exists() && await file.length() > 0) {
try {
await file.setLastModified(DateTime.now());
} catch (_) {}
return file;
}
return null;
}
/// Возвращает кэш-файл [name], скачивая [url] при отсутствии.
///
/// Загрузка идёт во временный `.part` и переименовывается атомарно —
/// прерванная закачка не считается валидным кэшем.
static Future<File?> getOrDownload(
String name,
String url, {
void Function(double progress)? onProgress,
}) async {
final existingFile = await existing(name);
if (existingFile != null) return existingFile;
final file = await fileFor(name);
final part = File('${file.path}.part');
final client = HttpClient();
try {
final request = await client.getUrl(Uri.parse(url));
final response = await request.close();
if (response.statusCode != 200) return null;
final total = response.contentLength;
var received = 0;
final sink = part.openWrite();
await for (final chunk in response) {
received += chunk.length;
sink.add(chunk);
if (onProgress != null && total > 0) {
onProgress(received / total);
}
}
await sink.close();
await part.rename(file.path);
await _enforceLimit();
return file;
} catch (_) {
if (await part.exists()) {
try {
await part.delete();
} catch (_) {}
}
return null;
} finally {
client.close();
}
}
/// Суммарный размер кэша в байтах.
static Future<int> currentSize() async {
final dir = await _cacheDir();
var total = 0;
await for (final entity in dir.list()) {
if (entity is File) {
try {
total += await entity.length();
} catch (_) {}
}
}
return total;
}
/// Полностью очищает кэш. Возвращает число удалённых байт.
static Future<int> clear() async {
final dir = await _cacheDir();
var freed = 0;
await for (final entity in dir.list()) {
if (entity is File) {
try {
freed += await entity.length();
await entity.delete();
} catch (_) {}
}
}
return freed;
}
/// Вытесняет старые файлы (по mtime), пока размер превышает [maxBytes].
static Future<void> _enforceLimit() async {
final dir = await _cacheDir();
final files = <File>[];
var total = 0;
await for (final entity in dir.list()) {
if (entity is File && !entity.path.endsWith('.part')) {
files.add(entity);
try {
total += await entity.length();
} catch (_) {}
}
}
if (maxBytes <= 0 || total <= maxBytes) return;
files.sort((a, b) =>
a.statSync().modified.compareTo(b.statSync().modified));
for (final file in files) {
if (total <= maxBytes) break;
try {
total -= await file.length();
await file.delete();
} catch (_) {}
}
}
static String _sanitize(String name) {
final cleaned = name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
return cleaned.isEmpty ? 'file' : cleaned;
}
}
@@ -6,9 +6,11 @@ import '../../../backend/modules/chats.dart';
import '../../../core/config/app_swipe_back_desktop.dart'; import '../../../core/config/app_swipe_back_desktop.dart';
import '../../../core/config/app_pranks.dart'; import '../../../core/config/app_pranks.dart';
import '../../../core/config/app_stories.dart'; import '../../../core/config/app_stories.dart';
import '../../../core/config/app_media_cache.dart';
import '../../../core/protocol/opcode_map.dart'; import '../../../core/protocol/opcode_map.dart';
import '../../../core/protocol/packet.dart'; import '../../../core/protocol/packet.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../core/utils/media_cache.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../widgets/login_success_screen.dart'; import '../../widgets/login_success_screen.dart';
@@ -26,6 +28,92 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
bool _hasSearched = false; bool _hasSearched = false;
final List<_SearchHit> _hits = []; final List<_SearchHit> _hits = [];
final Map<String, String> _errors = {}; final Map<String, String> _errors = {};
int _cacheSize = 0;
bool _clearingCache = false;
@override
void initState() {
super.initState();
_loadCacheSize();
}
Future<void> _loadCacheSize() async {
final size = await MediaCache.currentSize();
if (mounted) setState(() => _cacheSize = size);
}
Future<void> _clearCache() async {
if (_clearingCache) return;
setState(() => _clearingCache = true);
final freed = await MediaCache.clear();
if (!mounted) return;
setState(() {
_clearingCache = false;
_cacheSize = 0;
});
showCustomNotification(context, 'Кэш очищен (${_formatBytes(freed)})');
}
void _pickCacheLimit() {
final cs = Theme.of(context).colorScheme;
showModalBottomSheet<void>(
context: context,
backgroundColor: cs.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 8),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Лимит кэша медиа',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
for (final preset in AppMediaCacheLimit.presets)
ListTile(
title: Text(
_limitLabel(preset),
style: TextStyle(color: cs.onSurface, fontSize: 16),
),
trailing: AppMediaCacheLimit.current.value == preset
? Icon(Symbols.check, color: cs.primary)
: null,
onTap: () {
AppMediaCacheLimit.save(preset);
Navigator.pop(sheetContext);
setState(() {});
},
),
const SizedBox(height: 8),
],
),
),
);
}
String _limitLabel(int bytes) =>
bytes <= 0 ? 'Без лимита' : _formatBytes(bytes);
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes Б';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} КБ';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} МБ';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} ГБ';
}
@override @override
void dispose() { void dispose() {
@@ -516,6 +604,129 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
), ),
), ),
), ),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: _pickCacheLimit,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.data_usage,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Лимит кэша медиа',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
_limitLabel(AppMediaCacheLimit.current.value),
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: _clearingCache ? null : _clearCache,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.delete_sweep,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Очистить кэш медиа',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
_clearingCache
? 'Очистка…'
: 'Занято: ${_formatBytes(_cacheSize)}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
if (_clearingCache)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: cs.onSurfaceVariant,
),
),
],
),
),
),
),
),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
+89 -46
View File
@@ -9,6 +9,8 @@ import '../../core/config/app_bubble_shape.dart';
import '../../core/utils/bubble_radius.dart'; import '../../core/utils/bubble_radius.dart';
import '../../core/utils/haptics.dart'; import '../../core/utils/haptics.dart';
import '../../core/utils/file_download.dart'; import '../../core/utils/file_download.dart';
import '../../core/utils/media_cache.dart';
import '../../core/utils/download_progress.dart';
import 'custom_notification.dart'; import 'custom_notification.dart';
import '../../models/attachment.dart'; import '../../models/attachment.dart';
import 'poll_view.dart'; import 'poll_view.dart';
@@ -1293,26 +1295,39 @@ class MessageBubble extends StatelessWidget {
) async { ) async {
final videoId = (video as dynamic).videoId as int?; final videoId = (video as dynamic).videoId as int?;
final token = (video as dynamic).videoToken as String?; final token = (video as dynamic).videoToken as String?;
if (videoId == null || token == null) { if (videoId == null) {
showCustomNotification(context, 'Не удалось открыть видео'); showCustomNotification(context, 'Не удалось открыть видео');
return; return;
} }
Haptics.tap(); Haptics.tap();
final url = await messagesModule.getVideoUrl(
messageId: message.id, final cacheName = 'video_$videoId.mp4';
chatId: message.chatId, final cached = await MediaCache.existing(cacheName) != null;
token: token,
videoId: videoId,
);
if (!context.mounted) return; if (!context.mounted) return;
if (url == null) {
showCustomNotification(context, 'Не удалось получить видео'); String? url;
return; if (!cached) {
if (token == null) {
showCustomNotification(context, 'Не удалось открыть видео');
return;
}
url = await messagesModule.getVideoUrl(
messageId: message.id,
chatId: message.chatId,
token: token,
videoId: videoId,
);
if (!context.mounted) return;
if (url == null) {
showCustomNotification(context, 'Не удалось получить видео');
return;
}
} }
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (_) => VideoPlayerScreen(url: url), builder: (_) => VideoPlayerScreen(cacheName: cacheName, url: url),
), ),
); );
} }
@@ -1321,6 +1336,8 @@ class MessageBubble extends StatelessWidget {
final name = (file as dynamic).name as String? ?? 'File'; final name = (file as dynamic).name as String? ?? 'File';
final size = (file as dynamic).size as int? ?? 0; final size = (file as dynamic).size as int? ?? 0;
final sizeStr = _formatFileSize(size); final sizeStr = _formatFileSize(size);
final fileId = (file as dynamic).fileId as int?;
final cacheName = '${fileId}_$name';
return IntrinsicWidth( return IntrinsicWidth(
child: Padding( child: Padding(
@@ -1366,35 +1383,61 @@ class MessageBubble extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( ValueListenableBuilder<double?>(
sizeStr, valueListenable: MediaDownloadProgress.notifier(cacheName),
style: TextStyle( builder: (context, progress, _) => Text(
color: ctx.dim, progress != null
fontSize: 12, ? '${(progress * 100).round()}% · $sizeStr'
height: 1.2, : sizeStr,
style: TextStyle(
color: ctx.dim,
fontSize: 12,
height: 1.2,
),
), ),
), ),
], ],
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
GestureDetector( ValueListenableBuilder<double?>(
onTap: () => _downloadFile(ctx.context, file, name), valueListenable: MediaDownloadProgress.notifier(cacheName),
child: Container( builder: (context, progress, _) {
width: 34, final downloading = progress != null;
height: 34, return GestureDetector(
decoration: BoxDecoration( onTap: downloading
color: isMe ? null
? ctx.cs.onPrimaryContainer.withValues(alpha: 0.12) : () => _downloadFile(ctx.context, file, name),
: ctx.cs.surfaceContainerHighest, child: Container(
shape: BoxShape.circle, width: 34,
), height: 34,
child: Icon( decoration: BoxDecoration(
Symbols.download, color: isMe
color: isMe ? ctx.cs.onPrimaryContainer : ctx.cs.primary, ? ctx.cs.onPrimaryContainer.withValues(alpha: 0.12)
size: 18, : ctx.cs.surfaceContainerHighest,
), shape: BoxShape.circle,
), ),
child: downloading
? Padding(
padding: const EdgeInsets.all(8),
child: CircularProgressIndicator(
strokeWidth: 2,
value: progress > 0 ? progress : null,
color: isMe
? ctx.cs.onPrimaryContainer
: ctx.cs.primary,
),
)
: Icon(
Symbols.download,
color: isMe
? ctx.cs.onPrimaryContainer
: ctx.cs.primary,
size: 18,
),
),
);
},
), ),
], ],
), ),
@@ -1698,20 +1741,20 @@ class MessageBubble extends StatelessWidget {
return; return;
} }
Haptics.tap(); Haptics.tap();
showCustomNotification(context, 'Скачивание «$name»…');
final url = await messagesModule.getFileUrl( final cacheName = '${fileId}_$name';
messageId: message.id,
chatId: message.chatId, MediaDownloadProgress.set(cacheName, 0);
fileId: fileId, final result = await openCachedFile(
cacheName,
() => messagesModule.getFileUrl(
messageId: message.id,
chatId: message.chatId,
fileId: fileId,
),
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
); );
if (!context.mounted) return; MediaDownloadProgress.set(cacheName, null);
if (url == null) {
showCustomNotification(context, 'Не удалось получить файл');
return;
}
final result = await downloadAndOpenFile(url, name);
if (!context.mounted) return; if (!context.mounted) return;
if (!result.ok) { if (!result.ok) {
showCustomNotification( showCustomNotification(
+51 -6
View File
@@ -1,11 +1,20 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class VideoPlayerScreen extends StatefulWidget { import '../../core/utils/media_cache.dart';
final String url;
const VideoPlayerScreen({super.key, required this.url}); class VideoPlayerScreen extends StatefulWidget {
final String cacheName;
final String? url;
const VideoPlayerScreen({
super.key,
required this.cacheName,
this.url,
});
@override @override
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState(); State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
@@ -14,6 +23,7 @@ class VideoPlayerScreen extends StatefulWidget {
class _VideoPlayerScreenState extends State<VideoPlayerScreen> { class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
VideoPlayerController? _controller; VideoPlayerController? _controller;
bool _error = false; bool _error = false;
double _progress = 0;
@override @override
void initState() { void initState() {
@@ -22,7 +32,23 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
} }
Future<void> _init() async { Future<void> _init() async {
final controller = VideoPlayerController.networkUrl(Uri.parse(widget.url)); File? file = await MediaCache.existing(widget.cacheName);
if (file == null && widget.url != null) {
file = await MediaCache.getOrDownload(
widget.cacheName,
widget.url!,
onProgress: (p) {
if (mounted) setState(() => _progress = p);
},
);
}
if (!mounted) return;
if (file == null) {
setState(() => _error = true);
return;
}
final controller = VideoPlayerController.file(file);
_controller = controller; _controller = controller;
try { try {
await controller.initialize(); await controller.initialize();
@@ -69,7 +95,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
aspectRatio: c.value.aspectRatio, aspectRatio: c.value.aspectRatio,
child: VideoPlayer(c), child: VideoPlayer(c),
) )
: const CircularProgressIndicator(color: Colors.white), : _buildLoading(),
), ),
if (ready) if (ready)
Positioned.fill( Positioned.fill(
@@ -83,7 +109,7 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
child: Container( child: Container(
width: 64, width: 64,
height: 64, height: 64,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.black54, color: Colors.black54,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
@@ -117,4 +143,23 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
), ),
); );
} }
Widget _buildLoading() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
color: Colors.white,
value: _progress > 0 && _progress < 1 ? _progress : null,
),
if (_progress > 0 && _progress < 1) ...[
const SizedBox(height: 12),
Text(
'${(_progress * 100).round()}%',
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
],
],
);
}
} }
+3
View File
@@ -21,6 +21,7 @@ import 'core/config/app_message_actions_style.dart';
import 'core/config/app_swipe_back_desktop.dart'; import 'core/config/app_swipe_back_desktop.dart';
import 'core/config/app_pranks.dart'; import 'core/config/app_pranks.dart';
import 'core/config/app_stories.dart'; import 'core/config/app_stories.dart';
import 'core/config/app_media_cache.dart';
import 'core/config/app_theme_mode.dart'; import 'core/config/app_theme_mode.dart';
import 'core/config/app_theme_schedule.dart'; import 'core/config/app_theme_schedule.dart';
import 'backend/modules/account.dart'; import 'backend/modules/account.dart';
@@ -88,6 +89,7 @@ void main() async {
final swipeBackFuture = AppSwipeBackDesktop.load(); final swipeBackFuture = AppSwipeBackDesktop.load();
final pranksFuture = AppPranks.load(); final pranksFuture = AppPranks.load();
final storiesFuture = AppStories.load(); final storiesFuture = AppStories.load();
final cacheLimitFuture = AppMediaCacheLimit.load();
await api.connect(); await api.connect();
@@ -121,6 +123,7 @@ void main() async {
AppSwipeBackDesktop.current.value = await swipeBackFuture; AppSwipeBackDesktop.current.value = await swipeBackFuture;
AppPranks.current.value = await pranksFuture; AppPranks.current.value = await pranksFuture;
AppStories.current.value = await storiesFuture; AppStories.current.value = await storiesFuture;
AppMediaCacheLimit.current.value = await cacheLimitFuture;
runApp( runApp(
KometApp( KometApp(
initialLocale: initialLocale, initialLocale: initialLocale,