manage_links_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:collection/collection.dart';
  4. import "package:fast_base58/fast_base58.dart";
  5. import 'package:flutter/material.dart';
  6. import "package:flutter/services.dart";
  7. import "package:photos/generated/l10n.dart";
  8. import "package:photos/models/api/collection/public_url.dart";
  9. import 'package:photos/models/collection.dart';
  10. import 'package:photos/services/collections_service.dart';
  11. import 'package:photos/theme/colors.dart';
  12. import 'package:photos/theme/ente_theme.dart';
  13. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  14. import 'package:photos/ui/components/captioned_text_widget.dart';
  15. import 'package:photos/ui/components/divider_widget.dart';
  16. import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
  17. import 'package:photos/ui/components/menu_section_description_widget.dart';
  18. import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart';
  19. import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart';
  20. import 'package:photos/utils/crypto_util.dart';
  21. import 'package:photos/utils/date_time_util.dart';
  22. import 'package:photos/utils/dialog_util.dart';
  23. import 'package:photos/utils/navigation_util.dart';
  24. import "package:photos/utils/share_util.dart";
  25. import 'package:photos/utils/toast_util.dart';
  26. class ManageSharedLinkWidget extends StatefulWidget {
  27. final Collection? collection;
  28. const ManageSharedLinkWidget({Key? key, this.collection}) : super(key: key);
  29. @override
  30. State<ManageSharedLinkWidget> createState() => _ManageSharedLinkWidgetState();
  31. }
  32. class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
  33. final CollectionActions sharingActions =
  34. CollectionActions(CollectionsService.instance);
  35. @override
  36. void initState() {
  37. super.initState();
  38. }
  39. @override
  40. Widget build(BuildContext context) {
  41. final isCollectEnabled =
  42. widget.collection!.publicURLs?.firstOrNull?.enableCollect ?? false;
  43. final isDownloadEnabled =
  44. widget.collection!.publicURLs?.firstOrNull?.enableDownload ?? true;
  45. final isPasswordEnabled =
  46. widget.collection!.publicURLs?.firstOrNull?.passwordEnabled ?? false;
  47. final enteColorScheme = getEnteColorScheme(context);
  48. final PublicURL url = widget.collection!.publicURLs!.firstOrNull!;
  49. final String collectionKey = Base58Encode(
  50. CollectionsService.instance.getCollectionKey(widget.collection!.id),
  51. );
  52. final String urlValue = "${url.url}#$collectionKey";
  53. return Scaffold(
  54. appBar: AppBar(
  55. elevation: 0,
  56. title: Text(
  57. S.of(context).manageLink,
  58. ),
  59. ),
  60. body: SingleChildScrollView(
  61. child: ListBody(
  62. children: <Widget>[
  63. Padding(
  64. padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
  65. child: Column(
  66. crossAxisAlignment: CrossAxisAlignment.start,
  67. children: [
  68. MenuItemWidget(
  69. key: ValueKey("Allow collect $isCollectEnabled"),
  70. captionedTextWidget: CaptionedTextWidget(
  71. title: S.of(context).allowAddingPhotos,
  72. ),
  73. alignCaptionedTextToLeft: true,
  74. menuItemColor: getEnteColorScheme(context).fillFaint,
  75. trailingWidget: Switch.adaptive(
  76. value: widget.collection!.publicURLs?.firstOrNull
  77. ?.enableCollect ??
  78. false,
  79. onChanged: (value) async {
  80. await _updateUrlSettings(
  81. context,
  82. {'enableCollect': value},
  83. );
  84. },
  85. ),
  86. ),
  87. MenuSectionDescriptionWidget(
  88. content: S.of(context).allowAddPhotosDescription,
  89. ),
  90. const SizedBox(height: 24),
  91. MenuItemWidget(
  92. alignCaptionedTextToLeft: true,
  93. captionedTextWidget: CaptionedTextWidget(
  94. title: S.of(context).linkExpiry,
  95. subTitle: (url.hasExpiry
  96. ? (url.isExpired
  97. ? S.of(context).linkExpired
  98. : S.of(context).linkEnabled)
  99. : S.of(context).linkNeverExpires),
  100. subTitleColor: url.isExpired ? warning500 : null,
  101. ),
  102. trailingIcon: Icons.chevron_right,
  103. menuItemColor: enteColorScheme.fillFaint,
  104. surfaceExecutionStates: false,
  105. onTap: () async {
  106. routeToPage(
  107. context,
  108. LinkExpiryPickerPage(widget.collection!),
  109. ).then((value) {
  110. setState(() {});
  111. });
  112. },
  113. ),
  114. url.hasExpiry
  115. ? MenuSectionDescriptionWidget(
  116. content: url.isExpired
  117. ? S.of(context).expiredLinkInfo
  118. : S.of(context).linkExpiresOn(
  119. getFormattedTime(
  120. context,
  121. DateTime.fromMicrosecondsSinceEpoch(
  122. url.validTill,
  123. ),
  124. ),
  125. ),
  126. )
  127. : const SizedBox.shrink(),
  128. const Padding(padding: EdgeInsets.only(top: 24)),
  129. MenuItemWidget(
  130. captionedTextWidget: CaptionedTextWidget(
  131. title: S.of(context).linkDeviceLimit,
  132. subTitle: widget
  133. .collection!.publicURLs!.first!.deviceLimit
  134. .toString(),
  135. ),
  136. trailingIcon: Icons.chevron_right,
  137. menuItemColor: enteColorScheme.fillFaint,
  138. alignCaptionedTextToLeft: true,
  139. isBottomBorderRadiusRemoved: true,
  140. onTap: () async {
  141. routeToPage(
  142. context,
  143. DeviceLimitPickerPage(widget.collection!),
  144. ).then((value) {
  145. setState(() {});
  146. });
  147. },
  148. surfaceExecutionStates: false,
  149. ),
  150. DividerWidget(
  151. dividerType: DividerType.menuNoIcon,
  152. bgColor: getEnteColorScheme(context).fillFaint,
  153. ),
  154. MenuItemWidget(
  155. key: ValueKey("Allow downloads $isDownloadEnabled"),
  156. captionedTextWidget: CaptionedTextWidget(
  157. title: S.of(context).allowDownloads,
  158. ),
  159. alignCaptionedTextToLeft: true,
  160. isBottomBorderRadiusRemoved: true,
  161. isTopBorderRadiusRemoved: true,
  162. menuItemColor: getEnteColorScheme(context).fillFaint,
  163. trailingWidget: Switch.adaptive(
  164. value: isDownloadEnabled,
  165. onChanged: (value) async {
  166. await _updateUrlSettings(
  167. context,
  168. {'enableDownload': value},
  169. );
  170. if (!value) {
  171. showErrorDialog(
  172. context,
  173. S.of(context).disableDownloadWarningTitle,
  174. S.of(context).disableDownloadWarningBody,
  175. );
  176. }
  177. },
  178. ),
  179. ),
  180. DividerWidget(
  181. dividerType: DividerType.menuNoIcon,
  182. bgColor: getEnteColorScheme(context).fillFaint,
  183. ),
  184. MenuItemWidget(
  185. key: ValueKey("Password lock $isPasswordEnabled"),
  186. captionedTextWidget: CaptionedTextWidget(
  187. title: S.of(context).passwordLock,
  188. ),
  189. alignCaptionedTextToLeft: true,
  190. isTopBorderRadiusRemoved: true,
  191. menuItemColor: getEnteColorScheme(context).fillFaint,
  192. trailingWidget: Switch.adaptive(
  193. value: isPasswordEnabled,
  194. onChanged: (enablePassword) async {
  195. if (enablePassword) {
  196. showTextInputDialog(
  197. context,
  198. title: S.of(context).setAPassword,
  199. submitButtonLabel: S.of(context).lockButtonLabel,
  200. hintText: S.of(context).enterPassword,
  201. isPasswordInput: true,
  202. alwaysShowSuccessState: true,
  203. onSubmit: (String password) async {
  204. if (password.trim().isNotEmpty) {
  205. final propToUpdate =
  206. await _getEncryptedPassword(
  207. password,
  208. );
  209. await _updateUrlSettings(
  210. context,
  211. propToUpdate,
  212. showProgressDialog: false,
  213. );
  214. }
  215. },
  216. );
  217. } else {
  218. await _updateUrlSettings(
  219. context,
  220. {'disablePassword': true},
  221. );
  222. }
  223. },
  224. ),
  225. ),
  226. const SizedBox(
  227. height: 24,
  228. ),
  229. if (url.isExpired)
  230. MenuItemWidget(
  231. captionedTextWidget: CaptionedTextWidget(
  232. title: S.of(context).linkHasExpired,
  233. textColor: getEnteColorScheme(context).warning500,
  234. ),
  235. leadingIcon: Icons.error_outline,
  236. leadingIconColor: getEnteColorScheme(context).warning500,
  237. menuItemColor: getEnteColorScheme(context).fillFaint,
  238. isBottomBorderRadiusRemoved: true,
  239. ),
  240. if (!url.isExpired)
  241. MenuItemWidget(
  242. captionedTextWidget: CaptionedTextWidget(
  243. title: S.of(context).copyLink,
  244. makeTextBold: true,
  245. ),
  246. leadingIcon: Icons.copy,
  247. menuItemColor: getEnteColorScheme(context).fillFaint,
  248. showOnlyLoadingState: true,
  249. onTap: () async {
  250. await Clipboard.setData(ClipboardData(text: urlValue));
  251. showShortToast(
  252. context,
  253. S.of(context).linkCopiedToClipboard,
  254. );
  255. },
  256. isBottomBorderRadiusRemoved: true,
  257. ),
  258. if (!url.isExpired)
  259. DividerWidget(
  260. dividerType: DividerType.menu,
  261. bgColor: getEnteColorScheme(context).fillFaint,
  262. ),
  263. if (!url.isExpired)
  264. MenuItemWidget(
  265. captionedTextWidget: CaptionedTextWidget(
  266. title: S.of(context).sendLink,
  267. makeTextBold: true,
  268. ),
  269. leadingIcon: Icons.adaptive.share,
  270. menuItemColor: getEnteColorScheme(context).fillFaint,
  271. onTap: () async {
  272. shareText(urlValue);
  273. },
  274. isTopBorderRadiusRemoved: true,
  275. ),
  276. const SizedBox(
  277. height: 24,
  278. ),
  279. MenuItemWidget(
  280. captionedTextWidget: CaptionedTextWidget(
  281. title: S.of(context).removeLink,
  282. textColor: warning500,
  283. makeTextBold: true,
  284. ),
  285. leadingIcon: Icons.remove_circle_outline,
  286. leadingIconColor: warning500,
  287. menuItemColor: getEnteColorScheme(context).fillFaint,
  288. surfaceExecutionStates: false,
  289. onTap: () async {
  290. final bool result = await sharingActions.disableUrl(
  291. context,
  292. widget.collection!,
  293. );
  294. if (result && mounted) {
  295. Navigator.of(context).pop();
  296. if (widget.collection!.isQuickLinkCollection()) {
  297. Navigator.of(context).pop();
  298. }
  299. }
  300. },
  301. ),
  302. ],
  303. ),
  304. ),
  305. ],
  306. ),
  307. ),
  308. );
  309. }
  310. Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
  311. final kekSalt = CryptoUtil.getSaltToDeriveKey();
  312. final result = await CryptoUtil.deriveInteractiveKey(
  313. utf8.encode(pass) as Uint8List,
  314. kekSalt,
  315. );
  316. return {
  317. 'passHash': CryptoUtil.bin2base64(result.key),
  318. 'nonce': CryptoUtil.bin2base64(kekSalt),
  319. 'memLimit': result.memLimit,
  320. 'opsLimit': result.opsLimit,
  321. };
  322. }
  323. Future<void> _updateUrlSettings(
  324. BuildContext context,
  325. Map<String, dynamic> prop, {
  326. bool showProgressDialog = true,
  327. }) async {
  328. final dialog = showProgressDialog
  329. ? createProgressDialog(context, S.of(context).pleaseWait)
  330. : null;
  331. await dialog?.show();
  332. try {
  333. await CollectionsService.instance
  334. .updateShareUrl(widget.collection!, prop);
  335. await dialog?.hide();
  336. showShortToast(context, S.of(context).albumUpdated);
  337. if (mounted) {
  338. setState(() {});
  339. }
  340. } catch (e) {
  341. await dialog?.hide();
  342. await showGenericErrorDialog(context: context);
  343. rethrow;
  344. }
  345. }
  346. }