123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- import 'dart:convert';
- import 'package:collection/collection.dart';
- import "package:fast_base58/fast_base58.dart";
- import 'package:flutter/material.dart';
- import "package:flutter/services.dart";
- import "package:photos/generated/l10n.dart";
- import "package:photos/models/api/collection/public_url.dart";
- import 'package:photos/models/collection/collection.dart';
- import 'package:photos/services/collections_service.dart';
- import 'package:photos/theme/colors.dart';
- import 'package:photos/theme/ente_theme.dart';
- import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
- import 'package:photos/ui/components/captioned_text_widget.dart';
- import 'package:photos/ui/components/divider_widget.dart';
- import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
- import 'package:photos/ui/components/menu_section_description_widget.dart';
- import "package:photos/ui/components/toggle_switch_widget.dart";
- import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart';
- import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart';
- import 'package:photos/utils/crypto_util.dart';
- import 'package:photos/utils/date_time_util.dart';
- import 'package:photos/utils/dialog_util.dart';
- import 'package:photos/utils/navigation_util.dart';
- import "package:photos/utils/share_util.dart";
- import 'package:photos/utils/toast_util.dart';
- class ManageSharedLinkWidget extends StatefulWidget {
- final Collection? collection;
- const ManageSharedLinkWidget({Key? key, this.collection}) : super(key: key);
- @override
- State<ManageSharedLinkWidget> createState() => _ManageSharedLinkWidgetState();
- }
- class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
- final CollectionActions sharingActions =
- CollectionActions(CollectionsService.instance);
- @override
- void initState() {
- super.initState();
- }
- @override
- Widget build(BuildContext context) {
- final isCollectEnabled =
- widget.collection!.publicURLs?.firstOrNull?.enableCollect ?? false;
- final isDownloadEnabled =
- widget.collection!.publicURLs?.firstOrNull?.enableDownload ?? true;
- final isPasswordEnabled =
- widget.collection!.publicURLs?.firstOrNull?.passwordEnabled ?? false;
- final enteColorScheme = getEnteColorScheme(context);
- final PublicURL url = widget.collection!.publicURLs!.firstOrNull!;
- final String collectionKey = Base58Encode(
- CollectionsService.instance.getCollectionKey(widget.collection!.id),
- );
- final String urlValue = "${url.url}#$collectionKey";
- return Scaffold(
- appBar: AppBar(
- elevation: 0,
- title: Text(
- S.of(context).manageLink,
- ),
- ),
- body: SingleChildScrollView(
- child: ListBody(
- children: <Widget>[
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- MenuItemWidget(
- key: ValueKey("Allow collect $isCollectEnabled"),
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).allowAddingPhotos,
- ),
- alignCaptionedTextToLeft: true,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- trailingWidget: ToggleSwitchWidget(
- value: () => isCollectEnabled,
- onChanged: () async {
- await _updateUrlSettings(
- context,
- {'enableCollect': !isCollectEnabled},
- );
- },
- ),
- ),
- MenuSectionDescriptionWidget(
- content: S.of(context).allowAddPhotosDescription,
- ),
- const SizedBox(height: 24),
- MenuItemWidget(
- alignCaptionedTextToLeft: true,
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).linkExpiry,
- subTitle: (url.hasExpiry
- ? (url.isExpired
- ? S.of(context).linkExpired
- : S.of(context).linkEnabled)
- : S.of(context).linkNeverExpires),
- subTitleColor: url.isExpired ? warning500 : null,
- ),
- trailingIcon: Icons.chevron_right,
- menuItemColor: enteColorScheme.fillFaint,
- surfaceExecutionStates: false,
- onTap: () async {
- // ignore: unawaited_futures
- routeToPage(
- context,
- LinkExpiryPickerPage(widget.collection!),
- ).then((value) {
- setState(() {});
- });
- },
- ),
- url.hasExpiry
- ? MenuSectionDescriptionWidget(
- content: url.isExpired
- ? S.of(context).expiredLinkInfo
- : S.of(context).linkExpiresOn(
- getFormattedTime(
- context,
- DateTime.fromMicrosecondsSinceEpoch(
- url.validTill,
- ),
- ),
- ),
- )
- : const SizedBox.shrink(),
- const Padding(padding: EdgeInsets.only(top: 24)),
- MenuItemWidget(
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).linkDeviceLimit,
- subTitle: url.deviceLimit == 0
- ? S.of(context).noDeviceLimit
- : "${url.deviceLimit}",
- ),
- trailingIcon: Icons.chevron_right,
- menuItemColor: enteColorScheme.fillFaint,
- alignCaptionedTextToLeft: true,
- isBottomBorderRadiusRemoved: true,
- onTap: () async {
- // ignore: unawaited_futures
- routeToPage(
- context,
- DeviceLimitPickerPage(widget.collection!),
- ).then((value) {
- setState(() {});
- });
- },
- surfaceExecutionStates: false,
- ),
- DividerWidget(
- dividerType: DividerType.menuNoIcon,
- bgColor: getEnteColorScheme(context).fillFaint,
- ),
- MenuItemWidget(
- key: ValueKey("Allow downloads $isDownloadEnabled"),
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).allowDownloads,
- ),
- alignCaptionedTextToLeft: true,
- isBottomBorderRadiusRemoved: true,
- isTopBorderRadiusRemoved: true,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- trailingWidget: ToggleSwitchWidget(
- value: () => isDownloadEnabled,
- onChanged: () async {
- await _updateUrlSettings(
- context,
- {'enableDownload': !isDownloadEnabled},
- );
- if (!isDownloadEnabled) {
- // ignore: unawaited_futures
- showErrorDialog(
- context,
- S.of(context).disableDownloadWarningTitle,
- S.of(context).disableDownloadWarningBody,
- );
- }
- },
- ),
- ),
- DividerWidget(
- dividerType: DividerType.menuNoIcon,
- bgColor: getEnteColorScheme(context).fillFaint,
- ),
- MenuItemWidget(
- key: ValueKey("Password lock $isPasswordEnabled"),
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).passwordLock,
- ),
- alignCaptionedTextToLeft: true,
- isTopBorderRadiusRemoved: true,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- trailingWidget: ToggleSwitchWidget(
- value: () => isPasswordEnabled,
- onChanged: () async {
- if (!isPasswordEnabled) {
- // ignore: unawaited_futures
- showTextInputDialog(
- context,
- title: S.of(context).setAPassword,
- submitButtonLabel: S.of(context).lockButtonLabel,
- hintText: S.of(context).enterPassword,
- isPasswordInput: true,
- alwaysShowSuccessState: true,
- onSubmit: (String password) async {
- if (password.trim().isNotEmpty) {
- final propToUpdate =
- await _getEncryptedPassword(
- password,
- );
- await _updateUrlSettings(
- context,
- propToUpdate,
- showProgressDialog: false,
- );
- }
- },
- );
- } else {
- await _updateUrlSettings(
- context,
- {'disablePassword': true},
- );
- }
- },
- ),
- ),
- const SizedBox(
- height: 24,
- ),
- if (url.isExpired)
- MenuItemWidget(
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).linkHasExpired,
- textColor: getEnteColorScheme(context).warning500,
- ),
- leadingIcon: Icons.error_outline,
- leadingIconColor: getEnteColorScheme(context).warning500,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- isBottomBorderRadiusRemoved: true,
- ),
- if (!url.isExpired)
- MenuItemWidget(
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).copyLink,
- makeTextBold: true,
- ),
- leadingIcon: Icons.copy,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- showOnlyLoadingState: true,
- onTap: () async {
- await Clipboard.setData(ClipboardData(text: urlValue));
- showShortToast(
- context,
- S.of(context).linkCopiedToClipboard,
- );
- },
- isBottomBorderRadiusRemoved: true,
- ),
- if (!url.isExpired)
- DividerWidget(
- dividerType: DividerType.menu,
- bgColor: getEnteColorScheme(context).fillFaint,
- ),
- if (!url.isExpired)
- MenuItemWidget(
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).sendLink,
- makeTextBold: true,
- ),
- leadingIcon: Icons.adaptive.share,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- onTap: () async {
- // ignore: unawaited_futures
- shareText(urlValue);
- },
- isTopBorderRadiusRemoved: true,
- ),
- const SizedBox(
- height: 24,
- ),
- MenuItemWidget(
- captionedTextWidget: CaptionedTextWidget(
- title: S.of(context).removeLink,
- textColor: warning500,
- makeTextBold: true,
- ),
- leadingIcon: Icons.remove_circle_outline,
- leadingIconColor: warning500,
- menuItemColor: getEnteColorScheme(context).fillFaint,
- surfaceExecutionStates: false,
- onTap: () async {
- final bool result = await sharingActions.disableUrl(
- context,
- widget.collection!,
- );
- if (result && mounted) {
- Navigator.of(context).pop();
- if (widget.collection!.isQuickLinkCollection()) {
- Navigator.of(context).pop();
- }
- }
- },
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
- Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
- final kekSalt = CryptoUtil.getSaltToDeriveKey();
- final result = await CryptoUtil.deriveInteractiveKey(
- utf8.encode(pass) as Uint8List,
- kekSalt,
- );
- return {
- 'passHash': CryptoUtil.bin2base64(result.key),
- 'nonce': CryptoUtil.bin2base64(kekSalt),
- 'memLimit': result.memLimit,
- 'opsLimit': result.opsLimit,
- };
- }
- Future<void> _updateUrlSettings(
- BuildContext context,
- Map<String, dynamic> prop, {
- bool showProgressDialog = true,
- }) async {
- final dialog = showProgressDialog
- ? createProgressDialog(context, S.of(context).pleaseWait)
- : null;
- await dialog?.show();
- try {
- await CollectionsService.instance
- .updateShareUrl(widget.collection!, prop);
- await dialog?.hide();
- showShortToast(context, S.of(context).albumUpdated);
- if (mounted) {
- setState(() {});
- }
- } catch (e) {
- await dialog?.hide();
- await showGenericErrorDialog(context: context, error: e);
- rethrow;
- }
- }
- }
|