feat: add share image

This commit is contained in:
cogwheel0
2025-08-21 15:19:47 +05:30
parent c874031e9b
commit 05f0974a86
3 changed files with 186 additions and 69 deletions

View File

@@ -1,8 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:dio/dio.dart' as dio;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../core/providers/app_providers.dart';
import '../../auth/providers/unified_auth_providers.dart';
@@ -276,17 +281,15 @@ class _EnhancedImageAttachmentState
// Apply fade animation only when first showing content
if (!widget.disableAnimation && _hasShownContent) {
return FadeTransition(
opacity: _fadeAnimation,
child: imageWidget,
);
return FadeTransition(opacity: _fadeAnimation, child: imageWidget);
}
return imageWidget;
}
Widget _buildLoadingState() {
final constraints = widget.constraints ??
final constraints =
widget.constraints ??
const BoxConstraints(
maxWidth: 300,
maxHeight: 300,
@@ -311,24 +314,26 @@ class _EnhancedImageAttachmentState
children: [
// Shimmer effect placeholder
Container(
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,
],
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,
),
),
),
)
.animate(onPlay: (controller) => controller.repeat())
.shimmer(
duration: const Duration(milliseconds: 1500),
color: context.conduitTheme.shimmerHighlight.withValues(alpha: 0.3),
),
// Progress indicator overlay
CircularProgressIndicator(
color: context.conduitTheme.buttonPrimary,
@@ -342,7 +347,8 @@ class _EnhancedImageAttachmentState
Widget _buildErrorState() {
return Container(
key: const ValueKey('error'),
constraints: widget.constraints ??
constraints:
widget.constraints ??
const BoxConstraints(
maxWidth: 300,
maxHeight: 150,
@@ -382,9 +388,7 @@ class _EnhancedImageAttachmentState
),
],
),
)
.animate()
.fadeIn(duration: const Duration(milliseconds: 200));
).animate().fadeIn(duration: const Duration(milliseconds: 200));
}
Widget _buildNetworkImage() {
@@ -396,7 +400,8 @@ class _EnhancedImageAttachmentState
// Add auth token from unified auth provider
if (authToken != null && authToken.isNotEmpty) {
headers['Authorization'] = 'Bearer $authToken';
} else if (api?.serverConfig.apiKey != null && api!.serverConfig.apiKey!.isNotEmpty) {
} else if (api?.serverConfig.apiKey != null &&
api!.serverConfig.apiKey!.isNotEmpty) {
// Fallback to API key from server config
headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}';
}
@@ -465,11 +470,9 @@ class _EnhancedImageAttachmentState
Widget _wrapImage(Widget imageWidget) {
final wrappedImage = Container(
constraints: widget.constraints ??
const BoxConstraints(
maxWidth: 400,
maxHeight: 400,
),
constraints:
widget.constraints ??
const BoxConstraints(maxWidth: 400, maxHeight: 400),
margin: widget.isMarkdownFormat
? const EdgeInsets.symmetric(vertical: Spacing.sm)
: EdgeInsets.zero,
@@ -491,17 +494,24 @@ class _EnhancedImageAttachmentState
child: InkWell(
onTap: widget.onTap ?? () => _showFullScreenImage(context),
child: Hero(
tag: 'image_${widget.attachmentId}_${DateTime.now().millisecondsSinceEpoch}',
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,
);
},
tag:
'image_${widget.attachmentId}_${DateTime.now().millisecondsSinceEpoch}',
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,
);
},
child: imageWidget,
),
),
@@ -548,7 +558,8 @@ class FullScreenImageViewer extends ConsumerWidget {
// Add auth token from unified auth provider
if (authToken != null && authToken.isNotEmpty) {
headers['Authorization'] = 'Bearer $authToken';
} else if (api?.serverConfig.apiKey != null && api!.serverConfig.apiKey!.isNotEmpty) {
} else if (api?.serverConfig.apiKey != null &&
api!.serverConfig.apiKey!.isNotEmpty) {
// Fallback to API key from server config
headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}';
}
@@ -585,10 +596,7 @@ class FullScreenImageViewer extends ConsumerWidget {
actualBase64 = imageData;
}
final imageBytes = base64.decode(actualBase64);
imageWidget = Image.memory(
imageBytes,
fit: BoxFit.contain,
);
imageWidget = Image.memory(imageBytes, fit: BoxFit.contain);
} catch (e) {
imageWidget = Center(
child: Icon(
@@ -617,17 +625,109 @@ class FullScreenImageViewer extends ConsumerWidget {
Positioned(
top: MediaQuery.of(context).padding.top + 16,
right: 16,
child: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
size: 28,
),
onPressed: () => Navigator.of(context).pop(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Platform.isIOS ? Icons.ios_share : Icons.share_outlined,
color: Colors.white,
size: 26,
),
onPressed: () => _shareImage(context, ref),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 28),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
],
),
);
}
Future<void> _shareImage(BuildContext context, WidgetRef ref) async {
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) {
throw Exception('Empty image data');
}
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) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Failed to share image')));
}
}
}

View File

@@ -968,6 +968,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
url: "https://pub.dev"
source: hosted
version: "11.1.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
shared_preferences:
dependency: "direct main"
description:

View File

@@ -53,6 +53,7 @@ dependencies:
json_annotation: ^4.9.0
google_fonts: ^6.2.1
wakelock_plus: ^1.2.10
share_plus: ^11.1.0
# Clipboard functionality is available through flutter/services (part of Flutter SDK)