share_collection_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import 'dart:ui';
  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:fluttercontactpicker/fluttercontactpicker.dart';
  7. import 'package:logging/logging.dart';
  8. import 'package:photos/core/configuration.dart';
  9. import 'package:photos/db/public_keys_db.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/feature_flag_service.dart';
  14. import 'package:photos/services/user_service.dart';
  15. import 'package:photos/ui/common/dialogs.dart';
  16. import 'package:photos/ui/common_elements.dart';
  17. import 'package:photos/ui/loading_widget.dart';
  18. import 'package:photos/ui/manage_links_widget.dart';
  19. import 'package:photos/ui/payment/subscription.dart';
  20. import 'package:photos/utils/dialog_util.dart';
  21. import 'package:photos/utils/email_util.dart';
  22. import 'package:photos/utils/navigation_util.dart';
  23. import 'package:photos/utils/share_util.dart';
  24. import 'package:photos/utils/toast_util.dart';
  25. class SharingDialog extends StatefulWidget {
  26. final Collection collection;
  27. SharingDialog(this.collection, {Key key}) : super(key: key);
  28. @override
  29. _SharingDialogState createState() => _SharingDialogState();
  30. }
  31. class _SharingDialogState extends State<SharingDialog> {
  32. bool _showEntryField = false;
  33. List<User> _sharees;
  34. String _email;
  35. final Logger _logger = Logger("SharingDialogState");
  36. @override
  37. Widget build(BuildContext context) {
  38. _sharees = widget.collection.sharees;
  39. final children = <Widget>[];
  40. if (!_showEntryField && _sharees.isEmpty) {
  41. _showEntryField = true;
  42. } else {
  43. for (final user in _sharees) {
  44. children.add(EmailItemWidget(widget.collection, user.email));
  45. }
  46. }
  47. if (_showEntryField) {
  48. children.add(_getEmailField());
  49. }
  50. children.add(Padding(
  51. padding: EdgeInsets.all(8),
  52. ));
  53. if (!_showEntryField) {
  54. children.add(SizedBox(
  55. width: 220,
  56. child: OutlineButton(
  57. child: Icon(
  58. Icons.add,
  59. ),
  60. onPressed: () {
  61. setState(() {
  62. _showEntryField = true;
  63. });
  64. },
  65. ),
  66. ));
  67. } else {
  68. children.add(
  69. SizedBox(
  70. width: 240,
  71. height: 50,
  72. child: button(
  73. "add",
  74. onPressed: () {
  75. _addEmailToCollection(_email?.trim() ?? '');
  76. },
  77. ),
  78. ),
  79. );
  80. }
  81. if (!FeatureFlagService.instance.disableUrlSharing()) {
  82. bool hasUrl = widget.collection.publicURLs?.isNotEmpty ?? false;
  83. children.addAll([
  84. Padding(padding: EdgeInsets.all(16)),
  85. Divider(height: 1),
  86. Padding(padding: EdgeInsets.all(12)),
  87. SizedBox(
  88. height: 36,
  89. child: Row(
  90. mainAxisAlignment: MainAxisAlignment.center,
  91. children: [
  92. Text("public link"),
  93. Switch(
  94. value: hasUrl,
  95. onChanged: (enable) async {
  96. // confirm if user wants to disable the url
  97. if (!enable) {
  98. final choice = await showChoiceDialog(
  99. context,
  100. 'disable link',
  101. 'are you sure that you want to disable the album link?',
  102. firstAction: 'yes, disable',
  103. secondAction: 'no',
  104. actionType: ActionType.critical);
  105. if (choice != DialogUserChoice.firstChoice) {
  106. return;
  107. }
  108. }
  109. final dialog = createProgressDialog(context,
  110. enable ? "creating link..." : "disabling link...");
  111. try {
  112. await dialog.show();
  113. enable
  114. ? await CollectionsService.instance
  115. .createShareUrl(widget.collection)
  116. : await CollectionsService.instance
  117. .disableShareUrl(widget.collection);
  118. dialog.hide();
  119. setState(() {});
  120. } catch (e) {
  121. dialog.hide();
  122. if (e is SharingNotPermittedForFreeAccountsError) {
  123. _showUnSupportedAlert();
  124. } else {
  125. _logger.severe("failed to share collection", e);
  126. showGenericErrorDialog(context);
  127. }
  128. }
  129. },
  130. ),
  131. ],
  132. ),
  133. ),
  134. ]);
  135. if (widget.collection.publicURLs?.isNotEmpty ?? false) {
  136. children.add(Padding(
  137. padding: EdgeInsets.all(2),
  138. ));
  139. children.add(_getShareableUrlWidget(context));
  140. }
  141. }
  142. return AlertDialog(
  143. title: Text("sharing"),
  144. content: SingleChildScrollView(
  145. child: ListBody(
  146. children: <Widget>[
  147. Padding(
  148. padding: const EdgeInsets.all(4.0),
  149. child: Column(
  150. children: children,
  151. )),
  152. ],
  153. ),
  154. ),
  155. contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 4),
  156. );
  157. }
  158. Widget _getEmailField() {
  159. return Row(
  160. children: [
  161. Expanded(
  162. child: TypeAheadField(
  163. textFieldConfiguration: TextFieldConfiguration(
  164. keyboardType: TextInputType.emailAddress,
  165. decoration: InputDecoration(
  166. border: InputBorder.none,
  167. hintText: "email@your-friend.com",
  168. ),
  169. ),
  170. hideOnEmpty: true,
  171. loadingBuilder: (context) {
  172. return loadWidget;
  173. },
  174. suggestionsCallback: (pattern) async {
  175. _email = pattern;
  176. return PublicKeysDB.instance.searchByEmail(_email);
  177. },
  178. itemBuilder: (context, suggestion) {
  179. return Container(
  180. padding: EdgeInsets.fromLTRB(12, 8, 12, 8),
  181. child: Text(
  182. suggestion.email,
  183. overflow: TextOverflow.clip,
  184. ),
  185. );
  186. },
  187. onSuggestionSelected: (PublicKey suggestion) {
  188. _addEmailToCollection(suggestion.email,
  189. publicKey: suggestion.publicKey);
  190. },
  191. ),
  192. ),
  193. Padding(padding: EdgeInsets.all(8)),
  194. IconButton(
  195. icon: Icon(
  196. Icons.contact_mail_outlined,
  197. color: Theme.of(context).buttonColor.withOpacity(0.8),
  198. ),
  199. onPressed: () async {
  200. final emailContact = await FlutterContactPicker.pickEmailContact(
  201. askForPermission: true);
  202. _addEmailToCollection(emailContact.email.email);
  203. },
  204. ),
  205. ],
  206. );
  207. }
  208. Widget _getShareableUrlWidget(BuildContext parentContext) {
  209. String collectionKey = Base58Encode(
  210. CollectionsService.instance.getCollectionKey(widget.collection.id));
  211. String url = "${widget.collection.publicURLs.first.url}#$collectionKey";
  212. return SingleChildScrollView(
  213. child: Column(
  214. mainAxisAlignment: MainAxisAlignment.start,
  215. crossAxisAlignment: CrossAxisAlignment.start,
  216. children: [
  217. Padding(padding: EdgeInsets.all(4)),
  218. GestureDetector(
  219. onTap: () async {
  220. await Clipboard.setData(ClipboardData(text: url));
  221. showToast("link copied to clipboard");
  222. },
  223. child: Container(
  224. padding: EdgeInsets.all(16),
  225. child: Row(
  226. crossAxisAlignment: CrossAxisAlignment.end,
  227. children: [
  228. Flexible(
  229. child: Text(
  230. url,
  231. style: TextStyle(
  232. fontSize: 16,
  233. fontFeatures: const [FontFeature.tabularFigures()],
  234. color: Colors.white.withOpacity(0.68),
  235. overflow: TextOverflow.ellipsis,
  236. ),
  237. ),
  238. ),
  239. Padding(padding: EdgeInsets.all(2)),
  240. Icon(
  241. Icons.copy,
  242. size: 18,
  243. ),
  244. ],
  245. ),
  246. color: Colors.white.withOpacity(0.02),
  247. ),
  248. ),
  249. Padding(padding: EdgeInsets.all(2)),
  250. TextButton(
  251. child: Padding(
  252. padding: const EdgeInsets.all(12),
  253. child: Row(
  254. mainAxisAlignment: MainAxisAlignment.center,
  255. children: [
  256. Icon(
  257. Icons.adaptive.share,
  258. color: Theme.of(context).buttonColor,
  259. ),
  260. Padding(
  261. padding: EdgeInsets.all(4),
  262. ),
  263. Text(
  264. "share link",
  265. style: TextStyle(
  266. color: Theme.of(context).buttonColor,
  267. ),
  268. ),
  269. ],
  270. ),
  271. ),
  272. onPressed: () {
  273. shareText(url);
  274. },
  275. ),
  276. Padding(padding: EdgeInsets.all(4)),
  277. TextButton(
  278. child: Center(
  279. child: Text(
  280. "manage link",
  281. style: TextStyle(
  282. color: Colors.white70,
  283. decoration: TextDecoration.underline,
  284. ),
  285. ),
  286. ),
  287. onPressed: () async {
  288. routeToPage(
  289. parentContext,
  290. ManageSharedLinkWidget(collection: widget.collection),
  291. );
  292. },
  293. ),
  294. ],
  295. ),
  296. );
  297. }
  298. Future<void> _addEmailToCollection(
  299. String email, {
  300. String publicKey,
  301. }) async {
  302. if (!isValidEmail(email)) {
  303. showErrorDialog(context, "invalid email address",
  304. "please enter a valid email address.");
  305. return;
  306. } else if (email == Configuration.instance.getEmail()) {
  307. showErrorDialog(context, "oops", "you cannot share with yourself");
  308. return;
  309. } else if (widget.collection.sharees.any((user) => user.email == email)) {
  310. showErrorDialog(
  311. context, "oops", "you're already sharing this with " + email);
  312. return;
  313. }
  314. if (publicKey == null) {
  315. final dialog = createProgressDialog(context, "searching for user...");
  316. await dialog.show();
  317. publicKey = await UserService.instance.getPublicKey(email);
  318. await dialog.hide();
  319. }
  320. if (publicKey == null) {
  321. Navigator.of(context, rootNavigator: true).pop('dialog');
  322. final dialog = AlertDialog(
  323. title: Text("invite to ente?"),
  324. content: Text(
  325. "looks like " +
  326. email +
  327. " hasn't signed up for ente yet. would you like to invite them?",
  328. style: TextStyle(
  329. height: 1.4,
  330. ),
  331. ),
  332. actions: [
  333. TextButton(
  334. child: Text(
  335. "invite",
  336. style: TextStyle(
  337. color: Theme.of(context).buttonColor,
  338. ),
  339. ),
  340. onPressed: () {
  341. shareText(
  342. "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.");
  343. },
  344. ),
  345. ],
  346. );
  347. showDialog(
  348. context: context,
  349. builder: (BuildContext context) {
  350. return dialog;
  351. },
  352. );
  353. } else {
  354. final dialog = createProgressDialog(context, "sharing...");
  355. await dialog.show();
  356. final collection = widget.collection;
  357. try {
  358. if (collection.type == CollectionType.folder) {
  359. final path =
  360. CollectionsService.instance.decryptCollectionPath(collection);
  361. if (!Configuration.instance.getPathsToBackUp().contains(path)) {
  362. await Configuration.instance.addPathToFoldersToBeBackedUp(path);
  363. }
  364. }
  365. await CollectionsService.instance
  366. .share(widget.collection.id, email, publicKey);
  367. await dialog.hide();
  368. showToast("shared successfully!");
  369. setState(() {
  370. _sharees.add(User(email: email));
  371. _showEntryField = false;
  372. });
  373. } catch (e) {
  374. await dialog.hide();
  375. if (e is SharingNotPermittedForFreeAccountsError) {
  376. _showUnSupportedAlert();
  377. } else {
  378. _logger.severe("failed to share collection", e);
  379. showGenericErrorDialog(context);
  380. }
  381. }
  382. }
  383. }
  384. void _showUnSupportedAlert() {
  385. AlertDialog alert = AlertDialog(
  386. title: Text("sorry"),
  387. content:
  388. Text("sharing is not permitted for free accounts, please subscribe"),
  389. actions: [
  390. TextButton(
  391. child: Text(
  392. "subscribe",
  393. style: TextStyle(
  394. color: Theme.of(context).buttonColor,
  395. ),
  396. ),
  397. onPressed: () {
  398. Navigator.of(context).pushReplacement(
  399. MaterialPageRoute(
  400. builder: (BuildContext context) {
  401. return getSubscriptionPage();
  402. },
  403. ),
  404. );
  405. },
  406. ),
  407. TextButton(
  408. child: Text(
  409. "ok",
  410. style: TextStyle(
  411. color: Colors.white,
  412. ),
  413. ),
  414. onPressed: () {
  415. Navigator.of(context, rootNavigator: true).pop();
  416. },
  417. ),
  418. ],
  419. );
  420. showDialog(
  421. context: context,
  422. builder: (BuildContext context) {
  423. return alert;
  424. },
  425. );
  426. }
  427. }
  428. class EmailItemWidget extends StatelessWidget {
  429. final Collection collection;
  430. final String email;
  431. const EmailItemWidget(
  432. this.collection,
  433. this.email, {
  434. Key key,
  435. }) : super(key: key);
  436. @override
  437. Widget build(BuildContext context) {
  438. return Row(
  439. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  440. children: [
  441. Padding(
  442. padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
  443. child: Text(
  444. email,
  445. style: TextStyle(fontSize: 16),
  446. ),
  447. ),
  448. Expanded(child: SizedBox()),
  449. IconButton(
  450. icon: Icon(Icons.delete_forever),
  451. color: Colors.redAccent,
  452. onPressed: () async {
  453. final dialog = createProgressDialog(context, "please wait...");
  454. await dialog.show();
  455. try {
  456. await CollectionsService.instance.unshare(collection.id, email);
  457. collection.sharees.removeWhere((user) => user.email == email);
  458. await dialog.hide();
  459. showToast("stopped sharing with " + email + ".");
  460. Navigator.of(context).pop();
  461. } catch (e, s) {
  462. Logger("EmailItemWidget").severe(e, s);
  463. await dialog.hide();
  464. showGenericErrorDialog(context);
  465. }
  466. },
  467. ),
  468. ],
  469. );
  470. }
  471. }