immich/mobile/lib/modules/shared_link/ui/shared_link_item.dart
shenlong 8dcc01b2be
feat(mobile): shared-links (#4490)
* add shared links page

* feat(mobile): shared link items

* feat(mobile): create / edit shared links page

* server: add changeExpiryTime to SharedLinkEditDto

* fix(mobile): edit expiry to never

* mobile: add icon when shares list is empty

* mobile: create new share from album / timeline

* mobile: add translation texts

* mobile: minor ui fixes

* fix: handle serverURL with /api path

* mobile: show share link on successful creation

* mobile: shared links list - 2 column layout

* mobile: use sharedlink pod class instead of dto

* mobile: show error on link creation

* mobile: show share icon only when remote assets are in selection

* mobile: use server endpoint instead of server url

* styling

* styling

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-22 15:05:10 +00:00

308 lines
9.6 KiB
Dart

import 'dart:math' as math;
import 'package:auto_route/auto_route.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/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 = "Expires ∞";
if (sharedLink.expiresAt != null) {
if (isExpired()) {
return Text(
"Expired",
style: TextStyle(color: Colors.red[300]),
);
}
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 = "in $dayDifference days";
} else if (difference.inHours > 0) {
expiresText = "in ${difference.inHours} hours";
} else if (difference.inMinutes > 0) {
expiresText = "in ${difference.inMinutes} minutes";
} else if (difference.inSeconds > 0) {
expiresText = "in ${difference.inSeconds} seconds";
}
}
return Text(
expiresText,
style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeData = Theme.of(context);
final isDarkMode = themeData.brightness == Brightness.dark;
final thumbnailUrl = sharedLink.thumbAssetId != null
? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!)
: null;
final imageSize = math.min(MediaQuery.of(context).size.width / 4, 100.0);
void copyShareLinkToClipboard() {
final serverUrl = getServerUrl();
if (serverUrl == null) {
ImmichToast.show(
context: context,
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
msg: 'Cannot fetch the server url',
);
return;
}
Clipboard.setData(
ClipboardData(
text: "$serverUrl/share/${sharedLink.key}",
),
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Copied to clipboard",
),
duration: Duration(seconds: 2),
),
);
});
}
Future<void> 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: 10,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.black : Colors.white,
),
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
),
),
);
}
Widget buildBottomInfo() {
return Row(
children: [
if (sharedLink.allowUpload) buildInfoChip("Upload"),
if (sharedLink.allowDownload) buildInfoChip("Download"),
if (sharedLink.showMetadata) buildInfoChip("EXIF"),
],
);
}
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: () => AutoRouter.of(context)
.push(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,
),
),
],
);
}
}