add_partipant_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import 'package:email_validator/email_validator.dart';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:photos/core/configuration.dart';
  5. import 'package:photos/models/collection.dart';
  6. import 'package:photos/services/collections_service.dart';
  7. import 'package:photos/theme/ente_theme.dart';
  8. import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
  9. import 'package:photos/ui/common/gradient_button.dart';
  10. import 'package:photos/ui/components/captioned_text_widget.dart';
  11. import 'package:photos/ui/components/divider_widget.dart';
  12. import 'package:photos/ui/components/menu_item_widget.dart';
  13. import 'package:photos/ui/components/menu_section_description_widget.dart';
  14. import 'package:photos/ui/components/menu_section_title.dart';
  15. import 'package:photos/ui/sharing/user_avator_widget.dart';
  16. import 'package:photos/utils/toast_util.dart';
  17. class AddParticipantPage extends StatefulWidget {
  18. final Collection collection;
  19. const AddParticipantPage(this.collection, {super.key});
  20. @override
  21. State<StatefulWidget> createState() => _AddParticipantPage();
  22. }
  23. class _AddParticipantPage extends State<AddParticipantPage> {
  24. late bool selectAsViewer;
  25. String selectedEmail = '';
  26. String _email = '';
  27. bool hideListOfEmails = false;
  28. bool _emailIsValid = false;
  29. bool isKeypadOpen = false;
  30. late CollectionActions collectionActions;
  31. // Focus nodes are necessary
  32. final textFieldFocusNode = FocusNode();
  33. final _textController = TextEditingController();
  34. @override
  35. void initState() {
  36. selectAsViewer = true;
  37. collectionActions = CollectionActions(CollectionsService.instance);
  38. super.initState();
  39. }
  40. @override
  41. void dispose() {
  42. _textController.dispose();
  43. super.dispose();
  44. }
  45. @override
  46. Widget build(BuildContext context) {
  47. isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
  48. final enteTextTheme = getEnteTextTheme(context);
  49. final List<User> suggestedUsers = _getSuggestedUser();
  50. hideListOfEmails = suggestedUsers.isEmpty;
  51. return Scaffold(
  52. resizeToAvoidBottomInset: isKeypadOpen,
  53. appBar: AppBar(
  54. title: const Text("Add people"),
  55. ),
  56. body: Column(
  57. mainAxisAlignment: MainAxisAlignment.start,
  58. crossAxisAlignment: CrossAxisAlignment.start,
  59. children: [
  60. const SizedBox(height: 12),
  61. Padding(
  62. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  63. child: Text(
  64. "Add a new email",
  65. style: enteTextTheme.body,
  66. ),
  67. ),
  68. const SizedBox(height: 4),
  69. Padding(
  70. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  71. child: _getEmailField(),
  72. ),
  73. (hideListOfEmails || isKeypadOpen)
  74. ? const Expanded(child: SizedBox())
  75. : Expanded(
  76. child: Padding(
  77. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  78. child: Column(
  79. children: [
  80. const SizedBox(height: 24),
  81. const MenuSectionTitle(
  82. title: "or pick an existing one",
  83. ),
  84. Expanded(
  85. child: ListView.builder(
  86. itemBuilder: (context, index) {
  87. final currentUser = suggestedUsers[index];
  88. return Column(
  89. children: [
  90. MenuItemWidget(
  91. captionedTextWidget: CaptionedTextWidget(
  92. title: currentUser.email,
  93. ),
  94. leadingIconSize: 24.0,
  95. leadingIconWidget: UserAvatarWidget(
  96. currentUser,
  97. type: AvatarType.mini,
  98. ),
  99. menuItemColor:
  100. getEnteColorScheme(context).fillFaint,
  101. pressedColor:
  102. getEnteColorScheme(context).fillFaint,
  103. trailingIcon:
  104. (selectedEmail == currentUser.email)
  105. ? Icons.check
  106. : null,
  107. onTap: () async {
  108. textFieldFocusNode.unfocus();
  109. if (selectedEmail == currentUser.email) {
  110. selectedEmail = '';
  111. } else {
  112. selectedEmail = currentUser.email;
  113. }
  114. setState(() => {});
  115. // showShortToast(context, "yet to implement");
  116. },
  117. isTopBorderRadiusRemoved: index > 0,
  118. isBottomBorderRadiusRemoved:
  119. index < (suggestedUsers.length - 1),
  120. ),
  121. (index == (suggestedUsers.length - 1))
  122. ? const SizedBox.shrink()
  123. : DividerWidget(
  124. dividerType: DividerType.menu,
  125. bgColor: getEnteColorScheme(context)
  126. .fillFaint,
  127. ),
  128. ],
  129. );
  130. },
  131. itemCount: suggestedUsers.length,
  132. // physics: const ClampingScrollPhysics(),
  133. ),
  134. ),
  135. ],
  136. ),
  137. ),
  138. ),
  139. const DividerWidget(
  140. dividerType: DividerType.solid,
  141. ),
  142. SafeArea(
  143. child: Padding(
  144. padding: const EdgeInsets.only(
  145. top: 8,
  146. bottom: 8,
  147. left: 16,
  148. right: 16,
  149. ),
  150. child: Column(
  151. crossAxisAlignment: CrossAxisAlignment.start,
  152. children: [
  153. const MenuSectionTitle(title: "Add as"),
  154. MenuItemWidget(
  155. captionedTextWidget: const CaptionedTextWidget(
  156. title: "Collaborator",
  157. ),
  158. leadingIcon: Icons.edit_outlined,
  159. menuItemColor: getEnteColorScheme(context).fillFaint,
  160. pressedColor: getEnteColorScheme(context).fillFaint,
  161. trailingIcon: !selectAsViewer ? Icons.check : null,
  162. onTap: () async {
  163. if (kDebugMode) {
  164. setState(() => {selectAsViewer = false});
  165. } else {
  166. showShortToast(context, "Coming soon...");
  167. }
  168. },
  169. isBottomBorderRadiusRemoved: true,
  170. ),
  171. DividerWidget(
  172. dividerType: DividerType.menu,
  173. bgColor: getEnteColorScheme(context).fillFaint,
  174. ),
  175. MenuItemWidget(
  176. captionedTextWidget: const CaptionedTextWidget(
  177. title: "Viewer",
  178. ),
  179. leadingIcon: Icons.photo_outlined,
  180. menuItemColor: getEnteColorScheme(context).fillFaint,
  181. pressedColor: getEnteColorScheme(context).fillFaint,
  182. trailingIcon: selectAsViewer ? Icons.check : null,
  183. onTap: () async {
  184. setState(() => {selectAsViewer = true});
  185. // showShortToast(context, "yet to implement");
  186. },
  187. isTopBorderRadiusRemoved: true,
  188. ),
  189. !isKeypadOpen
  190. ? const MenuSectionDescriptionWidget(
  191. content:
  192. "Collaborators can add photos and videos to the shared album.",
  193. )
  194. : const SizedBox.shrink(),
  195. const SizedBox(height: 12),
  196. SizedBox(
  197. width: double.infinity,
  198. child: GradientButton(
  199. onTap: (selectedEmail == '' && !_emailIsValid)
  200. ? null
  201. : () async {
  202. final emailToAdd =
  203. selectedEmail == '' ? _email : selectedEmail;
  204. final result =
  205. await collectionActions.addEmailToCollection(
  206. context,
  207. widget.collection,
  208. emailToAdd,
  209. role: selectAsViewer
  210. ? CollectionParticipantRole.viewer
  211. : CollectionParticipantRole.collaborator,
  212. );
  213. if (result != null && result && mounted) {
  214. Navigator.of(context).pop(true);
  215. }
  216. },
  217. text: selectAsViewer ? "Add viewer" : "Add collaborator",
  218. ),
  219. ),
  220. const SizedBox(height: 8),
  221. ],
  222. ),
  223. ),
  224. ),
  225. ],
  226. ),
  227. );
  228. }
  229. void clearFocus() {
  230. _textController.clear();
  231. _email = _textController.text;
  232. _emailIsValid = false;
  233. textFieldFocusNode.unfocus();
  234. setState(() => {});
  235. }
  236. Widget _getEmailField() {
  237. return TextFormField(
  238. controller: _textController,
  239. focusNode: textFieldFocusNode,
  240. style: getEnteTextTheme(context).body,
  241. autofillHints: const [AutofillHints.email],
  242. decoration: InputDecoration(
  243. focusedBorder: OutlineInputBorder(
  244. borderRadius: const BorderRadius.all(Radius.circular(4.0)),
  245. borderSide:
  246. BorderSide(color: getEnteColorScheme(context).strokeMuted),
  247. ),
  248. fillColor: getEnteColorScheme(context).fillFaint,
  249. filled: true,
  250. hintText: 'Enter email',
  251. contentPadding: const EdgeInsets.symmetric(
  252. horizontal: 16,
  253. vertical: 14,
  254. ),
  255. border: UnderlineInputBorder(
  256. borderSide: BorderSide.none,
  257. borderRadius: BorderRadius.circular(4),
  258. ),
  259. prefixIcon: Icon(
  260. Icons.email_outlined,
  261. color: getEnteColorScheme(context).strokeMuted,
  262. ),
  263. suffixIcon: _email == ''
  264. ? null
  265. : IconButton(
  266. onPressed: clearFocus,
  267. icon: Icon(
  268. Icons.cancel,
  269. color: getEnteColorScheme(context).strokeMuted,
  270. ),
  271. ),
  272. ),
  273. onChanged: (value) {
  274. if (selectedEmail != '') {
  275. selectedEmail = '';
  276. }
  277. _email = value.trim();
  278. if (_emailIsValid != EmailValidator.validate(_email)) {
  279. setState(() {
  280. _emailIsValid = EmailValidator.validate(_email);
  281. });
  282. } else if (_email.length < 2) {
  283. setState(() {});
  284. }
  285. },
  286. autocorrect: false,
  287. keyboardType: TextInputType.emailAddress,
  288. //initialValue: _email,
  289. textInputAction: TextInputAction.next,
  290. );
  291. }
  292. List<User> _getSuggestedUser() {
  293. final List<User> suggestedUsers = [];
  294. final Set<int> existingUserIDs = {};
  295. final int ownerID = Configuration.instance.getUserID()!;
  296. for (final User? u in widget.collection.sharees ?? []) {
  297. if (u != null && u.id != null) {
  298. existingUserIDs.add(u.id!);
  299. }
  300. }
  301. for (final c in CollectionsService.instance.getActiveCollections()) {
  302. if (c.owner?.id == ownerID) {
  303. for (final User? u in c.sharees ?? []) {
  304. if (u != null && u.id != null && !existingUserIDs.contains(u.id)) {
  305. existingUserIDs.add(u.id!);
  306. suggestedUsers.add(u);
  307. }
  308. }
  309. } else if (c.owner != null &&
  310. c.owner!.id != null &&
  311. !existingUserIDs.contains(c.owner!.id!)) {
  312. existingUserIDs.add(c.owner!.id!);
  313. suggestedUsers.add(c.owner!);
  314. }
  315. }
  316. suggestedUsers.sort((a, b) => a.email.compareTo(b.email));
  317. return suggestedUsers;
  318. }
  319. }