add_partipant_page.dart 12 KB

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