Merge pull request #196 from cogwheel0/svg-image-attachment-support

svg-image-attachment-support
This commit is contained in:
cogwheel
2025-11-29 12:11:12 +05:30
committed by GitHub
7 changed files with 280 additions and 28 deletions

View File

@@ -272,6 +272,10 @@ Map<String, dynamic> _parseOpenWebUIMessageToJson(
}; };
if (entry['name'] != null) fileMap['name'] = entry['name']; if (entry['name'] != null) fileMap['name'] = entry['name'];
if (entry['size'] != null) fileMap['size'] = entry['size']; if (entry['size'] != null) fileMap['size'] = entry['size'];
final headers = _coerceStringMap(entry['headers']);
if (headers != null && headers.isNotEmpty) {
fileMap['headers'] = headers;
}
allFiles.add(fileMap); allFiles.add(fileMap);
final url = entry['url'].toString(); final url = entry['url'].toString();
@@ -388,6 +392,24 @@ List<Map<String, dynamic>> _parseStatusHistoryField(dynamic raw) {
return const <Map<String, dynamic>>[]; return const <Map<String, dynamic>>[];
} }
Map<String, String>? _coerceStringMap(dynamic raw) {
if (raw is Map) {
final result = <String, String>{};
raw.forEach((key, value) {
final keyString = key?.toString();
final valueString = value?.toString();
if (keyString != null &&
keyString.isNotEmpty &&
valueString != null &&
valueString.isNotEmpty) {
result[keyString] = valueString;
}
});
return result.isEmpty ? null : result;
}
return null;
}
List<String> _coerceStringList(dynamic raw) { List<String> _coerceStringList(dynamic raw) {
if (raw is List) { if (raw is List) {
return raw return raw

View File

@@ -981,8 +981,8 @@ Future<String?> _getFileAsBase64(dynamic api, String fileId) async {
final ext = fileName.toLowerCase().split('.').last; final ext = fileName.toLowerCase().split('.').last;
// Only process image files // Only process image files (including SVG)
if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) { if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].contains(ext)) {
return null; return null;
} }
@@ -1039,6 +1039,8 @@ Future<Map<String, dynamic>> _buildMessagePayloadWithAttachments({
mimeType = 'image/gif'; mimeType = 'image/gif';
} else if (ext == 'webp') { } else if (ext == 'webp') {
mimeType = 'image/webp'; mimeType = 'image/webp';
} else if (ext == 'svg') {
mimeType = 'image/svg+xml';
} }
final dataUrl = 'data:$mimeType;base64,$base64Data'; final dataUrl = 'data:$mimeType;base64,$base64Data';

View File

@@ -1063,6 +1063,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
), ),
disableAnimation: disableAnimation:
false, // Keep animations enabled to prevent black display false, // Keep animations enabled to prevent black display
httpHeaders: _headersForFile(imageFiles[0]),
); );
}, },
), ),
@@ -1087,12 +1088,31 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
), ),
disableAnimation: disableAnimation:
false, // Keep animations enabled to prevent black display false, // Keep animations enabled to prevent black display
httpHeaders: _headersForFile(file),
); );
}).toList(), }).toList(),
), ),
); );
} }
Map<String, String>? _headersForFile(dynamic file) {
if (file is! Map) return null;
final rawHeaders = file['headers'];
if (rawHeaders is! Map) return null;
final result = <String, String>{};
rawHeaders.forEach((key, value) {
final keyString = key?.toString();
final valueString = value?.toString();
if (keyString != null &&
keyString.isNotEmpty &&
valueString != null &&
valueString.isNotEmpty) {
result[keyString] = valueString;
}
});
return result.isEmpty ? null : result;
}
Widget _buildNonImageFiles(List<dynamic> nonImageFiles) { Widget _buildNonImageFiles(List<dynamic> nonImageFiles) {
return Wrap( return Wrap(
spacing: Spacing.sm, spacing: Spacing.sm,

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:dio/dio.dart' as dio; import 'package:dio/dio.dart' as dio;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@@ -22,6 +23,7 @@ final _globalImageCache = <String, String>{};
final _globalLoadingStates = <String, bool>{}; final _globalLoadingStates = <String, bool>{};
final _globalErrorStates = <String, String>{}; final _globalErrorStates = <String, String>{};
final _globalImageBytesCache = <String, Uint8List>{}; final _globalImageBytesCache = <String, Uint8List>{};
final _globalSvgStates = <String, bool>{};
final _base64WhitespacePattern = RegExp(r'\s'); final _base64WhitespacePattern = RegExp(r'\s');
Uint8List _decodeImageData(String data) { Uint8List _decodeImageData(String data) {
@@ -37,6 +39,55 @@ Uint8List _decodeImageData(String data) {
return base64.decode(payload); return base64.decode(payload);
} }
/// 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');
}
Map<String, String>? _mergeHeaders(
Map<String, String>? defaults,
Map<String, String>? overrides,
) {
if ((defaults == null || defaults.isEmpty) &&
(overrides == null || overrides.isEmpty)) {
return null;
}
return {...?defaults, ...?overrides};
}
class EnhancedImageAttachment extends ConsumerStatefulWidget { class EnhancedImageAttachment extends ConsumerStatefulWidget {
final String attachmentId; final String attachmentId;
final bool isMarkdownFormat; final bool isMarkdownFormat;
@@ -44,6 +95,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget {
final BoxConstraints? constraints; final BoxConstraints? constraints;
final bool isUserMessage; final bool isUserMessage;
final bool disableAnimation; final bool disableAnimation;
final Map<String, String>? httpHeaders;
const EnhancedImageAttachment({ const EnhancedImageAttachment({
super.key, super.key,
@@ -53,6 +105,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget {
this.constraints, this.constraints,
this.isUserMessage = false, this.isUserMessage = false,
this.disableAnimation = false, this.disableAnimation = false,
this.httpHeaders,
}); });
@override @override
@@ -68,6 +121,7 @@ class _EnhancedImageAttachmentState
bool _isLoading = true; bool _isLoading = true;
String? _errorMessage; String? _errorMessage;
bool _isDecoding = false; bool _isDecoding = false;
bool _isSvg = false;
late final String _heroTag; late final String _heroTag;
// Removed unused animation and state flags // Removed unused animation and state flags
@@ -107,10 +161,12 @@ class _EnhancedImageAttachmentState
if (_globalImageCache.containsKey(widget.attachmentId)) { if (_globalImageCache.containsKey(widget.attachmentId)) {
final cachedData = _globalImageCache[widget.attachmentId]!; final cachedData = _globalImageCache[widget.attachmentId]!;
final cachedBytes = _globalImageBytesCache[widget.attachmentId]; final cachedBytes = _globalImageBytesCache[widget.attachmentId];
final cachedIsSvg = _globalSvgStates[widget.attachmentId] ?? false;
if (mounted) { if (mounted) {
setState(() { setState(() {
_cachedImageData = cachedData; _cachedImageData = cachedData;
_cachedBytes = cachedBytes; _cachedBytes = cachedBytes;
_isSvg = cachedIsSvg;
_isLoading = cachedBytes == null && !_isRemoteContent(cachedData); _isLoading = cachedBytes == null && !_isRemoteContent(cachedData);
}); });
} }
@@ -129,13 +185,18 @@ class _EnhancedImageAttachmentState
final attachmentId = widget.attachmentId; final attachmentId = widget.attachmentId;
if (attachmentId.startsWith('data:') || attachmentId.startsWith('http')) { if (attachmentId.startsWith('data:') || attachmentId.startsWith('http')) {
// Detect SVG from data URL or HTTP URL
final isSvgContent =
_isSvgDataUrl(attachmentId) || _isSvgUrl(attachmentId);
_globalImageCache[attachmentId] = attachmentId; _globalImageCache[attachmentId] = attachmentId;
_globalLoadingStates[attachmentId] = false; _globalLoadingStates[attachmentId] = false;
_globalSvgStates[attachmentId] = isSvgContent;
final cachedBytes = _globalImageBytesCache[attachmentId]; final cachedBytes = _globalImageBytesCache[attachmentId];
if (mounted) { if (mounted) {
setState(() { setState(() {
_cachedImageData = attachmentId; _cachedImageData = attachmentId;
_cachedBytes = cachedBytes; _cachedBytes = cachedBytes;
_isSvg = isSvgContent;
_isLoading = cachedBytes == null && !_isRemoteContent(attachmentId); _isLoading = cachedBytes == null && !_isRemoteContent(attachmentId);
}); });
} }
@@ -149,12 +210,15 @@ class _EnhancedImageAttachmentState
final api = ref.read(apiServiceProvider); final api = ref.read(apiServiceProvider);
if (api != null) { if (api != null) {
final fullUrl = api.baseUrl + attachmentId; final fullUrl = api.baseUrl + attachmentId;
final isSvgContent = _isSvgUrl(fullUrl);
_globalImageCache[attachmentId] = fullUrl; _globalImageCache[attachmentId] = fullUrl;
_globalLoadingStates[attachmentId] = false; _globalLoadingStates[attachmentId] = false;
_globalSvgStates[attachmentId] = isSvgContent;
if (mounted) { if (mounted) {
setState(() { setState(() {
_cachedImageData = fullUrl; _cachedImageData = fullUrl;
_cachedBytes = null; _cachedBytes = null;
_isSvg = isSvgContent;
_isLoading = false; _isLoading = false;
}); });
} }
@@ -184,10 +248,14 @@ class _EnhancedImageAttachmentState
return; return;
} }
// Track if this is an SVG file based on extension
final isSvgFile = ext == 'svg';
final fileContent = await api.getFileContent(attachmentId); final fileContent = await api.getFileContent(attachmentId);
_globalImageCache[attachmentId] = fileContent; _globalImageCache[attachmentId] = fileContent;
_globalLoadingStates[attachmentId] = false; _globalLoadingStates[attachmentId] = false;
_globalSvgStates[attachmentId] = isSvgFile;
if (_globalImageCache.length > 50) { if (_globalImageCache.length > 50) {
final firstKey = _globalImageCache.keys.first; final firstKey = _globalImageCache.keys.first;
@@ -195,12 +263,14 @@ class _EnhancedImageAttachmentState
_globalLoadingStates.remove(firstKey); _globalLoadingStates.remove(firstKey);
_globalErrorStates.remove(firstKey); _globalErrorStates.remove(firstKey);
_globalImageBytesCache.remove(firstKey); _globalImageBytesCache.remove(firstKey);
_globalSvgStates.remove(firstKey);
} }
if (mounted) { if (mounted) {
setState(() { setState(() {
_cachedImageData = fileContent; _cachedImageData = fileContent;
_cachedBytes = null; _cachedBytes = null;
_isSvg = isSvgFile;
_isLoading = !_isRemoteContent(fileContent); _isLoading = !_isRemoteContent(fileContent);
}); });
} }
@@ -234,9 +304,18 @@ class _EnhancedImageAttachmentState
debugLabel: 'decode_image', debugLabel: 'decode_image',
); );
_globalImageBytesCache[widget.attachmentId] = bytes; _globalImageBytesCache[widget.attachmentId] = bytes;
// 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;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_cachedBytes = bytes; _cachedBytes = bytes;
_isSvg = isSvgContent;
_isLoading = false; _isLoading = false;
}); });
} on FormatException { } on FormatException {
@@ -255,6 +334,7 @@ class _EnhancedImageAttachmentState
_globalLoadingStates[widget.attachmentId] = false; _globalLoadingStates[widget.attachmentId] = false;
_globalImageCache.remove(widget.attachmentId); _globalImageCache.remove(widget.attachmentId);
_globalImageBytesCache.remove(widget.attachmentId); _globalImageBytesCache.remove(widget.attachmentId);
_globalSvgStates.remove(widget.attachmentId);
if (!mounted) { if (!mounted) {
return; return;
} }
@@ -297,11 +377,14 @@ class _EnhancedImageAttachmentState
} }
// Handle different image data formats // Handle different image data formats
// Include fallback URL/data detection to match FullScreenImageViewer behavior
Widget imageWidget; Widget imageWidget;
if (_cachedImageData!.startsWith('http')) { if (_cachedImageData!.startsWith('http')) {
imageWidget = _buildNetworkImage(); final isSvgContent = _isSvg || _isSvgUrl(_cachedImageData!);
imageWidget = isSvgContent ? _buildNetworkSvg() : _buildNetworkImage();
} else { } else {
imageWidget = _buildBase64Image(); final isSvgContent = _isSvg || _isSvgDataUrl(_cachedImageData!);
imageWidget = isSvgContent ? _buildBase64Svg() : _buildBase64Image();
} }
// Always show the image without fade transitions during streaming to prevent black display // Always show the image without fade transitions during streaming to prevent black display
@@ -415,7 +498,8 @@ class _EnhancedImageAttachmentState
Widget _buildNetworkImage() { Widget _buildNetworkImage() {
// Get authentication headers if available // Get authentication headers if available
final headers = buildImageHeadersFromWidgetRef(ref); final defaultHeaders = buildImageHeadersFromWidgetRef(ref);
final headers = _mergeHeaders(defaultHeaders, widget.httpHeaders);
final cacheManager = ref.watch(selfSignedImageCacheManagerProvider); final cacheManager = ref.watch(selfSignedImageCacheManagerProvider);
final imageWidget = CachedNetworkImage( final imageWidget = CachedNetworkImage(
@@ -446,6 +530,40 @@ class _EnhancedImageAttachmentState
return _wrapImage(imageWidget); return _wrapImage(imageWidget);
} }
Widget _buildNetworkSvg() {
// Get authentication headers if available
final defaultHeaders = buildImageHeadersFromWidgetRef(ref);
final headers = _mergeHeaders(defaultHeaders, widget.httpHeaders);
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);
}
Widget _buildBase64Image() { Widget _buildBase64Image() {
final bytes = _cachedBytes; final bytes = _cachedBytes;
if (bytes == null) { if (bytes == null) {
@@ -466,6 +584,25 @@ class _EnhancedImageAttachmentState
return _wrapImage(imageWidget); return _wrapImage(imageWidget);
} }
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);
}
Widget _wrapImage(Widget imageWidget) { Widget _wrapImage(Widget imageWidget) {
final wrappedImage = Container( final wrappedImage = Container(
constraints: constraints:
@@ -523,8 +660,12 @@ class _EnhancedImageAttachmentState
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => builder: (context) => FullScreenImageViewer(
FullScreenImageViewer(imageData: _cachedImageData!, tag: _heroTag), imageData: _cachedImageData!,
tag: _heroTag,
isSvg: _isSvg,
customHeaders: widget.httpHeaders,
),
), ),
); );
} }
@@ -533,11 +674,15 @@ class _EnhancedImageAttachmentState
class FullScreenImageViewer extends ConsumerWidget { class FullScreenImageViewer extends ConsumerWidget {
final String imageData; final String imageData;
final String tag; final String tag;
final bool isSvg;
final Map<String, String>? customHeaders;
const FullScreenImageViewer({ const FullScreenImageViewer({
super.key, super.key,
required this.imageData, required this.imageData,
required this.tag, required this.tag,
this.isSvg = false,
this.customHeaders,
}); });
@override @override
@@ -546,8 +691,28 @@ class FullScreenImageViewer extends ConsumerWidget {
if (imageData.startsWith('http')) { if (imageData.startsWith('http')) {
// Get authentication headers if available // Get authentication headers if available
final headers = buildImageHeadersFromWidgetRef(ref); final defaultHeaders = buildImageHeadersFromWidgetRef(ref);
final headers = _mergeHeaders(defaultHeaders, customHeaders);
if (isSvg || _isSvgUrl(imageData)) {
imageWidget = SvgPicture.network(
imageData,
fit: BoxFit.contain,
headers: headers,
placeholderBuilder: (context) => Center(
child: CircularProgressIndicator(
color: context.conduitTheme.buttonPrimary,
),
),
errorBuilder: (context, error, stackTrace) => Center(
child: Icon(
Icons.error_outline,
color: context.conduitTheme.error,
size: 48,
),
),
);
} else {
final cacheManager = ref.watch(selfSignedImageCacheManagerProvider); final cacheManager = ref.watch(selfSignedImageCacheManagerProvider);
imageWidget = CachedNetworkImage( imageWidget = CachedNetworkImage(
imageUrl: imageData, imageUrl: imageData,
@@ -567,17 +732,37 @@ class FullScreenImageViewer extends ConsumerWidget {
), ),
), ),
); );
}
} else { } else {
try { try {
String actualBase64; String actualBase64;
if (imageData.startsWith('data:')) { if (imageData.startsWith('data:')) {
final commaIndex = imageData.indexOf(','); final commaIndex = imageData.indexOf(',');
if (commaIndex == -1) {
throw const FormatException('Invalid data URI');
}
actualBase64 = imageData.substring(commaIndex + 1); actualBase64 = imageData.substring(commaIndex + 1);
} else { } else {
actualBase64 = imageData; actualBase64 = imageData;
} }
final imageBytes = base64.decode(actualBase64); final imageBytes = base64.decode(actualBase64);
// 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); imageWidget = Image.memory(imageBytes, fit: BoxFit.contain);
}
} catch (e) { } catch (e) {
imageWidget = Center( imageWidget = Center(
child: Icon( child: Icon(
@@ -654,13 +839,14 @@ class FullScreenImageViewer extends ConsumerWidget {
if (api != null && api.serverConfig.customHeaders.isNotEmpty) { if (api != null && api.serverConfig.customHeaders.isNotEmpty) {
headers.addAll(api.serverConfig.customHeaders); headers.addAll(api.serverConfig.customHeaders);
} }
final mergedHeaders = _mergeHeaders(headers, customHeaders);
final client = api?.dio ?? dio.Dio(); final client = api?.dio ?? dio.Dio();
final response = await client.get<List<int>>( final response = await client.get<List<int>>(
imageData, imageData,
options: dio.Options( options: dio.Options(
responseType: dio.ResponseType.bytes, responseType: dio.ResponseType.bytes,
headers: headers.isNotEmpty ? headers : null, headers: mergedHeaders,
), ),
); );
final data = response.data; final data = response.data;

View File

@@ -150,6 +150,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: 350, maxHeight: 350,
), ),
disableAnimation: widget.isStreaming, disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(imageFiles[0]),
), ),
), ),
), ),
@@ -191,6 +192,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: 180, maxHeight: 180,
), ),
disableAnimation: widget.isStreaming, disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(entry.value),
), ),
), ),
), ),
@@ -232,6 +234,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: imageCount == 3 ? 135 : 90, maxHeight: imageCount == 3 ? 135 : 90,
), ),
disableAnimation: widget.isStreaming, disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(file),
), ),
), ),
); );
@@ -401,6 +404,24 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
); );
} }
Map<String, String>? _headersForFile(dynamic file) {
if (file is! Map) return null;
final rawHeaders = file['headers'];
if (rawHeaders is! Map) return null;
final result = <String, String>{};
rawHeaders.forEach((key, value) {
final keyString = key?.toString();
final valueString = value?.toString();
if (keyString != null &&
keyString.isNotEmpty &&
valueString != null &&
valueString.isNotEmpty) {
result[keyString] = valueString;
}
});
return result.isEmpty ? null : result;
}
// Assistant-only helpers removed; this widget renders only user bubbles. // Assistant-only helpers removed; this widget renders only user bubbles.
@override @override

View File

@@ -652,7 +652,7 @@ packages:
source: hosted source: hosted
version: "0.1.3" version: "0.1.3"
flutter_svg: flutter_svg:
dependency: transitive dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"

View File

@@ -76,6 +76,7 @@ dependencies:
flutter_callkit_incoming: ^3.0.0 flutter_callkit_incoming: ^3.0.0
flutter_app_intents: ^0.7.0 flutter_app_intents: ^0.7.0
quick_actions: 1.1.0 quick_actions: 1.1.0
flutter_svg: ^2.2.3
# Clipboard functionality is available through flutter/services (part of Flutter SDK) # Clipboard functionality is available through flutter/services (part of Flutter SDK)