file_caption_widget.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import 'package:flutter/material.dart';
  2. import "package:photos/generated/l10n.dart";
  3. import 'package:photos/models/file.dart';
  4. import 'package:photos/theme/ente_theme.dart';
  5. import 'package:photos/ui/components/keyboard/keybiard_oveylay.dart';
  6. import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
  7. import 'package:photos/utils/magic_util.dart';
  8. class FileCaptionReadyOnly extends StatelessWidget {
  9. final String caption;
  10. const FileCaptionReadyOnly({super.key, required this.caption});
  11. @override
  12. Widget build(BuildContext context) {
  13. final colorScheme = getEnteColorScheme(context);
  14. final textTheme = getEnteTextTheme(context);
  15. return Padding(
  16. padding: const EdgeInsets.all(4.0),
  17. child: ConstrainedBox(
  18. constraints: const BoxConstraints(
  19. minHeight: 32.0,
  20. minWidth: double.infinity,
  21. maxHeight: 200.0,
  22. maxWidth: double.infinity,
  23. ),
  24. child: DecoratedBox(
  25. decoration: BoxDecoration(
  26. color: colorScheme.fillFaint,
  27. borderRadius: BorderRadius.circular(4),
  28. ),
  29. child: SingleChildScrollView(
  30. scrollDirection: Axis.vertical,
  31. child: Padding(
  32. padding: const EdgeInsets.all(12.0),
  33. child: Text(
  34. caption,
  35. style: textTheme.small,
  36. ),
  37. ),
  38. ),
  39. ),
  40. ),
  41. );
  42. }
  43. }
  44. class FileCaptionWidget extends StatefulWidget {
  45. final File file;
  46. const FileCaptionWidget({required this.file, super.key});
  47. @override
  48. State<FileCaptionWidget> createState() => _FileCaptionWidgetState();
  49. }
  50. class _FileCaptionWidgetState extends State<FileCaptionWidget> {
  51. static const int maxLength = 5000;
  52. // counterThreshold represents the nun of char after which
  53. // currentLength/maxLength will show up
  54. static const int counterThreshold = 1000;
  55. int currentLength = 0;
  56. final _textController = TextEditingController();
  57. final _focusNode = FocusNode();
  58. String? editedCaption;
  59. late String defaultHintText = S.of(context).fileInfoAddDescHint;
  60. String hintText = '';
  61. Widget? keyboardTopButtons;
  62. @override
  63. void initState() {
  64. _focusNode.addListener(_focusNodeListener);
  65. editedCaption = widget.file.caption;
  66. if (editedCaption != null && editedCaption!.isNotEmpty) {
  67. hintText = editedCaption!;
  68. }
  69. super.initState();
  70. }
  71. @override
  72. void dispose() {
  73. if (editedCaption != null) {
  74. editFileCaption(null, widget.file, editedCaption!);
  75. }
  76. _textController.dispose();
  77. _focusNode.removeListener(_focusNodeListener);
  78. super.dispose();
  79. }
  80. @override
  81. Widget build(BuildContext context) {
  82. if (hintText.isEmpty) {
  83. hintText = defaultHintText;
  84. }
  85. final colorScheme = getEnteColorScheme(context);
  86. final textTheme = getEnteTextTheme(context);
  87. return TextField(
  88. onSubmitted: (value) async {
  89. await _onDoneClick(context);
  90. },
  91. controller: _textController,
  92. focusNode: _focusNode,
  93. decoration: InputDecoration(
  94. counterStyle: textTheme.mini.copyWith(color: colorScheme.textMuted),
  95. counterText: currentLength >= counterThreshold
  96. ? currentLength.toString() + " / " + maxLength.toString()
  97. : "",
  98. contentPadding: const EdgeInsets.all(16),
  99. border: OutlineInputBorder(
  100. borderRadius: BorderRadius.circular(2),
  101. borderSide: const BorderSide(
  102. width: 0,
  103. style: BorderStyle.none,
  104. ),
  105. ),
  106. focusedBorder: OutlineInputBorder(
  107. borderRadius: BorderRadius.circular(2),
  108. borderSide: const BorderSide(
  109. width: 0,
  110. style: BorderStyle.none,
  111. ),
  112. ),
  113. filled: true,
  114. fillColor: colorScheme.fillFaint,
  115. hintText: hintText,
  116. hintStyle: hintText == defaultHintText
  117. ? textTheme.small.copyWith(color: colorScheme.textMuted)
  118. : textTheme.small,
  119. ),
  120. style: textTheme.small,
  121. cursorWidth: 1.5,
  122. maxLength: maxLength,
  123. minLines: 1,
  124. maxLines: 10,
  125. textCapitalization: TextCapitalization.sentences,
  126. keyboardType: TextInputType.multiline,
  127. onChanged: (value) {
  128. setState(() {
  129. hintText = defaultHintText;
  130. currentLength = value.length;
  131. editedCaption = value;
  132. });
  133. },
  134. );
  135. }
  136. Future<void> _onDoneClick(BuildContext context) async {
  137. if (editedCaption != null) {
  138. final isSuccesful =
  139. await editFileCaption(context, widget.file, editedCaption!);
  140. if (isSuccesful) {
  141. if (mounted) {
  142. Navigator.pop(context);
  143. }
  144. }
  145. }
  146. }
  147. void onCancelTap() {
  148. _textController.text = widget.file.caption ?? '';
  149. _focusNode.unfocus();
  150. editedCaption = null;
  151. }
  152. void onDoneTap() {
  153. _focusNode.unfocus();
  154. _onDoneClick(context);
  155. }
  156. void _focusNodeListener() {
  157. final caption = widget.file.caption;
  158. if (_focusNode.hasFocus && caption != null) {
  159. _textController.text = caption;
  160. editedCaption = caption;
  161. }
  162. final bool hasFocus = _focusNode.hasFocus;
  163. keyboardTopButtons ??= KeyboardTopButton(
  164. onDoneTap: onDoneTap,
  165. onCancelTap: onCancelTap,
  166. );
  167. if (hasFocus) {
  168. KeyboardOverlay.showOverlay(context, keyboardTopButtons!);
  169. } else {
  170. KeyboardOverlay.removeOverlay();
  171. }
  172. }
  173. }