share_collection_page.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // @dart=2.9
  2. import 'package:fast_base58/fast_base58.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_typeahead/flutter_typeahead.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/db/public_keys_db.dart';
  9. import 'package:photos/ente_theme_data.dart';
  10. import 'package:photos/models/collection.dart';
  11. import 'package:photos/models/public_key.dart';
  12. import 'package:photos/services/collections_service.dart';
  13. import 'package:photos/services/user_service.dart';
  14. import 'package:photos/theme/ente_theme.dart';
  15. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  16. import 'package:photos/ui/common/gradient_button.dart';
  17. import 'package:photos/ui/common/loading_widget.dart';
  18. import 'package:photos/ui/components/captioned_text_widget.dart';
  19. import 'package:photos/ui/components/divider_widget.dart';
  20. import 'package:photos/ui/components/menu_item_widget.dart';
  21. import 'package:photos/ui/components/menu_section_title.dart';
  22. import 'package:photos/ui/payment/subscription.dart';
  23. import 'package:photos/ui/sharing/manage_album_participant.dart';
  24. import 'package:photos/ui/sharing/manage_links_widget.dart';
  25. import 'package:photos/utils/dialog_util.dart';
  26. import 'package:photos/utils/email_util.dart';
  27. import 'package:photos/utils/navigation_util.dart';
  28. import 'package:photos/utils/share_util.dart';
  29. import 'package:photos/utils/toast_util.dart';
  30. class ShareCollectionPage extends StatefulWidget {
  31. final Collection collection;
  32. const ShareCollectionPage(this.collection, {Key key}) : super(key: key);
  33. @override
  34. State<ShareCollectionPage> createState() => _ShareCollectionPageState();
  35. }
  36. class _ShareCollectionPageState extends State<ShareCollectionPage> {
  37. bool _showEntryField = false;
  38. List<User> _sharees;
  39. String _email;
  40. final Logger _logger = Logger("SharingDialogState");
  41. final CollectionSharingActions sharingActions =
  42. CollectionSharingActions(CollectionsService.instance);
  43. @override
  44. Widget build(BuildContext context) {
  45. _sharees = widget.collection.sharees;
  46. final children = <Widget>[];
  47. children.add(
  48. MenuSectionTitle(
  49. title: _sharees.isEmpty
  50. ? "Share with specific people"
  51. : "Shared with ${_sharees.length} people",
  52. iconData: Icons.workspaces,
  53. ),
  54. );
  55. if (!_showEntryField && _sharees.isEmpty) {
  56. _showEntryField = true;
  57. } else {
  58. for (final user in _sharees) {
  59. children.add(
  60. EmailItemWidget(
  61. widget.collection,
  62. user.email,
  63. user,
  64. ),
  65. );
  66. }
  67. }
  68. if (_showEntryField) {
  69. children.add(_getEmailField());
  70. }
  71. children.add(
  72. const Padding(
  73. padding: EdgeInsets.all(8),
  74. ),
  75. );
  76. if (!_showEntryField) {
  77. children.add(
  78. SizedBox(
  79. width: 220,
  80. child: GradientButton(
  81. onTap: () async {
  82. setState(() {
  83. _showEntryField = true;
  84. });
  85. },
  86. iconData: Icons.add,
  87. ),
  88. ),
  89. );
  90. } else {
  91. children.add(
  92. SizedBox(
  93. width: 240,
  94. height: 50,
  95. child: OutlinedButton(
  96. child: const Text("Add"),
  97. onPressed: () {
  98. _addEmailToCollection(_email?.trim() ?? '');
  99. },
  100. ),
  101. ),
  102. );
  103. }
  104. final bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
  105. children.addAll([
  106. const SizedBox(
  107. height: 24,
  108. ),
  109. MenuSectionTitle(
  110. title: hasUrl ? "Public link enabled" : "Share a public link",
  111. iconData: Icons.public,
  112. ),
  113. ]);
  114. if (hasUrl) {
  115. final String collectionKey = Base58Encode(
  116. CollectionsService.instance.getCollectionKey(widget.collection.id),
  117. );
  118. final String url =
  119. "${widget.collection.publicURLs.first.url}#$collectionKey";
  120. children.addAll(
  121. [
  122. MenuItemWidget(
  123. captionedTextWidget: const CaptionedTextWidget(
  124. title: "Copy link",
  125. makeTextBold: true,
  126. ),
  127. leadingIcon: Icons.copy,
  128. menuItemColor: getEnteColorScheme(context).fillFaint,
  129. pressedColor: getEnteColorScheme(context).fillFaint,
  130. onTap: () async {
  131. await Clipboard.setData(ClipboardData(text: url));
  132. showToast(context, "Link copied to clipboard");
  133. },
  134. isBottomBorderRadiusRemoved: true,
  135. ),
  136. DividerWidget(
  137. dividerType: DividerType.menu,
  138. bgColor: getEnteColorScheme(context).blurStrokeFaint,
  139. ),
  140. MenuItemWidget(
  141. captionedTextWidget: const CaptionedTextWidget(
  142. title: "Send link",
  143. makeTextBold: true,
  144. ),
  145. leadingIcon: Icons.adaptive.share,
  146. menuItemColor: getEnteColorScheme(context).fillFaint,
  147. pressedColor: getEnteColorScheme(context).fillFaint,
  148. onTap: () async {
  149. shareText(url);
  150. },
  151. isTopBorderRadiusRemoved: true,
  152. ),
  153. DividerWidget(
  154. dividerType: DividerType.menu,
  155. bgColor: getEnteColorScheme(context).blurStrokeFaint,
  156. ),
  157. MenuItemWidget(
  158. captionedTextWidget: const CaptionedTextWidget(
  159. title: "Manage link",
  160. makeTextBold: true,
  161. ),
  162. leadingIcon: Icons.link,
  163. trailingIcon: Icons.navigate_next,
  164. menuItemColor: getEnteColorScheme(context).fillFaint,
  165. pressedColor: getEnteColorScheme(context).fillFaint,
  166. trailingIconIsMuted: true,
  167. onTap: () async {
  168. routeToPage(
  169. context,
  170. ManageSharedLinkWidget(collection: widget.collection),
  171. ).then(
  172. (value) => {
  173. if (mounted) {setState(() => {})}
  174. },
  175. );
  176. },
  177. isTopBorderRadiusRemoved: true,
  178. ),
  179. ],
  180. );
  181. } else {
  182. children.add(
  183. MenuItemWidget(
  184. captionedTextWidget: const CaptionedTextWidget(
  185. title: "Create public link",
  186. ),
  187. leadingIcon: Icons.link,
  188. menuItemColor: getEnteColorScheme(context).fillFaint,
  189. pressedColor: getEnteColorScheme(context).fillFaint,
  190. onTap: () async {
  191. final bool result = await sharingActions.publicLinkToggle(
  192. context,
  193. widget.collection,
  194. true,
  195. );
  196. if (result && mounted) {
  197. setState(() => {});
  198. }
  199. },
  200. ),
  201. );
  202. }
  203. return Scaffold(
  204. appBar: AppBar(title: const Text("Sharing")),
  205. body: SingleChildScrollView(
  206. child: ListBody(
  207. children: <Widget>[
  208. Padding(
  209. padding:
  210. const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16),
  211. child: Column(
  212. children: children,
  213. ),
  214. ),
  215. ],
  216. ),
  217. ),
  218. );
  219. }
  220. Widget _getEmailField() {
  221. return Container(
  222. padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
  223. child: Row(
  224. children: [
  225. Expanded(
  226. child: TypeAheadField(
  227. textFieldConfiguration: const TextFieldConfiguration(
  228. keyboardType: TextInputType.emailAddress,
  229. decoration: InputDecoration(
  230. border: InputBorder.none,
  231. hintText: "email@your-friend.com",
  232. ),
  233. ),
  234. hideOnEmpty: true,
  235. loadingBuilder: (context) {
  236. return const EnteLoadingWidget();
  237. },
  238. suggestionsCallback: (pattern) async {
  239. _email = pattern;
  240. return PublicKeysDB.instance.searchByEmail(_email);
  241. },
  242. itemBuilder: (context, suggestion) {
  243. return Container(
  244. padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
  245. child: Text(
  246. suggestion.email,
  247. overflow: TextOverflow.clip,
  248. ),
  249. );
  250. },
  251. onSuggestionSelected: (PublicKey suggestion) {
  252. _addEmailToCollection(
  253. suggestion.email,
  254. publicKey: suggestion.publicKey,
  255. );
  256. },
  257. ),
  258. ),
  259. ],
  260. ),
  261. );
  262. }
  263. Future<void> _addEmailToCollection(
  264. String email, {
  265. String publicKey,
  266. }) async {
  267. if (!isValidEmail(email)) {
  268. showErrorDialog(
  269. context,
  270. "Invalid email address",
  271. "Please enter a valid email address.",
  272. );
  273. return;
  274. } else if (email == Configuration.instance.getEmail()) {
  275. showErrorDialog(context, "Oops", "You cannot share with yourself");
  276. return;
  277. } else if (widget.collection.sharees.any((user) => user.email == email)) {
  278. showErrorDialog(
  279. context,
  280. "Oops",
  281. "You're already sharing this with " + email,
  282. );
  283. return;
  284. }
  285. if (publicKey == null) {
  286. final dialog = createProgressDialog(context, "Searching for user...");
  287. await dialog.show();
  288. publicKey = await UserService.instance.getPublicKey(email);
  289. await dialog.hide();
  290. }
  291. if (publicKey == null) {
  292. Navigator.of(context, rootNavigator: true).pop('dialog');
  293. final dialog = AlertDialog(
  294. title: const Text("Invite to ente?"),
  295. content: Text(
  296. "Looks like " +
  297. email +
  298. " hasn't signed up for ente yet. would you like to invite them?",
  299. style: const TextStyle(
  300. height: 1.4,
  301. ),
  302. ),
  303. actions: [
  304. TextButton(
  305. child: Text(
  306. "Invite",
  307. style: TextStyle(
  308. color: Theme.of(context).colorScheme.greenAlternative,
  309. ),
  310. ),
  311. onPressed: () {
  312. shareText(
  313. "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.",
  314. );
  315. },
  316. ),
  317. ],
  318. );
  319. showDialog(
  320. context: context,
  321. builder: (BuildContext context) {
  322. return dialog;
  323. },
  324. );
  325. } else {
  326. final dialog = createProgressDialog(context, "Sharing...");
  327. await dialog.show();
  328. try {
  329. await CollectionsService.instance
  330. .share(widget.collection.id, email, publicKey);
  331. await dialog.hide();
  332. showShortToast(context, "Shared successfully!");
  333. setState(() {
  334. _sharees.add(User(email: email));
  335. _showEntryField = false;
  336. });
  337. } catch (e) {
  338. await dialog.hide();
  339. if (e is SharingNotPermittedForFreeAccountsError) {
  340. _showUnSupportedAlert();
  341. } else {
  342. _logger.severe("failed to share collection", e);
  343. showGenericErrorDialog(context);
  344. }
  345. }
  346. }
  347. }
  348. void _showUnSupportedAlert() {
  349. final AlertDialog alert = AlertDialog(
  350. title: const Text("Sorry"),
  351. content: const Text(
  352. "Sharing is not permitted for free accounts, please subscribe",
  353. ),
  354. actions: [
  355. TextButton(
  356. child: Text(
  357. "Subscribe",
  358. style: TextStyle(
  359. color: Theme.of(context).colorScheme.greenAlternative,
  360. ),
  361. ),
  362. onPressed: () {
  363. Navigator.of(context).pushReplacement(
  364. MaterialPageRoute(
  365. builder: (BuildContext context) {
  366. return getSubscriptionPage();
  367. },
  368. ),
  369. );
  370. },
  371. ),
  372. TextButton(
  373. child: Text(
  374. "Ok",
  375. style: TextStyle(
  376. color: Theme.of(context).colorScheme.onSurface,
  377. ),
  378. ),
  379. onPressed: () {
  380. Navigator.of(context, rootNavigator: true).pop();
  381. },
  382. ),
  383. ],
  384. );
  385. showDialog(
  386. context: context,
  387. builder: (BuildContext context) {
  388. return alert;
  389. },
  390. );
  391. }
  392. }
  393. class EmailItemWidget extends StatelessWidget {
  394. final Collection collection;
  395. final String email;
  396. final User user;
  397. const EmailItemWidget(
  398. this.collection,
  399. this.email,
  400. this.user, {
  401. Key key,
  402. }) : super(key: key);
  403. @override
  404. Widget build(BuildContext context) {
  405. return Row(
  406. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  407. children: [
  408. Padding(
  409. padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
  410. child: GestureDetector(
  411. onTap: () async {
  412. await routeToPage(
  413. context,
  414. ManageIndividualParticipant(collection: collection, user: user),
  415. );
  416. },
  417. child: Text(
  418. email,
  419. style: const TextStyle(fontSize: 16),
  420. ),
  421. ),
  422. ),
  423. const Expanded(child: SizedBox()),
  424. IconButton(
  425. icon: const Icon(Icons.delete_forever),
  426. color: Colors.redAccent,
  427. onPressed: () async {
  428. final dialog = createProgressDialog(context, "Please wait...");
  429. await dialog.show();
  430. try {
  431. await CollectionsService.instance.unshare(collection.id, email);
  432. collection.sharees.removeWhere((user) => user.email == email);
  433. await dialog.hide();
  434. showToast(context, "Stopped sharing with " + email + ".");
  435. Navigator.of(context).pop();
  436. } catch (e, s) {
  437. Logger("EmailItemWidget").severe(e, s);
  438. await dialog.hide();
  439. showGenericErrorDialog(context);
  440. }
  441. },
  442. ),
  443. ],
  444. );
  445. }
  446. }