ente/lib/ui/sharing/manage_links_widget.dart

295 lines
12 KiB
Dart
Raw Normal View History

import 'dart:convert';
import 'dart:typed_data';
2022-02-21 02:13:10 +00:00
import 'package:collection/collection.dart';
2022-02-21 02:13:10 +00:00
import 'package:flutter/material.dart';
2023-04-05 03:25:35 +00:00
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
2022-11-20 10:14:45 +00:00
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
2022-11-20 10:14:45 +00:00
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
2023-01-31 12:21:57 +00:00
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
2022-11-20 10:14:45 +00:00
import 'package:photos/ui/components/menu_section_description_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/toast_util.dart';
2022-02-21 02:13:10 +00:00
class ManageSharedLinkWidget extends StatefulWidget {
final Collection? collection;
const ManageSharedLinkWidget({Key? key, this.collection}) : super(key: key);
2022-02-21 02:13:10 +00:00
@override
2022-07-03 09:45:00 +00:00
State<ManageSharedLinkWidget> createState() => _ManageSharedLinkWidgetState();
2022-02-21 02:13:10 +00:00
}
class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
2022-12-15 10:02:46 +00:00
final CollectionActions sharingActions =
CollectionActions(CollectionsService.instance);
2022-02-21 02:13:10 +00:00
2022-02-22 11:29:16 +00:00
@override
void initState() {
super.initState();
}
2022-02-21 02:13:10 +00:00
@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;
2022-11-20 10:14:45 +00:00
final enteColorScheme = getEnteColorScheme(context);
final PublicURL url = widget.collection!.publicURLs!.firstOrNull!;
2022-02-21 02:13:10 +00:00
return Scaffold(
appBar: AppBar(
elevation: 0,
2023-04-05 03:25:35 +00:00
title: Text(
S.of(context).manageLink,
2022-02-21 02:13:10 +00:00
),
),
body: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Padding(
2022-02-26 13:00:38 +00:00
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
2022-02-21 02:13:10 +00:00
child: Column(
2022-11-20 10:14:45 +00:00
crossAxisAlignment: CrossAxisAlignment.start,
2022-02-21 02:13:10 +00:00
children: [
2022-12-12 07:25:46 +00:00
MenuItemWidget(
key: ValueKey("Allow collect $isCollectEnabled"),
2023-04-05 03:25:35 +00:00
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).allowAddingPhotos,
2022-12-12 07:25:46 +00:00
),
alignCaptionedTextToLeft: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget.collection!.publicURLs?.firstOrNull
?.enableCollect ??
2022-12-12 07:25:46 +00:00
false,
onChanged: (value) async {
await _updateUrlSettings(
context,
{'enableCollect': value},
);
},
),
),
2023-04-05 03:25:35 +00:00
MenuSectionDescriptionWidget(
content: S.of(context).allowAddPhotosDescription,
2022-12-12 07:25:46 +00:00
),
2022-12-12 07:32:00 +00:00
const SizedBox(height: 24),
2022-11-20 10:14:45 +00:00
MenuItemWidget(
alignCaptionedTextToLeft: true,
captionedTextWidget: CaptionedTextWidget(
2023-04-05 03:25:35 +00:00
title: S.of(context).linkExpiry,
2022-11-21 09:28:17 +00:00
subTitle: (url.hasExpiry
2023-04-05 03:25:35 +00:00
? (url.isExpired
? S.of(context).linkExpired
: S.of(context).linkEnabled)
: S.of(context).linkNeverExpires),
2022-11-21 09:28:17 +00:00
subTitleColor: url.isExpired ? warning500 : null,
2022-11-20 10:14:45 +00:00
),
trailingIcon: Icons.chevron_right,
menuItemColor: enteColorScheme.fillFaint,
surfaceExecutionStates: false,
2022-02-21 02:13:10 +00:00
onTap: () async {
routeToPage(
context,
LinkExpiryPickerPage(widget.collection!),
).then((value) {
setState(() {});
});
2022-02-21 02:13:10 +00:00
},
),
2022-11-21 09:28:17 +00:00
url.hasExpiry
2022-11-20 10:14:45 +00:00
? MenuSectionDescriptionWidget(
2022-11-21 09:28:17 +00:00
content: url.isExpired
2023-04-05 03:25:35 +00:00
? S.of(context).expiredLinkInfo
: S.of(context).linkExpiresOn(
getFormattedTime(
context,
2023-04-05 03:25:35 +00:00
DateTime.fromMicrosecondsSinceEpoch(
url.validTill,
),
),
),
2022-11-20 10:14:45 +00:00
)
: const SizedBox.shrink(),
const Padding(padding: EdgeInsets.only(top: 24)),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
2023-04-05 03:25:35 +00:00
title: S.of(context).linkDeviceLimit,
subTitle: widget
.collection!.publicURLs!.first!.deviceLimit
2022-11-20 10:14:45 +00:00
.toString(),
2022-02-26 13:01:47 +00:00
),
2022-11-20 10:14:45 +00:00
trailingIcon: Icons.chevron_right,
menuItemColor: enteColorScheme.fillFaint,
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
onTap: () async {
2023-02-03 05:52:50 +00:00
routeToPage(
context,
DeviceLimitPickerPage(widget.collection!),
).then((value) {
setState(() {});
});
2022-11-20 10:14:45 +00:00
},
surfaceExecutionStates: false,
2022-02-26 13:01:47 +00:00
),
2022-11-20 10:14:45 +00:00
DividerWidget(
2022-12-16 04:46:20 +00:00
dividerType: DividerType.menuNoIcon,
bgColor: getEnteColorScheme(context).fillFaint,
2022-02-21 02:13:10 +00:00
),
2022-11-20 10:14:45 +00:00
MenuItemWidget(
key: ValueKey("Allow downloads $isDownloadEnabled"),
2023-04-05 03:25:35 +00:00
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).allowDownloads,
2022-11-20 10:14:45 +00:00
),
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: isDownloadEnabled,
2022-11-20 10:14:45 +00:00
onChanged: (value) async {
await _updateUrlSettings(
context,
{'enableDownload': value},
);
if (!value) {
showErrorDialog(
context,
2023-04-05 03:25:35 +00:00
S.of(context).disableDownloadWarningTitle,
S.of(context).disableDownloadWarningBody,
2022-11-20 10:14:45 +00:00
);
}
},
),
),
DividerWidget(
2022-12-16 04:46:20 +00:00
dividerType: DividerType.menuNoIcon,
bgColor: getEnteColorScheme(context).fillFaint,
2022-11-20 10:14:45 +00:00
),
MenuItemWidget(
key: ValueKey("Password lock $isPasswordEnabled"),
2023-04-05 03:25:35 +00:00
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).passwordLock,
2022-11-20 10:14:45 +00:00
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: isPasswordEnabled,
2022-11-20 10:14:45 +00:00
onChanged: (enablePassword) async {
if (enablePassword) {
showTextInputDialog(
context,
2023-04-05 03:25:35 +00:00
title: S.of(context).setAPassword,
submitButtonLabel: S.of(context).lockButtonLabel,
hintText: S.of(context).enterPassword,
isPasswordInput: true,
2023-02-13 11:06:41 +00:00
alwaysShowSuccessState: true,
onSubmit: (String password) async {
if (password.trim().isNotEmpty) {
final propToUpdate =
await _getEncryptedPassword(
password,
);
await _updateUrlSettings(
context,
propToUpdate,
showProgressDialog: false,
);
}
},
);
2022-11-20 10:14:45 +00:00
} else {
await _updateUrlSettings(
context,
{'disablePassword': true},
);
}
},
2022-02-21 02:13:10 +00:00
),
),
const SizedBox(
height: 24,
),
MenuItemWidget(
2023-04-05 03:25:35 +00:00
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).removeLink,
textColor: warning500,
2022-11-21 02:11:04 +00:00
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();
}
},
),
2022-02-21 02:13:10 +00:00
],
),
),
],
),
),
);
}
Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
final kekSalt = CryptoUtil.getSaltToDeriveKey();
2023-02-03 04:41:45 +00:00
final result = await CryptoUtil.deriveInteractiveKey(
utf8.encode(pass) as Uint8List,
2022-06-11 08:23:52 +00:00
kekSalt,
);
return {
2023-02-03 04:41:45 +00:00
'passHash': CryptoUtil.bin2base64(result.key),
'nonce': CryptoUtil.bin2base64(kekSalt),
'memLimit': result.memLimit,
'opsLimit': result.opsLimit,
};
}
Future<void> _updateUrlSettings(
2022-06-11 08:23:52 +00:00
BuildContext context,
Map<String, dynamic> prop, {
bool showProgressDialog = true,
}) async {
final dialog = showProgressDialog
2023-04-05 03:25:35 +00:00
? createProgressDialog(context, S.of(context).pleaseWait)
: null;
await dialog?.show();
try {
await CollectionsService.instance
.updateShareUrl(widget.collection!, prop);
await dialog?.hide();
2023-04-05 03:25:35 +00:00
showShortToast(context, S.of(context).albumUpdated);
if (mounted) {
setState(() {});
}
} catch (e) {
await dialog?.hide();
await showGenericErrorDialog(context: context);
rethrow;
}
}
2022-02-21 02:13:10 +00:00
}