2025-08-20 18:39:30 +05:30
|
|
|
import 'dart:convert';
|
2025-08-21 15:19:47 +05:30
|
|
|
import 'dart:io';
|
2025-11-01 00:57:40 +05:30
|
|
|
import 'dart:typed_data';
|
2025-09-25 23:22:48 +05:30
|
|
|
import 'package:flutter/material.dart';
|
2025-08-20 18:39:30 +05:30
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
2025-08-21 12:49:41 +05:30
|
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
2025-11-29 11:04:59 +05:30
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
2025-08-21 15:19:47 +05:30
|
|
|
import 'package:dio/dio.dart' as dio;
|
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
2025-08-20 18:39:30 +05:30
|
|
|
import '../../../shared/theme/theme_extensions.dart';
|
2025-08-23 20:09:43 +05:30
|
|
|
import 'package:conduit/l10n/app_localizations.dart';
|
2025-08-20 18:39:30 +05:30
|
|
|
import '../../../core/providers/app_providers.dart';
|
2025-08-20 23:42:31 +05:30
|
|
|
import '../../auth/providers/unified_auth_providers.dart';
|
2025-09-25 22:36:42 +05:30
|
|
|
import '../../../core/utils/debug_logger.dart';
|
2025-10-23 17:36:31 +05:30
|
|
|
import '../../../core/network/self_signed_image_cache_manager.dart';
|
|
|
|
|
import '../../../core/network/image_header_utils.dart';
|
2025-11-01 00:57:40 +05:30
|
|
|
import '../../../core/services/worker_manager.dart';
|
2025-09-25 22:36:42 +05:30
|
|
|
|
2025-08-21 12:49:41 +05:30
|
|
|
// Simple global cache to prevent reloading
|
2025-08-20 18:39:30 +05:30
|
|
|
final _globalImageCache = <String, String>{};
|
2025-08-21 12:49:41 +05:30
|
|
|
final _globalLoadingStates = <String, bool>{};
|
|
|
|
|
final _globalErrorStates = <String, String>{};
|
2025-10-19 14:18:26 +05:30
|
|
|
final _globalImageBytesCache = <String, Uint8List>{};
|
2025-11-29 11:04:59 +05:30
|
|
|
final _globalSvgStates = <String, bool>{};
|
2025-10-19 14:18:26 +05:30
|
|
|
final _base64WhitespacePattern = RegExp(r'\s');
|
|
|
|
|
|
|
|
|
|
Uint8List _decodeImageData(String data) {
|
|
|
|
|
var payload = data;
|
|
|
|
|
if (payload.startsWith('data:')) {
|
|
|
|
|
final commaIndex = payload.indexOf(',');
|
|
|
|
|
if (commaIndex == -1) {
|
|
|
|
|
throw FormatException('Invalid data URI');
|
|
|
|
|
}
|
|
|
|
|
payload = payload.substring(commaIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
payload = payload.replaceAll(_base64WhitespacePattern, '');
|
|
|
|
|
return base64.decode(payload);
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
|
2025-11-29 11:04:59 +05:30
|
|
|
/// Checks if data URL or content indicates SVG format.
|
|
|
|
|
bool _isSvgDataUrl(String data) {
|
|
|
|
|
final lower = data.toLowerCase();
|
|
|
|
|
return lower.startsWith('data:image/svg+xml');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Checks if a URL points to an SVG file.
|
|
|
|
|
bool _isSvgUrl(String url) {
|
|
|
|
|
final lowerUrl = url.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Check for .svg file extension (with or without query string)
|
|
|
|
|
final queryIndex = lowerUrl.indexOf('?');
|
|
|
|
|
final pathPart = queryIndex >= 0 ? lowerUrl.substring(0, queryIndex) : lowerUrl;
|
|
|
|
|
if (pathPart.endsWith('.svg')) return true;
|
|
|
|
|
|
|
|
|
|
// Check for SVG MIME type in query parameters only (not in path)
|
|
|
|
|
// This handles cases like ?format=image/svg+xml or &type=image/svg+xml
|
|
|
|
|
if (queryIndex >= 0) {
|
|
|
|
|
final queryPart = lowerUrl.substring(queryIndex);
|
|
|
|
|
if (queryPart.contains('image/svg+xml')) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Checks if decoded bytes represent SVG content by looking for the SVG tag.
|
|
|
|
|
bool _isSvgBytes(Uint8List bytes) {
|
|
|
|
|
// Check first 1KB for SVG tag (not just XML declaration, which is too broad)
|
|
|
|
|
final checkLength = bytes.length < 1024 ? bytes.length : 1024;
|
|
|
|
|
final header = utf8.decode(
|
|
|
|
|
bytes.sublist(0, checkLength),
|
|
|
|
|
allowMalformed: true,
|
|
|
|
|
);
|
|
|
|
|
return header.toLowerCase().contains('<svg');
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
class EnhancedImageAttachment extends ConsumerStatefulWidget {
|
|
|
|
|
final String attachmentId;
|
|
|
|
|
final bool isMarkdownFormat;
|
|
|
|
|
final VoidCallback? onTap;
|
|
|
|
|
final BoxConstraints? constraints;
|
|
|
|
|
final bool isUserMessage;
|
2025-08-21 12:49:41 +05:30
|
|
|
final bool disableAnimation;
|
2025-08-20 18:39:30 +05:30
|
|
|
|
|
|
|
|
const EnhancedImageAttachment({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.attachmentId,
|
|
|
|
|
this.isMarkdownFormat = false,
|
|
|
|
|
this.onTap,
|
|
|
|
|
this.constraints,
|
|
|
|
|
this.isUserMessage = false,
|
2025-08-21 12:49:41 +05:30
|
|
|
this.disableAnimation = false,
|
2025-08-20 18:39:30 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
ConsumerState<EnhancedImageAttachment> createState() =>
|
|
|
|
|
_EnhancedImageAttachmentState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _EnhancedImageAttachmentState
|
|
|
|
|
extends ConsumerState<EnhancedImageAttachment>
|
2025-09-07 11:29:29 +05:30
|
|
|
with AutomaticKeepAliveClientMixin {
|
2025-08-20 18:39:30 +05:30
|
|
|
String? _cachedImageData;
|
2025-10-19 14:18:26 +05:30
|
|
|
Uint8List? _cachedBytes;
|
2025-08-20 18:39:30 +05:30
|
|
|
bool _isLoading = true;
|
|
|
|
|
String? _errorMessage;
|
2025-10-19 14:18:26 +05:30
|
|
|
bool _isDecoding = false;
|
2025-11-29 11:04:59 +05:30
|
|
|
bool _isSvg = false;
|
2025-10-19 14:18:26 +05:30
|
|
|
late final String _heroTag;
|
2025-09-07 11:29:29 +05:30
|
|
|
// Removed unused animation and state flags
|
2025-08-20 18:39:30 +05:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-10-19 14:18:26 +05:30
|
|
|
_heroTag = 'image_${widget.attachmentId}_${identityHashCode(this)}';
|
2025-08-24 20:55:51 +05:30
|
|
|
// Defer loading until after first frame to avoid accessing inherited widgets
|
|
|
|
|
// (e.g., Localizations) during initState
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
_loadImage();
|
|
|
|
|
});
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
2025-08-21 12:49:41 +05:30
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
Future<void> _loadImage() async {
|
2025-08-23 20:09:43 +05:30
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2025-10-19 14:18:26 +05:30
|
|
|
final cachedError = _globalErrorStates[widget.attachmentId];
|
|
|
|
|
if (cachedError != null) {
|
2025-08-20 18:39:30 +05:30
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
2025-10-19 14:18:26 +05:30
|
|
|
_errorMessage = cachedError;
|
2025-08-20 18:39:30 +05:30
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
if (_globalImageCache.containsKey(widget.attachmentId)) {
|
|
|
|
|
final cachedData = _globalImageCache[widget.attachmentId]!;
|
|
|
|
|
final cachedBytes = _globalImageBytesCache[widget.attachmentId];
|
2025-11-29 11:04:59 +05:30
|
|
|
final cachedIsSvg = _globalSvgStates[widget.attachmentId] ?? false;
|
2025-08-21 12:49:41 +05:30
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
2025-10-19 14:18:26 +05:30
|
|
|
_cachedImageData = cachedData;
|
|
|
|
|
_cachedBytes = cachedBytes;
|
2025-11-29 11:04:59 +05:30
|
|
|
_isSvg = cachedIsSvg;
|
2025-10-19 14:18:26 +05:30
|
|
|
_isLoading = cachedBytes == null && !_isRemoteContent(cachedData);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (cachedBytes == null && !_isRemoteContent(cachedData)) {
|
|
|
|
|
await _decodeAndAssign(cachedData, l10n);
|
|
|
|
|
} else if (mounted) {
|
|
|
|
|
setState(() {
|
2025-08-21 12:49:41 +05:30
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_globalLoadingStates[widget.attachmentId] = true;
|
|
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
final attachmentId = widget.attachmentId;
|
|
|
|
|
|
|
|
|
|
if (attachmentId.startsWith('data:') || attachmentId.startsWith('http')) {
|
2025-11-29 11:04:59 +05:30
|
|
|
// Detect SVG from data URL or HTTP URL
|
|
|
|
|
final isSvgContent =
|
|
|
|
|
_isSvgDataUrl(attachmentId) || _isSvgUrl(attachmentId);
|
2025-10-19 14:18:26 +05:30
|
|
|
_globalImageCache[attachmentId] = attachmentId;
|
|
|
|
|
_globalLoadingStates[attachmentId] = false;
|
2025-11-29 11:04:59 +05:30
|
|
|
_globalSvgStates[attachmentId] = isSvgContent;
|
2025-10-19 14:18:26 +05:30
|
|
|
final cachedBytes = _globalImageBytesCache[attachmentId];
|
2025-08-20 18:39:30 +05:30
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
2025-10-19 14:18:26 +05:30
|
|
|
_cachedImageData = attachmentId;
|
|
|
|
|
_cachedBytes = cachedBytes;
|
2025-11-29 11:04:59 +05:30
|
|
|
_isSvg = isSvgContent;
|
2025-10-19 14:18:26 +05:30
|
|
|
_isLoading = cachedBytes == null && !_isRemoteContent(attachmentId);
|
2025-08-20 18:39:30 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-10-19 14:18:26 +05:30
|
|
|
if (!_isRemoteContent(attachmentId) && cachedBytes == null) {
|
|
|
|
|
await _decodeAndAssign(attachmentId, l10n);
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
return;
|
|
|
|
|
}
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
if (attachmentId.startsWith('/')) {
|
2025-08-20 23:42:31 +05:30
|
|
|
final api = ref.read(apiServiceProvider);
|
|
|
|
|
if (api != null) {
|
2025-10-19 14:18:26 +05:30
|
|
|
final fullUrl = api.baseUrl + attachmentId;
|
2025-11-29 11:04:59 +05:30
|
|
|
final isSvgContent = _isSvgUrl(fullUrl);
|
2025-10-19 14:18:26 +05:30
|
|
|
_globalImageCache[attachmentId] = fullUrl;
|
|
|
|
|
_globalLoadingStates[attachmentId] = false;
|
2025-11-29 11:04:59 +05:30
|
|
|
_globalSvgStates[attachmentId] = isSvgContent;
|
2025-08-20 23:42:31 +05:30
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_cachedImageData = fullUrl;
|
2025-10-19 14:18:26 +05:30
|
|
|
_cachedBytes = null;
|
2025-11-29 11:04:59 +05:30
|
|
|
_isSvg = isSvgContent;
|
2025-08-20 23:42:31 +05:30
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
2025-08-23 20:09:43 +05:30
|
|
|
final error = l10n.unableToLoadImage;
|
2025-10-19 14:18:26 +05:30
|
|
|
_cacheError(error);
|
2025-08-20 23:42:31 +05:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
|
|
|
|
|
final api = ref.read(apiServiceProvider);
|
|
|
|
|
if (api == null) {
|
2025-08-23 20:09:43 +05:30
|
|
|
final error = l10n.apiUnavailable;
|
2025-10-19 14:18:26 +05:30
|
|
|
_cacheError(error);
|
2025-08-20 18:39:30 +05:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-10-19 14:18:26 +05:30
|
|
|
final fileInfo = await api.getFileInfo(attachmentId);
|
2025-08-20 18:39:30 +05:30
|
|
|
final fileName = _extractFileName(fileInfo);
|
|
|
|
|
final ext = fileName.toLowerCase().split('.').last;
|
|
|
|
|
|
|
|
|
|
if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].contains(ext)) {
|
2025-08-23 20:09:43 +05:30
|
|
|
final error = l10n.notAnImageFile(fileName);
|
2025-10-19 14:18:26 +05:30
|
|
|
_cacheError(error);
|
2025-08-20 18:39:30 +05:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 11:04:59 +05:30
|
|
|
// Track if this is an SVG file based on extension
|
|
|
|
|
final isSvgFile = ext == 'svg';
|
|
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
final fileContent = await api.getFileContent(attachmentId);
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
_globalImageCache[attachmentId] = fileContent;
|
|
|
|
|
_globalLoadingStates[attachmentId] = false;
|
2025-11-29 11:04:59 +05:30
|
|
|
_globalSvgStates[attachmentId] = isSvgFile;
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
if (_globalImageCache.length > 50) {
|
2025-08-21 12:49:41 +05:30
|
|
|
final firstKey = _globalImageCache.keys.first;
|
|
|
|
|
_globalImageCache.remove(firstKey);
|
|
|
|
|
_globalLoadingStates.remove(firstKey);
|
|
|
|
|
_globalErrorStates.remove(firstKey);
|
2025-10-19 14:18:26 +05:30
|
|
|
_globalImageBytesCache.remove(firstKey);
|
2025-11-29 11:04:59 +05:30
|
|
|
_globalSvgStates.remove(firstKey);
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_cachedImageData = fileContent;
|
2025-10-19 14:18:26 +05:30
|
|
|
_cachedBytes = null;
|
2025-11-29 11:04:59 +05:30
|
|
|
_isSvg = isSvgFile;
|
2025-10-19 14:18:26 +05:30
|
|
|
_isLoading = !_isRemoteContent(fileContent);
|
2025-08-20 18:39:30 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-10-19 14:18:26 +05:30
|
|
|
|
|
|
|
|
if (_isRemoteContent(fileContent)) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
setState(() {
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await _decodeAndAssign(fileContent, l10n);
|
2025-08-20 18:39:30 +05:30
|
|
|
} catch (e) {
|
2025-08-23 20:09:43 +05:30
|
|
|
final error = l10n.failedToLoadImage(e.toString());
|
2025-10-19 14:18:26 +05:30
|
|
|
_cacheError(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _isRemoteContent(String data) => data.startsWith('http');
|
|
|
|
|
|
|
|
|
|
Future<void> _decodeAndAssign(String data, AppLocalizations l10n) async {
|
|
|
|
|
if (_isDecoding) return;
|
|
|
|
|
_isDecoding = true;
|
|
|
|
|
try {
|
2025-11-01 00:57:40 +05:30
|
|
|
final worker = ref.read(workerManagerProvider);
|
|
|
|
|
final bytes = await worker.schedule(
|
|
|
|
|
_decodeImageData,
|
|
|
|
|
data,
|
|
|
|
|
debugLabel: 'decode_image',
|
|
|
|
|
);
|
2025-10-19 14:18:26 +05:30
|
|
|
_globalImageBytesCache[widget.attachmentId] = bytes;
|
2025-11-29 11:04:59 +05:30
|
|
|
|
|
|
|
|
// Use byte content as authoritative SVG detection when positive, but
|
|
|
|
|
// preserve prior true-hints (e.g., from file extension) if detection fails.
|
|
|
|
|
final previousHint = _globalSvgStates[widget.attachmentId] ?? _isSvg;
|
|
|
|
|
final detectedSvg = _isSvgBytes(bytes) || _isSvgDataUrl(data);
|
|
|
|
|
final isSvgContent = detectedSvg ? true : previousHint;
|
|
|
|
|
_globalSvgStates[widget.attachmentId] = isSvgContent;
|
|
|
|
|
|
2025-10-19 14:18:26 +05:30
|
|
|
if (!mounted) return;
|
|
|
|
|
setState(() {
|
|
|
|
|
_cachedBytes = bytes;
|
2025-11-29 11:04:59 +05:30
|
|
|
_isSvg = isSvgContent;
|
2025-10-19 14:18:26 +05:30
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
|
|
|
|
} on FormatException {
|
|
|
|
|
final error = l10n.invalidImageFormat;
|
|
|
|
|
_cacheError(error);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
final error = l10n.failedToDecodeImage;
|
|
|
|
|
_cacheError(error);
|
|
|
|
|
} finally {
|
|
|
|
|
_isDecoding = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _cacheError(String error) {
|
|
|
|
|
_globalErrorStates[widget.attachmentId] = error;
|
|
|
|
|
_globalLoadingStates[widget.attachmentId] = false;
|
|
|
|
|
_globalImageCache.remove(widget.attachmentId);
|
|
|
|
|
_globalImageBytesCache.remove(widget.attachmentId);
|
2025-11-29 11:04:59 +05:30
|
|
|
_globalSvgStates.remove(widget.attachmentId);
|
2025-10-19 14:18:26 +05:30
|
|
|
if (!mounted) {
|
|
|
|
|
return;
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
2025-10-19 14:18:26 +05:30
|
|
|
setState(() {
|
|
|
|
|
_errorMessage = error;
|
|
|
|
|
_cachedBytes = null;
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
});
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _extractFileName(Map<String, dynamic> fileInfo) {
|
|
|
|
|
return fileInfo['filename'] ??
|
|
|
|
|
fileInfo['meta']?['name'] ??
|
|
|
|
|
fileInfo['name'] ??
|
|
|
|
|
fileInfo['file_name'] ??
|
|
|
|
|
fileInfo['original_name'] ??
|
|
|
|
|
fileInfo['original_filename'] ??
|
|
|
|
|
'unknown';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
super.build(context); // Required for AutomaticKeepAliveClientMixin
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-09-05 21:05:58 +05:30
|
|
|
// Directly return content without AnimatedSwitcher to prevent black flash during streaming
|
|
|
|
|
return _buildContent();
|
2025-08-21 12:49:41 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildContent() {
|
2025-08-20 18:39:30 +05:30
|
|
|
if (_isLoading) {
|
|
|
|
|
return _buildLoadingState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_errorMessage != null) {
|
|
|
|
|
return _buildErrorState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_cachedImageData == null) {
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle different image data formats
|
2025-11-29 11:04:59 +05:30
|
|
|
// Include fallback URL/data detection to match FullScreenImageViewer behavior
|
2025-08-21 12:49:41 +05:30
|
|
|
Widget imageWidget;
|
2025-08-20 18:39:30 +05:30
|
|
|
if (_cachedImageData!.startsWith('http')) {
|
2025-11-29 11:04:59 +05:30
|
|
|
final isSvgContent = _isSvg || _isSvgUrl(_cachedImageData!);
|
|
|
|
|
imageWidget = isSvgContent ? _buildNetworkSvg() : _buildNetworkImage();
|
2025-08-20 18:39:30 +05:30
|
|
|
} else {
|
2025-11-29 11:04:59 +05:30
|
|
|
final isSvgContent = _isSvg || _isSvgDataUrl(_cachedImageData!);
|
|
|
|
|
imageWidget = isSvgContent ? _buildBase64Svg() : _buildBase64Image();
|
2025-08-21 12:49:41 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-05 21:05:58 +05:30
|
|
|
// Always show the image without fade transitions during streaming to prevent black display
|
|
|
|
|
// The AutomaticKeepAliveClientMixin and global caching should preserve the image state
|
2025-08-21 12:49:41 +05:30
|
|
|
return imageWidget;
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildLoadingState() {
|
2025-08-21 15:19:47 +05:30
|
|
|
final constraints =
|
|
|
|
|
widget.constraints ??
|
2025-08-21 12:49:41 +05:30
|
|
|
const BoxConstraints(
|
|
|
|
|
maxWidth: 300,
|
|
|
|
|
maxHeight: 300,
|
|
|
|
|
minHeight: 150,
|
|
|
|
|
minWidth: 200,
|
|
|
|
|
);
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
return Container(
|
2025-08-21 12:49:41 +05:30
|
|
|
key: const ValueKey('loading'),
|
|
|
|
|
constraints: constraints,
|
2025-08-20 18:39:30 +05:30
|
|
|
margin: const EdgeInsets.only(bottom: Spacing.xs),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: context.conduitTheme.surfaceBackground.withValues(alpha: 0.5),
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: context.conduitTheme.dividerColor.withValues(alpha: 0.3),
|
|
|
|
|
width: BorderWidth.thin,
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 12:49:41 +05:30
|
|
|
child: Stack(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
// Shimmer effect placeholder
|
|
|
|
|
Container(
|
2025-08-21 15:19:47 +05:30
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
colors: [
|
|
|
|
|
context.conduitTheme.shimmerBase,
|
|
|
|
|
context.conduitTheme.shimmerHighlight,
|
|
|
|
|
context.conduitTheme.shimmerBase,
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.animate(onPlay: (controller) => controller.repeat())
|
|
|
|
|
.shimmer(
|
|
|
|
|
duration: const Duration(milliseconds: 1500),
|
|
|
|
|
color: context.conduitTheme.shimmerHighlight.withValues(
|
|
|
|
|
alpha: 0.3,
|
|
|
|
|
),
|
2025-08-21 12:49:41 +05:30
|
|
|
),
|
|
|
|
|
// Progress indicator overlay
|
|
|
|
|
CircularProgressIndicator(
|
|
|
|
|
color: context.conduitTheme.buttonPrimary,
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildErrorState() {
|
|
|
|
|
return Container(
|
2025-08-21 12:49:41 +05:30
|
|
|
key: const ValueKey('error'),
|
2025-08-21 15:19:47 +05:30
|
|
|
constraints:
|
|
|
|
|
widget.constraints ??
|
2025-08-20 18:39:30 +05:30
|
|
|
const BoxConstraints(
|
|
|
|
|
maxWidth: 300,
|
|
|
|
|
maxHeight: 150,
|
|
|
|
|
minHeight: 100,
|
|
|
|
|
minWidth: 200,
|
|
|
|
|
),
|
|
|
|
|
margin: const EdgeInsets.only(bottom: Spacing.xs),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: context.conduitTheme.surfaceBackground.withValues(alpha: 0.3),
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
border: Border.all(
|
|
|
|
|
color: context.conduitTheme.error.withValues(alpha: 0.3),
|
|
|
|
|
width: BorderWidth.thin,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.broken_image_outlined,
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
size: 32,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: Spacing.xs),
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.sm),
|
|
|
|
|
child: Text(
|
|
|
|
|
_errorMessage!,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
fontSize: AppTypography.bodySmall,
|
|
|
|
|
),
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-21 15:19:47 +05:30
|
|
|
).animate().fadeIn(duration: const Duration(milliseconds: 200));
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildNetworkImage() {
|
2025-08-20 23:42:31 +05:30
|
|
|
// Get authentication headers if available
|
2025-10-23 17:36:31 +05:30
|
|
|
final headers = buildImageHeadersFromWidgetRef(ref);
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-11-01 15:42:08 +05:30
|
|
|
final cacheManager = ref.watch(selfSignedImageCacheManagerProvider);
|
2025-08-20 18:39:30 +05:30
|
|
|
final imageWidget = CachedNetworkImage(
|
2025-08-21 12:49:41 +05:30
|
|
|
key: ValueKey('image_${widget.attachmentId}'),
|
2025-08-20 18:39:30 +05:30
|
|
|
imageUrl: _cachedImageData!,
|
|
|
|
|
fit: BoxFit.cover,
|
2025-10-23 17:36:31 +05:30
|
|
|
cacheManager: cacheManager,
|
|
|
|
|
httpHeaders: headers,
|
2025-10-19 14:18:26 +05:30
|
|
|
fadeInDuration: widget.disableAnimation
|
|
|
|
|
? Duration.zero
|
|
|
|
|
: const Duration(milliseconds: 200),
|
|
|
|
|
fadeOutDuration: widget.disableAnimation
|
|
|
|
|
? Duration.zero
|
|
|
|
|
: const Duration(milliseconds: 200),
|
2025-08-21 12:49:41 +05:30
|
|
|
placeholder: (context, url) => Container(
|
|
|
|
|
constraints: widget.constraints,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: context.conduitTheme.shimmerBase,
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-20 18:39:30 +05:30
|
|
|
errorWidget: (context, url, error) {
|
|
|
|
|
_errorMessage = error.toString();
|
|
|
|
|
return _buildErrorState();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return _wrapImage(imageWidget);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 11:04:59 +05:30
|
|
|
Widget _buildNetworkSvg() {
|
|
|
|
|
// Get authentication headers if available
|
|
|
|
|
final headers = buildImageHeadersFromWidgetRef(ref);
|
|
|
|
|
|
|
|
|
|
final svgWidget = SvgPicture.network(
|
|
|
|
|
_cachedImageData!,
|
|
|
|
|
key: ValueKey('svg_${widget.attachmentId}'),
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
headers: headers,
|
|
|
|
|
placeholderBuilder: (context) => Container(
|
|
|
|
|
constraints: widget.constraints,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: context.conduitTheme.shimmerBase,
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
color: context.conduitTheme.buttonPrimary,
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
_errorMessage = AppLocalizations.of(
|
|
|
|
|
context,
|
|
|
|
|
)!.failedToLoadImage(error.toString());
|
|
|
|
|
return _buildErrorState();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return _wrapImage(svgWidget);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
Widget _buildBase64Image() {
|
2025-10-19 14:18:26 +05:30
|
|
|
final bytes = _cachedBytes;
|
|
|
|
|
if (bytes == null) {
|
|
|
|
|
return _buildLoadingState();
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
2025-10-19 14:18:26 +05:30
|
|
|
|
|
|
|
|
final imageWidget = Image.memory(
|
|
|
|
|
key: ValueKey('image_${widget.attachmentId}'),
|
|
|
|
|
bytes,
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
gaplessPlayback: true, // Prevents flashing during rebuilds
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
_errorMessage = AppLocalizations.of(context)!.failedToDecodeImage;
|
|
|
|
|
return _buildErrorState();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return _wrapImage(imageWidget);
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
2025-11-29 11:04:59 +05:30
|
|
|
Widget _buildBase64Svg() {
|
|
|
|
|
final bytes = _cachedBytes;
|
|
|
|
|
if (bytes == null) {
|
|
|
|
|
return _buildLoadingState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final svgWidget = SvgPicture.memory(
|
|
|
|
|
bytes,
|
|
|
|
|
key: ValueKey('svg_${widget.attachmentId}'),
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
_errorMessage = AppLocalizations.of(context)!.failedToDecodeImage;
|
|
|
|
|
return _buildErrorState();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return _wrapImage(svgWidget);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
Widget _wrapImage(Widget imageWidget) {
|
2025-08-21 12:49:41 +05:30
|
|
|
final wrappedImage = Container(
|
2025-08-21 15:19:47 +05:30
|
|
|
constraints:
|
|
|
|
|
widget.constraints ??
|
|
|
|
|
const BoxConstraints(maxWidth: 400, maxHeight: 400),
|
2025-08-20 18:39:30 +05:30
|
|
|
margin: widget.isMarkdownFormat
|
|
|
|
|
? const EdgeInsets.symmetric(vertical: Spacing.sm)
|
|
|
|
|
: EdgeInsets.zero,
|
2025-08-21 12:49:41 +05:30
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
// Add subtle shadow for depth
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: context.conduitTheme.cardShadow.withValues(alpha: 0.1),
|
|
|
|
|
blurRadius: 8,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
child: Material(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
onTap: widget.onTap ?? () => _showFullScreenImage(context),
|
|
|
|
|
child: Hero(
|
2025-10-19 14:18:26 +05:30
|
|
|
tag: _heroTag,
|
2025-08-21 15:19:47 +05:30
|
|
|
flightShuttleBuilder:
|
|
|
|
|
(
|
|
|
|
|
flightContext,
|
|
|
|
|
animation,
|
|
|
|
|
flightDirection,
|
|
|
|
|
fromHeroContext,
|
|
|
|
|
toHeroContext,
|
|
|
|
|
) {
|
|
|
|
|
final hero = flightDirection == HeroFlightDirection.push
|
|
|
|
|
? fromHeroContext.widget as Hero
|
|
|
|
|
: toHeroContext.widget as Hero;
|
|
|
|
|
return FadeTransition(
|
|
|
|
|
opacity: animation,
|
|
|
|
|
child: hero.child,
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-08-21 12:49:41 +05:30
|
|
|
child: imageWidget,
|
|
|
|
|
),
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-08-21 12:49:41 +05:30
|
|
|
|
|
|
|
|
return wrappedImage;
|
2025-08-20 18:39:30 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _showFullScreenImage(BuildContext context) {
|
|
|
|
|
Navigator.of(context).push(
|
|
|
|
|
MaterialPageRoute(
|
|
|
|
|
fullscreenDialog: true,
|
2025-11-29 11:04:59 +05:30
|
|
|
builder: (context) => FullScreenImageViewer(
|
|
|
|
|
imageData: _cachedImageData!,
|
|
|
|
|
tag: _heroTag,
|
|
|
|
|
isSvg: _isSvg,
|
|
|
|
|
),
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 23:42:31 +05:30
|
|
|
class FullScreenImageViewer extends ConsumerWidget {
|
2025-08-20 18:39:30 +05:30
|
|
|
final String imageData;
|
|
|
|
|
final String tag;
|
2025-11-29 11:04:59 +05:30
|
|
|
final bool isSvg;
|
2025-08-20 18:39:30 +05:30
|
|
|
|
|
|
|
|
const FullScreenImageViewer({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.imageData,
|
|
|
|
|
required this.tag,
|
2025-11-29 11:04:59 +05:30
|
|
|
this.isSvg = false,
|
2025-08-20 18:39:30 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
2025-08-20 23:42:31 +05:30
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2025-08-20 18:39:30 +05:30
|
|
|
Widget imageWidget;
|
|
|
|
|
|
|
|
|
|
if (imageData.startsWith('http')) {
|
2025-08-20 23:42:31 +05:30
|
|
|
// Get authentication headers if available
|
2025-10-23 17:36:31 +05:30
|
|
|
final headers = buildImageHeadersFromWidgetRef(ref);
|
2025-08-21 15:19:47 +05:30
|
|
|
|
2025-11-29 11:04:59 +05:30
|
|
|
if (isSvg || _isSvgUrl(imageData)) {
|
|
|
|
|
imageWidget = SvgPicture.network(
|
|
|
|
|
imageData,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
headers: headers,
|
|
|
|
|
placeholderBuilder: (context) => Center(
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
color: context.conduitTheme.buttonPrimary,
|
|
|
|
|
),
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
2025-11-29 11:04:59 +05:30
|
|
|
errorBuilder: (context, error, stackTrace) => Center(
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.error_outline,
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
size: 48,
|
|
|
|
|
),
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
2025-11-29 11:04:59 +05:30
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
final cacheManager = ref.watch(selfSignedImageCacheManagerProvider);
|
|
|
|
|
imageWidget = CachedNetworkImage(
|
|
|
|
|
imageUrl: imageData,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
cacheManager: cacheManager,
|
|
|
|
|
httpHeaders: headers,
|
|
|
|
|
placeholder: (context, url) => Center(
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
color: context.conduitTheme.buttonPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
errorWidget: (context, url, error) => Center(
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.error_outline,
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
size: 48,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
String actualBase64;
|
|
|
|
|
if (imageData.startsWith('data:')) {
|
|
|
|
|
final commaIndex = imageData.indexOf(',');
|
2025-11-29 11:04:59 +05:30
|
|
|
if (commaIndex == -1) {
|
|
|
|
|
throw const FormatException('Invalid data URI');
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
actualBase64 = imageData.substring(commaIndex + 1);
|
|
|
|
|
} else {
|
|
|
|
|
actualBase64 = imageData;
|
|
|
|
|
}
|
|
|
|
|
final imageBytes = base64.decode(actualBase64);
|
2025-11-29 11:04:59 +05:30
|
|
|
|
|
|
|
|
// Check if SVG content
|
|
|
|
|
if (isSvg || _isSvgDataUrl(imageData) || _isSvgBytes(imageBytes)) {
|
|
|
|
|
imageWidget = SvgPicture.memory(
|
|
|
|
|
imageBytes,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
errorBuilder: (context, error, stackTrace) => Center(
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.error_outline,
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
size: 48,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
imageWidget = Image.memory(imageBytes, fit: BoxFit.contain);
|
|
|
|
|
}
|
2025-08-20 18:39:30 +05:30
|
|
|
} catch (e) {
|
|
|
|
|
imageWidget = Center(
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.error_outline,
|
|
|
|
|
color: context.conduitTheme.error,
|
|
|
|
|
size: 48,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 00:12:25 +05:30
|
|
|
final tokens = context.colorTokens;
|
|
|
|
|
final background = tokens.neutralTone10;
|
|
|
|
|
final iconColor = tokens.neutralOnSurface;
|
|
|
|
|
|
2025-08-20 18:39:30 +05:30
|
|
|
return Scaffold(
|
2025-10-03 00:12:25 +05:30
|
|
|
backgroundColor: background,
|
2025-08-20 18:39:30 +05:30
|
|
|
body: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
Center(
|
|
|
|
|
child: Hero(
|
|
|
|
|
tag: tag,
|
|
|
|
|
child: InteractiveViewer(
|
|
|
|
|
minScale: 0.5,
|
|
|
|
|
maxScale: 5.0,
|
|
|
|
|
child: imageWidget,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Positioned(
|
|
|
|
|
top: MediaQuery.of(context).padding.top + 16,
|
|
|
|
|
right: 16,
|
2025-08-21 15:19:47 +05:30
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: Icon(
|
|
|
|
|
Platform.isIOS ? Icons.ios_share : Icons.share_outlined,
|
2025-10-03 00:12:25 +05:30
|
|
|
color: iconColor,
|
2025-08-21 15:19:47 +05:30
|
|
|
size: 26,
|
|
|
|
|
),
|
|
|
|
|
onPressed: () => _shareImage(context, ref),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
IconButton(
|
2025-10-03 00:12:25 +05:30
|
|
|
icon: Icon(Icons.close, color: iconColor, size: 28),
|
2025-08-21 15:19:47 +05:30
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-08-20 18:39:30 +05:30
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-21 15:19:47 +05:30
|
|
|
|
|
|
|
|
Future<void> _shareImage(BuildContext context, WidgetRef ref) async {
|
2025-08-23 20:09:43 +05:30
|
|
|
final l10n = AppLocalizations.of(context)!;
|
2025-08-21 15:19:47 +05:30
|
|
|
try {
|
|
|
|
|
Uint8List bytes;
|
|
|
|
|
String? fileExtension;
|
|
|
|
|
|
|
|
|
|
if (imageData.startsWith('http')) {
|
|
|
|
|
final api = ref.read(apiServiceProvider);
|
|
|
|
|
final authToken = ref.read(authTokenProvider3);
|
|
|
|
|
final headers = <String, String>{};
|
|
|
|
|
|
|
|
|
|
if (authToken != null && authToken.isNotEmpty) {
|
|
|
|
|
headers['Authorization'] = 'Bearer $authToken';
|
|
|
|
|
} else if (api?.serverConfig.apiKey != null &&
|
|
|
|
|
api!.serverConfig.apiKey!.isNotEmpty) {
|
|
|
|
|
headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}';
|
|
|
|
|
}
|
|
|
|
|
if (api != null && api.serverConfig.customHeaders.isNotEmpty) {
|
|
|
|
|
headers.addAll(api.serverConfig.customHeaders);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final client = api?.dio ?? dio.Dio();
|
|
|
|
|
final response = await client.get<List<int>>(
|
|
|
|
|
imageData,
|
|
|
|
|
options: dio.Options(
|
|
|
|
|
responseType: dio.ResponseType.bytes,
|
|
|
|
|
headers: headers.isNotEmpty ? headers : null,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
final data = response.data;
|
|
|
|
|
if (data == null || data.isEmpty) {
|
2025-08-23 20:09:43 +05:30
|
|
|
throw Exception(l10n.emptyImageData);
|
2025-08-21 15:19:47 +05:30
|
|
|
}
|
|
|
|
|
bytes = Uint8List.fromList(data);
|
|
|
|
|
|
|
|
|
|
final contentType = response.headers.map['content-type']?.first;
|
|
|
|
|
if (contentType != null && contentType.startsWith('image/')) {
|
|
|
|
|
fileExtension = contentType.split('/').last;
|
|
|
|
|
if (fileExtension == 'jpeg') fileExtension = 'jpg';
|
|
|
|
|
} else {
|
|
|
|
|
final uri = Uri.tryParse(imageData);
|
|
|
|
|
final lastSegment = uri?.pathSegments.isNotEmpty == true
|
|
|
|
|
? uri!.pathSegments.last
|
|
|
|
|
: '';
|
|
|
|
|
final dotIndex = lastSegment.lastIndexOf('.');
|
|
|
|
|
if (dotIndex != -1 && dotIndex < lastSegment.length - 1) {
|
|
|
|
|
final ext = lastSegment.substring(dotIndex + 1).toLowerCase();
|
|
|
|
|
if (ext.length <= 5) {
|
|
|
|
|
fileExtension = ext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
String actualBase64 = imageData;
|
|
|
|
|
if (imageData.startsWith('data:')) {
|
|
|
|
|
final commaIndex = imageData.indexOf(',');
|
|
|
|
|
final meta = imageData.substring(5, commaIndex); // image/png;base64
|
|
|
|
|
final slashIdx = meta.indexOf('/');
|
|
|
|
|
final semicolonIdx = meta.indexOf(';');
|
|
|
|
|
if (slashIdx != -1 && semicolonIdx != -1 && slashIdx < semicolonIdx) {
|
|
|
|
|
final subtype = meta.substring(slashIdx + 1, semicolonIdx);
|
|
|
|
|
fileExtension = subtype == 'jpeg' ? 'jpg' : subtype;
|
|
|
|
|
}
|
|
|
|
|
actualBase64 = imageData.substring(commaIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
bytes = base64.decode(actualBase64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileExtension ??= 'png';
|
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
|
|
|
final filePath =
|
|
|
|
|
'${tempDir.path}/conduit_shared_${DateTime.now().millisecondsSinceEpoch}.$fileExtension';
|
|
|
|
|
final file = File(filePath);
|
|
|
|
|
await file.writeAsBytes(bytes);
|
|
|
|
|
|
|
|
|
|
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
|
|
|
|
} catch (e) {
|
2025-08-21 16:15:27 +05:30
|
|
|
// Swallowing UI feedback per requirements; keep a log for debugging
|
2025-09-25 23:22:48 +05:30
|
|
|
DebugLogger.log(
|
|
|
|
|
'Failed to share image: $e',
|
|
|
|
|
scope: 'chat/image-attachment',
|
|
|
|
|
);
|
2025-08-21 15:19:47 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|