share_collection_widget.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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. Padding(padding: EdgeInsets.all(8)),
  135. ]);
  136. if (widget.collection.publicURLs?.isNotEmpty ?? false) {
  137. children.add(Padding(
  138. padding: EdgeInsets.all(2),
  139. ));
  140. children.add(_getShareableUrlWidget(context));
  141. }
  142. }
  143. return AlertDialog(
  144. title: Text("sharing"),
  145. content: SingleChildScrollView(
  146. child: ListBody(
  147. children: <Widget>[
  148. Padding(
  149. padding: const EdgeInsets.all(4.0),
  150. child: Column(
  151. children: children,
  152. )),
  153. ],
  154. ),
  155. ),
  156. contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 4),
  157. );
  158. }
  159. Widget _getEmailField() {
  160. return Row(
  161. children: [
  162. Expanded(
  163. child: TypeAheadField(
  164. textFieldConfiguration: TextFieldConfiguration(
  165. keyboardType: TextInputType.emailAddress,
  166. decoration: InputDecoration(
  167. border: InputBorder.none,
  168. hintText: "email@your-friend.com",
  169. ),
  170. ),
  171. hideOnEmpty: true,
  172. loadingBuilder: (context) {
  173. return loadWidget;
  174. },
  175. suggestionsCallback: (pattern) async {
  176. _email = pattern;
  177. return PublicKeysDB.instance.searchByEmail(_email);
  178. },
  179. itemBuilder: (context, suggestion) {
  180. return Container(
  181. padding: EdgeInsets.fromLTRB(12, 8, 12, 8),
  182. child: Text(
  183. suggestion.email,
  184. overflow: TextOverflow.clip,
  185. ),
  186. );
  187. },
  188. onSuggestionSelected: (PublicKey suggestion) {
  189. _addEmailToCollection(suggestion.email,
  190. publicKey: suggestion.publicKey);
  191. },
  192. ),
  193. ),
  194. Padding(padding: EdgeInsets.all(8)),
  195. IconButton(
  196. icon: Icon(
  197. Icons.contact_mail_outlined,
  198. color: Theme.of(context).buttonColor.withOpacity(0.8),
  199. ),
  200. onPressed: () async {
  201. final emailContact = await FlutterContactPicker.pickEmailContact(
  202. askForPermission: true);
  203. _addEmailToCollection(emailContact.email.email);
  204. },
  205. ),
  206. ],
  207. );
  208. }
  209. Widget _getShareableUrlWidget(BuildContext parentContext) {
  210. String collectionKey = Base58Encode(
  211. CollectionsService.instance.getCollectionKey(widget.collection.id));
  212. String url = "${widget.collection.publicURLs.first.url}#$collectionKey";
  213. return SingleChildScrollView(
  214. child: Column(
  215. mainAxisAlignment: MainAxisAlignment.start,
  216. crossAxisAlignment: CrossAxisAlignment.start,
  217. children: [
  218. Padding(padding: EdgeInsets.all(4)),
  219. GestureDetector(
  220. onTap: () async {
  221. await Clipboard.setData(ClipboardData(text: url));
  222. showToast("link copied to clipboard");
  223. },
  224. child: Container(
  225. padding: EdgeInsets.all(16),
  226. child: Row(
  227. crossAxisAlignment: CrossAxisAlignment.end,
  228. children: [
  229. Flexible(
  230. child: Text(
  231. url,
  232. style: TextStyle(
  233. fontSize: 16,
  234. fontFeatures: const [FontFeature.tabularFigures()],
  235. color: Colors.white.withOpacity(0.68),
  236. overflow: TextOverflow.ellipsis,
  237. ),
  238. ),
  239. ),
  240. Padding(padding: EdgeInsets.all(2)),
  241. Icon(
  242. Icons.copy,
  243. size: 18,
  244. ),
  245. ],
  246. ),
  247. color: Colors.white.withOpacity(0.02),
  248. ),
  249. ),
  250. Padding(padding: EdgeInsets.all(2)),
  251. TextButton(
  252. child: Padding(
  253. padding: const EdgeInsets.all(12),
  254. child: Row(
  255. mainAxisAlignment: MainAxisAlignment.center,
  256. children: [
  257. Icon(
  258. Icons.adaptive.share,
  259. color: Theme.of(context).buttonColor,
  260. ),
  261. Padding(
  262. padding: EdgeInsets.all(4),
  263. ),
  264. Text(
  265. "share link",
  266. style: TextStyle(
  267. color: Theme.of(context).buttonColor,
  268. ),
  269. ),
  270. ],
  271. ),
  272. ),
  273. onPressed: () {
  274. shareText(url);
  275. },
  276. ),
  277. Padding(padding: EdgeInsets.all(4)),
  278. TextButton(
  279. child: Center(
  280. child: Text(
  281. "manage link",
  282. style: TextStyle(
  283. color: Colors.white70,
  284. decoration: TextDecoration.underline,
  285. ),
  286. ),
  287. ),
  288. onPressed: () async {
  289. routeToPage(
  290. parentContext,
  291. ManageSharedLinkWidget(collection: widget.collection),
  292. );
  293. },
  294. ),
  295. ],
  296. ),
  297. );
  298. }
  299. Future<void> _addEmailToCollection(
  300. String email, {
  301. String publicKey,
  302. }) async {
  303. if (!isValidEmail(email)) {
  304. showErrorDialog(context, "invalid email address",
  305. "please enter a valid email address.");
  306. return;
  307. } else if (email == Configuration.instance.getEmail()) {
  308. showErrorDialog(context, "oops", "you cannot share with yourself");
  309. return;
  310. } else if (widget.collection.sharees.any((user) => user.email == email)) {
  311. showErrorDialog(
  312. context, "oops", "you're already sharing this with " + email);
  313. return;
  314. }
  315. if (publicKey == null) {
  316. final dialog = createProgressDialog(context, "searching for user...");
  317. await dialog.show();
  318. publicKey = await UserService.instance.getPublicKey(email);
  319. await dialog.hide();
  320. }
  321. if (publicKey == null) {
  322. Navigator.of(context, rootNavigator: true).pop('dialog');
  323. final dialog = AlertDialog(
  324. title: Text("invite to ente?"),
  325. content: Text(
  326. "looks like " +
  327. email +
  328. " hasn't signed up for ente yet. would you like to invite them?",
  329. style: TextStyle(
  330. height: 1.4,
  331. ),
  332. ),
  333. actions: [
  334. TextButton(
  335. child: Text(
  336. "invite",
  337. style: TextStyle(
  338. color: Theme.of(context).buttonColor,
  339. ),
  340. ),
  341. onPressed: () {
  342. shareText(
  343. "Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.");
  344. },
  345. ),
  346. ],
  347. );
  348. showDialog(
  349. context: context,
  350. builder: (BuildContext context) {
  351. return dialog;
  352. },
  353. );
  354. } else {
  355. final dialog = createProgressDialog(context, "sharing...");
  356. await dialog.show();
  357. final collection = widget.collection;
  358. try {
  359. if (collection.type == CollectionType.folder) {
  360. final path =
  361. CollectionsService.instance.decryptCollectionPath(collection);
  362. if (!Configuration.instance.getPathsToBackUp().contains(path)) {
  363. await Configuration.instance.addPathToFoldersToBeBackedUp(path);
  364. }
  365. }
  366. await CollectionsService.instance
  367. .share(widget.collection.id, email, publicKey);
  368. await dialog.hide();
  369. showToast("shared successfully!");
  370. setState(() {
  371. _sharees.add(User(email: email));
  372. _showEntryField = false;
  373. });
  374. } catch (e) {
  375. await dialog.hide();
  376. if (e is SharingNotPermittedForFreeAccountsError) {
  377. _showUnSupportedAlert();
  378. } else {
  379. _logger.severe("failed to share collection", e);
  380. showGenericErrorDialog(context);
  381. }
  382. }
  383. }
  384. }
  385. void _showUnSupportedAlert() {
  386. AlertDialog alert = AlertDialog(
  387. title: Text("sorry"),
  388. content:
  389. Text("sharing is not permitted for free accounts, please subscribe"),
  390. actions: [
  391. TextButton(
  392. child: Text(
  393. "subscribe",
  394. style: TextStyle(
  395. color: Theme.of(context).buttonColor,
  396. ),
  397. ),
  398. onPressed: () {
  399. Navigator.of(context).pushReplacement(
  400. MaterialPageRoute(
  401. builder: (BuildContext context) {
  402. return getSubscriptionPage();
  403. },
  404. ),
  405. );
  406. },
  407. ),
  408. TextButton(
  409. child: Text(
  410. "ok",
  411. style: TextStyle(
  412. color: Colors.white,
  413. ),
  414. ),
  415. onPressed: () {
  416. Navigator.of(context, rootNavigator: true).pop();
  417. },
  418. ),
  419. ],
  420. );
  421. showDialog(
  422. context: context,
  423. builder: (BuildContext context) {
  424. return alert;
  425. },
  426. );
  427. }
  428. }
  429. class EmailItemWidget extends StatelessWidget {
  430. final Collection collection;
  431. final String email;
  432. const EmailItemWidget(
  433. this.collection,
  434. this.email, {
  435. Key key,
  436. }) : super(key: key);
  437. @override
  438. Widget build(BuildContext context) {
  439. return Row(
  440. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  441. children: [
  442. Padding(
  443. padding: const EdgeInsets.fromLTRB(8, 0, 0, 0),
  444. child: Text(
  445. email,
  446. style: TextStyle(fontSize: 16),
  447. ),
  448. ),
  449. Expanded(child: SizedBox()),
  450. IconButton(
  451. icon: Icon(Icons.delete_forever),
  452. color: Colors.redAccent,
  453. onPressed: () async {
  454. final dialog = createProgressDialog(context, "please wait...");
  455. await dialog.show();
  456. try {
  457. await CollectionsService.instance.unshare(collection.id, email);
  458. collection.sharees.removeWhere((user) => user.email == email);
  459. await dialog.hide();
  460. showToast("stopped sharing with " + email + ".");
  461. Navigator.of(context).pop();
  462. } catch (e, s) {
  463. Logger("EmailItemWidget").severe(e, s);
  464. await dialog.hide();
  465. showGenericErrorDialog(context);
  466. }
  467. },
  468. ),
  469. ],
  470. );
  471. }
  472. }