feat: предпросмотр расшифрованный
This commit is contained in:
@@ -12,13 +12,17 @@ import '../../../../core/utils/media_cache.dart';
|
||||
import '../../../../core/utils/format.dart';
|
||||
import '../../../../core/utils/haptics.dart';
|
||||
import '../../../../core/crypto/chat_crypto_service.dart';
|
||||
import '../../../../core/crypto/encrypted_photo.dart';
|
||||
import '../../../../core/crypto/encrypted_photo_cache.dart';
|
||||
import '../../../../models/attachment.dart';
|
||||
import '../../custom_notification.dart';
|
||||
import '../../decrypted_photo.dart';
|
||||
import '../../photo_viewer.dart';
|
||||
import 'bubble_context.dart';
|
||||
|
||||
class FileBubble extends StatelessWidget {
|
||||
static const double _previewWidth = 240;
|
||||
static const double _previewHeight = 160;
|
||||
|
||||
final BubbleContext ctx;
|
||||
final FileAttachment file;
|
||||
final bool fill;
|
||||
@@ -41,6 +45,11 @@ class FileBubble extends StatelessWidget {
|
||||
|
||||
final preview = file.preview;
|
||||
final previewUrl = preview?.baseUrl ?? preview?.previewData ?? '';
|
||||
final previewWidget = _preview(
|
||||
cacheName: cacheName,
|
||||
previewUrl: previewUrl,
|
||||
encrypted: fileId != null && _isEncryptedImage(name),
|
||||
);
|
||||
|
||||
final inner = Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 4),
|
||||
@@ -48,21 +57,7 @@ class FileBubble extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (previewUrl.isNotEmpty) ...[
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: previewUrl,
|
||||
width: 240,
|
||||
height: 160,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 480,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
?previewWidget,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -185,6 +180,116 @@ class FileBubble extends StatelessWidget {
|
||||
static bool _isViewableImage(String name) =>
|
||||
name.toLowerCase().endsWith('.png');
|
||||
|
||||
bool _isEncryptedImage(String name) =>
|
||||
_isViewableImage(name) &&
|
||||
ChatCryptoService.instance.isEnabled(
|
||||
ctx.message.accountId,
|
||||
ctx.message.chatId,
|
||||
);
|
||||
|
||||
Widget? _preview({
|
||||
required String cacheName,
|
||||
required String previewUrl,
|
||||
required bool encrypted,
|
||||
}) {
|
||||
if (encrypted) {
|
||||
return DecryptedPhoto(
|
||||
accountId: ctx.message.accountId,
|
||||
chatId: ctx.message.chatId,
|
||||
cacheName: cacheName,
|
||||
size: file.size ?? 0,
|
||||
urlLoader: _fileUrl,
|
||||
builder: (view) => _encryptedPreview(view, previewUrl),
|
||||
);
|
||||
}
|
||||
if (previewUrl.isEmpty) return null;
|
||||
return _networkPreview(previewUrl);
|
||||
}
|
||||
|
||||
Widget _encryptedPreview(EncryptedPhotoView? view, String previewUrl) {
|
||||
switch (view?.status) {
|
||||
case EncryptedPhotoStatus.decrypted:
|
||||
return _framed(
|
||||
Image.file(
|
||||
view!.file!,
|
||||
width: _previewWidth,
|
||||
height: _previewHeight,
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: 480,
|
||||
errorBuilder: (_, _, _) => _placeholder(
|
||||
icon: Symbols.broken_image,
|
||||
label: 'Файл повреждён',
|
||||
),
|
||||
),
|
||||
);
|
||||
case EncryptedPhotoStatus.plain:
|
||||
return previewUrl.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _networkPreview(previewUrl);
|
||||
case EncryptedPhotoStatus.wrongKey:
|
||||
return _placeholder(icon: Symbols.lock, label: 'Неверный ключ');
|
||||
case EncryptedPhotoStatus.locked:
|
||||
return _placeholder(
|
||||
icon: Symbols.lock,
|
||||
label: 'Нажмите, чтобы открыть',
|
||||
);
|
||||
case null:
|
||||
return _placeholder();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _networkPreview(String url) => _framed(
|
||||
CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
width: _previewWidth,
|
||||
height: _previewHeight,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 480,
|
||||
fadeInDuration: const Duration(milliseconds: 120),
|
||||
errorWidget: (_, _, _) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _placeholder({IconData? icon, String? label}) => _framed(
|
||||
Container(
|
||||
width: _previewWidth,
|
||||
height: _previewHeight,
|
||||
alignment: Alignment.center,
|
||||
color: ctx.isMe ? ctx.systemTint : ctx.cs.surfaceContainerHighest,
|
||||
child: icon == null
|
||||
? SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: ctx.dim),
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 26, color: ctx.dim),
|
||||
if (label != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(label, style: TextStyle(color: ctx.dim, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _framed(Widget child) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: ClipRRect(borderRadius: BorderRadius.circular(10), child: child),
|
||||
);
|
||||
|
||||
Future<String?> _fileUrl() {
|
||||
final fileId = file.fileId;
|
||||
if (fileId == null) return Future.value(null);
|
||||
return messagesModule.getFileUrl(
|
||||
messageId: ctx.message.id,
|
||||
chatId: ctx.message.chatId,
|
||||
fileId: fileId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openInViewer(
|
||||
BuildContext context,
|
||||
String name,
|
||||
@@ -198,11 +303,7 @@ class FileBubble extends StatelessWidget {
|
||||
if (!wasCached) MediaDownloadProgress.set(cacheName, 0);
|
||||
File? local;
|
||||
try {
|
||||
final url = await messagesModule.getFileUrl(
|
||||
messageId: ctx.message.id,
|
||||
chatId: ctx.message.chatId,
|
||||
fileId: fileId,
|
||||
);
|
||||
final url = await _fileUrl();
|
||||
if (url != null && url.isNotEmpty) {
|
||||
local = await MediaCache.getOrDownload(
|
||||
cacheName,
|
||||
@@ -241,12 +342,9 @@ class FileBubble extends StatelessWidget {
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
final shown = await _decryptIfNeeded(local, cacheName);
|
||||
if (!context.mounted) return;
|
||||
if (shown == null) {
|
||||
showCustomNotification(context, 'Неверный ключ');
|
||||
return;
|
||||
}
|
||||
final shown = await _decryptIfNeeded(context, local, cacheName);
|
||||
if (!context.mounted || shown == null) return;
|
||||
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -260,20 +358,35 @@ class FileBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<File?> _decryptIfNeeded(File local, String cacheName) async {
|
||||
Future<File?> _decryptIfNeeded(
|
||||
BuildContext context,
|
||||
File local,
|
||||
String cacheName,
|
||||
) async {
|
||||
final accountId = ctx.message.accountId;
|
||||
final chatId = ctx.message.chatId;
|
||||
if (!ChatCryptoService.instance.isEnabled(accountId, chatId)) return local;
|
||||
if (!await ChatCryptoService.instance.looksEncryptedImage(local.path)) {
|
||||
return local;
|
||||
}
|
||||
final result = await openEncryptedPhoto(
|
||||
|
||||
final view = await EncryptedPhotoCache.instance.resolve(
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
encrypted: local,
|
||||
cacheName: cacheName,
|
||||
urlLoader: _fileUrl,
|
||||
);
|
||||
return result.file;
|
||||
if (!context.mounted) return null;
|
||||
|
||||
switch (view.status) {
|
||||
case EncryptedPhotoStatus.decrypted:
|
||||
return view.file;
|
||||
case EncryptedPhotoStatus.plain:
|
||||
return local;
|
||||
case EncryptedPhotoStatus.wrongKey:
|
||||
showCustomNotification(context, 'Неверный ключ');
|
||||
return null;
|
||||
case EncryptedPhotoStatus.locked:
|
||||
showCustomNotification(context, 'Не удалось расшифровать фото');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(
|
||||
|
||||
Reference in New Issue
Block a user