fix: починил загрузку файлов
This commit is contained in:
@@ -1786,9 +1786,10 @@ class MessagesModule {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _api.sendRequest(Opcode.fileDownload, {
|
final response = await _api.sendRequest(Opcode.fileDownload, {
|
||||||
'messageId': int.tryParse(messageId) ?? 0,
|
|
||||||
'chatId': chatId,
|
|
||||||
'fileId': fileId,
|
'fileId': fileId,
|
||||||
|
'chatId': chatId,
|
||||||
|
'messageId': int.tryParse(messageId) ?? 0,
|
||||||
|
'itemType': 'REGULAR',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.isOk) return null;
|
if (!response.isOk) return null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ class MediaCache {
|
|||||||
static Directory? _dir;
|
static Directory? _dir;
|
||||||
static int? _cachedSize;
|
static int? _cachedSize;
|
||||||
static final Map<String, Future<File?>> _inFlight = {};
|
static final Map<String, Future<File?>> _inFlight = {};
|
||||||
|
static final Set<String> _present = {};
|
||||||
|
static final Map<String, ValueNotifier<bool>> _presence = {};
|
||||||
|
|
||||||
static Future<Directory> _cacheDir() async {
|
static Future<Directory> _cacheDir() async {
|
||||||
final cached = _dir;
|
final cached = _dir;
|
||||||
@@ -49,8 +52,10 @@ class MediaCache {
|
|||||||
try {
|
try {
|
||||||
await file.setLastModified(DateTime.now());
|
await file.setLastModified(DateTime.now());
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
_markPresent(name, true);
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
_markPresent(name, false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +108,7 @@ class MediaCache {
|
|||||||
}
|
}
|
||||||
await sink.close();
|
await sink.close();
|
||||||
await part.rename(file.path);
|
await part.rename(file.path);
|
||||||
|
_markPresent(name, true);
|
||||||
final known = _cachedSize;
|
final known = _cachedSize;
|
||||||
if (known != null) {
|
if (known != null) {
|
||||||
try {
|
try {
|
||||||
@@ -161,6 +167,10 @@ class MediaCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_cachedSize = 0;
|
_cachedSize = 0;
|
||||||
|
_present.clear();
|
||||||
|
for (final notifier in _presence.values) {
|
||||||
|
notifier.value = false;
|
||||||
|
}
|
||||||
return freed;
|
return freed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +205,35 @@ class MediaCache {
|
|||||||
try {
|
try {
|
||||||
total -= await file.length();
|
total -= await file.length();
|
||||||
await file.delete();
|
await file.delete();
|
||||||
|
_markAbsentByBasename(p.basename(file.path));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
_cachedSize = total;
|
_cachedSize = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Реактивный флаг наличия файла [name] в кэше (для UI-иконки «скачано»).
|
||||||
|
static ValueListenable<bool> presence(String name) {
|
||||||
|
final key = _sanitize(name);
|
||||||
|
return _presence.putIfAbsent(key, () {
|
||||||
|
final notifier = ValueNotifier(_present.contains(key));
|
||||||
|
if (!notifier.value) existing(name).ignore();
|
||||||
|
return notifier;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _markPresent(String name, bool present) {
|
||||||
|
final key = _sanitize(name);
|
||||||
|
final changed = present ? _present.add(key) : _present.remove(key);
|
||||||
|
if (!changed) return;
|
||||||
|
_presence[key]?.value = present;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _markAbsentByBasename(String basename) {
|
||||||
|
if (_present.remove(basename)) {
|
||||||
|
_presence[basename]?.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String _sanitize(String name) {
|
static String _sanitize(String name) {
|
||||||
final cleaned = name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
|
final cleaned = name.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
|
||||||
return cleaned.isEmpty ? 'file' : cleaned;
|
return cleaned.isEmpty ? 'file' : cleaned;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
this.showAttachButton = true,
|
this.showAttachButton = true,
|
||||||
this.forceSend = false,
|
this.forceSend = false,
|
||||||
this.hintText = 'Message',
|
this.hintText = 'Message',
|
||||||
|
this.bottomSafe = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String chatType;
|
final String chatType;
|
||||||
@@ -87,6 +88,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
final bool showAttachButton;
|
final bool showAttachButton;
|
||||||
final bool forceSend;
|
final bool forceSend;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
|
final bool bottomSafe;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -172,6 +174,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final bar = SafeArea(
|
final bar = SafeArea(
|
||||||
|
bottom: bottomSafe,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -2353,9 +2353,12 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ComposerInputBar(
|
AnimatedBuilder(
|
||||||
chatType: _commentsMode ? 'CHAT' : widget.chatType,
|
animation: _stickers.anim,
|
||||||
chrome: _effectiveChrome,
|
builder: (context, _) => ComposerInputBar(
|
||||||
|
bottomSafe: _stickers.anim.value == 0,
|
||||||
|
chatType: _commentsMode ? 'CHAT' : widget.chatType,
|
||||||
|
chrome: _effectiveChrome,
|
||||||
style: AppComposerStyle.current.value,
|
style: AppComposerStyle.current.value,
|
||||||
background: AppComposerBackground.current.value,
|
background: AppComposerBackground.current.value,
|
||||||
backdropKey: _pillBackdrop,
|
backdropKey: _pillBackdrop,
|
||||||
@@ -2385,8 +2388,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
onSubscribe: _subscribeChannel,
|
onSubscribe: _subscribeChannel,
|
||||||
showStickerButton: !_commentsMode,
|
showStickerButton: !_commentsMode,
|
||||||
showAttachButton: !_commentsMode,
|
showAttachButton: !_commentsMode,
|
||||||
forceSend: _commentsMode,
|
forceSend: _commentsMode,
|
||||||
hintText: _commentsMode ? 'Комментарий' : 'Message',
|
hintText: _commentsMode ? 'Комментарий' : 'Message',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
StickerPanelView(
|
StickerPanelView(
|
||||||
stickers: _stickers,
|
stickers: _stickers,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:komet/main.dart';
|
|||||||
|
|
||||||
import '../../../../core/utils/download_progress.dart';
|
import '../../../../core/utils/download_progress.dart';
|
||||||
import '../../../../core/utils/file_download.dart';
|
import '../../../../core/utils/file_download.dart';
|
||||||
|
import '../../../../core/utils/media_cache.dart';
|
||||||
import '../../../../core/utils/format.dart';
|
import '../../../../core/utils/format.dart';
|
||||||
import '../../../../core/utils/haptics.dart';
|
import '../../../../core/utils/haptics.dart';
|
||||||
import '../../../../models/attachment.dart';
|
import '../../../../models/attachment.dart';
|
||||||
@@ -113,38 +114,49 @@ class FileBubble extends StatelessWidget {
|
|||||||
ValueListenableBuilder<double?>(
|
ValueListenableBuilder<double?>(
|
||||||
valueListenable: MediaDownloadProgress.notifier(cacheName),
|
valueListenable: MediaDownloadProgress.notifier(cacheName),
|
||||||
builder: (context, progress, _) {
|
builder: (context, progress, _) {
|
||||||
final downloading = progress != null;
|
final iconColor = isMe
|
||||||
return GestureDetector(
|
? ctx.cs.onPrimaryContainer
|
||||||
onTap: downloading
|
: ctx.cs.primary;
|
||||||
? null
|
Widget circle(Widget child, VoidCallback? onTap) {
|
||||||
: () => _downloadFile(ctx.context, file, name),
|
return GestureDetector(
|
||||||
child: Container(
|
onTap: onTap,
|
||||||
width: 34,
|
child: Container(
|
||||||
height: 34,
|
width: 34,
|
||||||
decoration: BoxDecoration(
|
height: 34,
|
||||||
color: isMe
|
decoration: BoxDecoration(
|
||||||
? ctx.systemTint
|
color: isMe
|
||||||
: ctx.cs.surfaceContainerHighest,
|
? ctx.systemTint
|
||||||
shape: BoxShape.circle,
|
: ctx.cs.surfaceContainerHighest,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
child: downloading
|
);
|
||||||
? Padding(
|
}
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: CircularProgressIndicator(
|
if (progress != null) {
|
||||||
strokeWidth: 2,
|
return circle(
|
||||||
value: progress > 0 ? progress : null,
|
Padding(
|
||||||
color: isMe
|
padding: const EdgeInsets.all(8),
|
||||||
? ctx.cs.onPrimaryContainer
|
child: CircularProgressIndicator(
|
||||||
: ctx.cs.primary,
|
strokeWidth: 2,
|
||||||
),
|
value: progress > 0 ? progress : null,
|
||||||
)
|
color: iconColor,
|
||||||
: Icon(
|
),
|
||||||
Symbols.download,
|
),
|
||||||
color: isMe
|
null,
|
||||||
? ctx.cs.onPrimaryContainer
|
);
|
||||||
: ctx.cs.primary,
|
}
|
||||||
size: 18,
|
|
||||||
),
|
return ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: MediaCache.presence(cacheName),
|
||||||
|
builder: (context, cached, _) => circle(
|
||||||
|
Icon(
|
||||||
|
cached ? Symbols.check : Symbols.download,
|
||||||
|
color: iconColor,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
() => _downloadFile(ctx.context, file, name),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -171,8 +183,9 @@ class FileBubble extends StatelessWidget {
|
|||||||
Haptics.tap();
|
Haptics.tap();
|
||||||
|
|
||||||
final cacheName = '${fileId}_$name';
|
final cacheName = '${fileId}_$name';
|
||||||
|
final cached = (await MediaCache.existing(cacheName)) != null;
|
||||||
|
|
||||||
MediaDownloadProgress.set(cacheName, 0);
|
if (!cached) MediaDownloadProgress.set(cacheName, 0);
|
||||||
final result = await openCachedFile(
|
final result = await openCachedFile(
|
||||||
cacheName,
|
cacheName,
|
||||||
() => messagesModule.getFileUrl(
|
() => messagesModule.getFileUrl(
|
||||||
@@ -182,7 +195,7 @@ class FileBubble extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
onProgress: (p) => MediaDownloadProgress.set(cacheName, p),
|
||||||
);
|
);
|
||||||
MediaDownloadProgress.set(cacheName, null);
|
if (!cached) MediaDownloadProgress.set(cacheName, null);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showCustomNotification(
|
showCustomNotification(
|
||||||
|
|||||||
Reference in New Issue
Block a user