import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/url_helper.dart'; class SharedLinkItem extends ConsumerWidget { final SharedLink sharedLink; const SharedLinkItem(this.sharedLink, {super.key}); bool isExpired() { if (sharedLink.expiresAt != null) { return DateTime.now().isAfter(sharedLink.expiresAt!); } return false; } Widget getExpiryDuration(bool isDarkMode) { var expiresText = "shared_link_expires_never".tr(); if (sharedLink.expiresAt != null) { if (isExpired()) { return Text( "shared_link_expired", style: TextStyle(color: Colors.red[300]), ).tr(); } final difference = sharedLink.expiresAt!.difference(DateTime.now()); debugPrint("Difference: $difference"); if (difference.inDays > 0) { var dayDifference = difference.inDays; if (difference.inHours % 24 > 12) { dayDifference += 1; } expiresText = "shared_link_expires_days".plural(dayDifference); } else if (difference.inHours > 0) { expiresText = "shared_link_expires_hours".plural(difference.inHours); } else if (difference.inMinutes > 0) { expiresText = "shared_link_expires_minutes".plural(difference.inMinutes); } else if (difference.inSeconds > 0) { expiresText = "shared_link_expires_seconds".plural(difference.inSeconds); } } return Text( expiresText, style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), ); } @override Widget build(BuildContext context, WidgetRef ref) { final themeData = context.themeData; final isDarkMode = themeData.brightness == Brightness.dark; final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; final imageSize = math.min(context.width / 4, 100.0); void copyShareLinkToClipboard() { final serverUrl = getServerUrl(); if (serverUrl == null) { ImmichToast.show( context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, msg: "shared_link_error_server_url_fetch".tr(), ); return; } Clipboard.setData( ClipboardData( text: "$serverUrl/share/${sharedLink.key}", ), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "shared_link_clipboard_copied_massage", style: context.textTheme.bodyLarge?.copyWith( color: context.primaryColor, ), ).tr(), duration: const Duration(seconds: 2), ), ); }); } Future deleteShareLink() async { return showDialog( context: context, builder: (BuildContext context) { return ConfirmDialog( title: "delete_shared_link_dialog_title", content: "delete_shared_link_dialog_content", onOk: () => ref .read(sharedLinksStateProvider.notifier) .deleteLink(sharedLink.id), ); }, ); } Widget buildThumbnail() { if (thumbnailUrl == null) { return Container( height: imageSize * 1.2, width: imageSize, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], ), child: Center( child: Icon( Icons.image_not_supported_outlined, color: isDarkMode ? Colors.grey[100] : Colors.grey[700], ), ), ); } return SizedBox( height: imageSize * 1.2, width: imageSize, child: Padding( padding: const EdgeInsets.only(right: 4.0), child: ThumbnailWithInfo( imageUrl: thumbnailUrl, key: key, textInfo: '', noImageIcon: Icons.image_not_supported_outlined, onTap: () {}, ), ), ); } Widget buildInfoChip(String labelText) { return Padding( padding: const EdgeInsets.only(right: 10), child: Chip( backgroundColor: themeData.primaryColor, label: Text( labelText, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: isDarkMode ? Colors.black : Colors.white, ), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(25)), ), ), ); } Widget buildBottomInfo() { return Row( children: [ if (sharedLink.allowUpload) buildInfoChip("shared_link_info_chip_upload".tr()), if (sharedLink.allowDownload) buildInfoChip("shared_link_info_chip_download".tr()), if (sharedLink.showMetadata) buildInfoChip("shared_link_info_chip_metadata".tr()), ], ); } Widget buildSharedLinkActions() { const actionIconSize = 20.0; return Row( children: [ IconButton( splashRadius: 25, constraints: const BoxConstraints(), iconSize: actionIconSize, icon: const Icon(Icons.delete_outline), style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), onPressed: deleteShareLink, ), IconButton( splashRadius: 25, constraints: const BoxConstraints(), iconSize: actionIconSize, icon: const Icon(Icons.edit_outlined), style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), onPressed: () => context.autoPush(SharedLinkEditRoute(existingLink: sharedLink)), ), IconButton( splashRadius: 25, constraints: const BoxConstraints(), iconSize: actionIconSize, icon: const Icon(Icons.copy_outlined), style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), onPressed: copyShareLinkToClipboard, ), ], ); } Widget buildSharedLinkDetails() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ getExpiryDuration(isDarkMode), Padding( padding: const EdgeInsets.only(top: 5), child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( color: themeData.primaryColor.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( color: isDarkMode ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), message: sharedLink.title, preferBelow: false, triggerMode: TooltipTriggerMode.tap, child: Text( sharedLink.title, style: TextStyle( color: themeData.primaryColor, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), ), ), ), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( color: themeData.primaryColor.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( color: isDarkMode ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), message: sharedLink.description ?? "", preferBelow: false, triggerMode: TooltipTriggerMode.tap, child: Text( sharedLink.description ?? "", overflow: TextOverflow.ellipsis, ), ), ), Padding( padding: const EdgeInsets.only(right: 15), child: buildSharedLinkActions(), ), ], ), buildBottomInfo(), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 15), child: buildThumbnail(), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 15), child: buildSharedLinkDetails(), ), ), ], ), const Padding( padding: EdgeInsets.all(20), child: Divider( height: 0, ), ), ], ); } }