Merge pull request #864 from ente-io/manage_link_fixes

New TextInputDialog for enabling password lock on public link
This commit is contained in:
Ashil 2023-02-13 16:37:58 +05:30 committed by GitHub
commit 72bcfa989f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 88 deletions

View file

@ -172,6 +172,7 @@ class TextInputDialog extends StatefulWidget {
final bool showOnlyLoadingState; final bool showOnlyLoadingState;
final TextCapitalization? textCapitalization; final TextCapitalization? textCapitalization;
final bool alwaysShowSuccessState; final bool alwaysShowSuccessState;
final bool isPasswordInput;
const TextInputDialog({ const TextInputDialog({
required this.title, required this.title,
this.body, this.body,
@ -188,6 +189,7 @@ class TextInputDialog extends StatefulWidget {
this.textCapitalization, this.textCapitalization,
this.showOnlyLoadingState = false, this.showOnlyLoadingState = false,
this.alwaysShowSuccessState = false, this.alwaysShowSuccessState = false,
this.isPasswordInput = false,
super.key, super.key,
}); });
@ -247,6 +249,7 @@ class _TextInputDialogState extends State<TextInputDialog> {
showOnlyLoadingState: widget.showOnlyLoadingState, showOnlyLoadingState: widget.showOnlyLoadingState,
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
alwaysShowSuccessState: widget.alwaysShowSuccessState, alwaysShowSuccessState: widget.alwaysShowSuccessState,
isPasswordInput: widget.isPasswordInput,
), ),
), ),
const SizedBox(height: 36), const SizedBox(height: 36),

View file

@ -26,6 +26,7 @@ class TextInputWidget extends StatefulWidget {
final bool popNavAfterSubmission; final bool popNavAfterSubmission;
final bool shouldSurfaceExecutionStates; final bool shouldSurfaceExecutionStates;
final TextCapitalization? textCapitalization; final TextCapitalization? textCapitalization;
final bool isPasswordInput;
const TextInputWidget({ const TextInputWidget({
required this.onSubmit, required this.onSubmit,
this.label, this.label,
@ -42,6 +43,7 @@ class TextInputWidget extends StatefulWidget {
this.popNavAfterSubmission = false, this.popNavAfterSubmission = false,
this.shouldSurfaceExecutionStates = true, this.shouldSurfaceExecutionStates = true,
this.textCapitalization = TextCapitalization.none, this.textCapitalization = TextCapitalization.none,
this.isPasswordInput = false,
super.key, super.key,
}); });
@ -53,6 +55,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
ExecutionState executionState = ExecutionState.idle; ExecutionState executionState = ExecutionState.idle;
final _textController = TextEditingController(); final _textController = TextEditingController();
final _debouncer = Debouncer(const Duration(milliseconds: 300)); final _debouncer = Debouncer(const Duration(milliseconds: 300));
late final ValueNotifier<bool> _obscureTextNotifier;
///This is to pass if the TextInputWidget is in a dialog and an error is ///This is to pass if the TextInputWidget is in a dialog and an error is
///thrown in executing onSubmit by passing it as arg in Navigator.pop() ///thrown in executing onSubmit by passing it as arg in Navigator.pop()
@ -68,12 +71,15 @@ class _TextInputWidgetState extends State<TextInputWidget> {
selection: TextSelection.collapsed(offset: widget.initialValue!.length), selection: TextSelection.collapsed(offset: widget.initialValue!.length),
); );
} }
_obscureTextNotifier = ValueNotifier(widget.isPasswordInput);
_obscureTextNotifier.addListener(_safeRefresh);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
widget.submitNotifier?.removeListener(_onSubmit); widget.submitNotifier?.removeListener(_onSubmit);
_obscureTextNotifier.dispose();
_textController.dispose(); _textController.dispose();
super.dispose(); super.dispose();
} }
@ -105,6 +111,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
inputFormatters: widget.maxLength != null inputFormatters: widget.maxLength != null
? [LengthLimitingTextInputFormatter(50)] ? [LengthLimitingTextInputFormatter(50)]
: null, : null,
obscureText: _obscureTextNotifier.value,
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.hintText, hintText: widget.hintText,
hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted), hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted),
@ -134,6 +141,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
executionState: executionState, executionState: executionState,
shouldSurfaceExecutionStates: shouldSurfaceExecutionStates:
widget.shouldSurfaceExecutionStates, widget.shouldSurfaceExecutionStates,
obscureTextNotifier: _obscureTextNotifier,
), ),
), ),
), ),
@ -186,6 +194,12 @@ class _TextInputWidgetState extends State<TextInputWidget> {
); );
} }
void _safeRefresh() {
if (mounted) {
setState(() {});
}
}
void _onSubmit() async { void _onSubmit() async {
_debouncer.run( _debouncer.run(
() => Future(() { () => Future(() {
@ -279,9 +293,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
class SuffixIconWidget extends StatelessWidget { class SuffixIconWidget extends StatelessWidget {
final ExecutionState executionState; final ExecutionState executionState;
final bool shouldSurfaceExecutionStates; final bool shouldSurfaceExecutionStates;
final ValueNotifier? obscureTextNotifier;
const SuffixIconWidget({ const SuffixIconWidget({
required this.executionState, required this.executionState,
required this.shouldSurfaceExecutionStates, required this.shouldSurfaceExecutionStates,
this.obscureTextNotifier,
super.key, super.key,
}); });
@ -291,7 +307,21 @@ class SuffixIconWidget extends StatelessWidget {
final colorScheme = getEnteColorScheme(context); final colorScheme = getEnteColorScheme(context);
if (executionState == ExecutionState.idle || if (executionState == ExecutionState.idle ||
!shouldSurfaceExecutionStates) { !shouldSurfaceExecutionStates) {
trailingWidget = const SizedBox.shrink(); if (obscureTextNotifier != null) {
trailingWidget = GestureDetector(
onTap: () {
obscureTextNotifier!.value = !obscureTextNotifier!.value;
},
child: Icon(
obscureTextNotifier!.value
? Icons.visibility_off_outlined
: Icons.visibility,
color: obscureTextNotifier!.value ? colorScheme.strokeMuted : null,
),
);
} else {
trailingWidget = const SizedBox.shrink();
}
} else if (executionState == ExecutionState.inProgress) { } else if (executionState == ExecutionState.inProgress) {
trailingWidget = EnteLoadingWidget( trailingWidget = EnteLoadingWidget(
color: colorScheme.strokeMuted, color: colorScheme.strokeMuted,

View file

@ -81,8 +81,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
context, context,
{'enableCollect': value}, {'enableCollect': value},
); );
setState(() {});
}, },
), ),
), ),
@ -170,7 +168,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
"Viewers can still take screenshots or save a copy of your photos using external tools", "Viewers can still take screenshots or save a copy of your photos using external tools",
); );
} }
setState(() {});
}, },
), ),
), ),
@ -190,23 +187,33 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
value: isPasswordEnabled, value: isPasswordEnabled,
onChanged: (enablePassword) async { onChanged: (enablePassword) async {
if (enablePassword) { if (enablePassword) {
final inputResult = showTextInputDialog(
await _displayLinkPasswordInput(context); context,
if (inputResult != null && title: "Set a password",
inputResult == 'ok' && submitButtonLabel: "Lock",
_textFieldController.text.trim().isNotEmpty) { hintText: "Enter password",
final propToUpdate = await _getEncryptedPassword( isPasswordInput: true,
_textFieldController.text, alwaysShowSuccessState: true,
); onSubmit: (String password) async {
await _updateUrlSettings(context, propToUpdate); if (password.trim().isNotEmpty) {
} final propToUpdate =
await _getEncryptedPassword(
password,
);
await _updateUrlSettings(
context,
propToUpdate,
showProgressDialog: false,
);
}
},
);
} else { } else {
await _updateUrlSettings( await _updateUrlSettings(
context, context,
{'disablePassword': true}, {'disablePassword': true},
); );
} }
setState(() {});
}, },
), ),
), ),
@ -242,72 +249,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
); );
} }
final TextEditingController _textFieldController = TextEditingController();
Future<String?> _displayLinkPasswordInput(BuildContext context) async {
_textFieldController.clear();
return showDialog<String>(
context: context,
builder: (context) {
bool passwordVisible = false;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('Enter password'),
content: TextFormField(
autofillHints: const [AutofillHints.newPassword],
decoration: InputDecoration(
hintText: "Password",
contentPadding: const EdgeInsets.all(12),
suffixIcon: IconButton(
icon: Icon(
passwordVisible ? Icons.visibility : Icons.visibility_off,
color: Colors.white.withOpacity(0.5),
size: 20,
),
onPressed: () {
passwordVisible = !passwordVisible;
setState(() {});
},
),
),
obscureText: !passwordVisible,
controller: _textFieldController,
autofocus: true,
autocorrect: false,
keyboardType: TextInputType.visiblePassword,
onChanged: (_) {
setState(() {});
},
),
actions: <Widget>[
TextButton(
child: Text(
'Cancel',
style: Theme.of(context).textTheme.subtitle2,
),
onPressed: () {
Navigator.pop(context, 'cancel');
},
),
TextButton(
child:
Text('Ok', style: Theme.of(context).textTheme.subtitle2),
onPressed: () {
if (_textFieldController.text.trim().isEmpty) {
return;
}
Navigator.pop(context, 'ok');
},
),
],
);
},
);
},
);
}
Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async { Future<Map<String, dynamic>> _getEncryptedPassword(String pass) async {
assert( assert(
Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault, Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
@ -331,19 +272,24 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
} }
Future<void> _updateUrlSettings( Future<void> _updateUrlSettings(
BuildContext context, BuildContext context, Map<String, dynamic> prop,
Map<String, dynamic> prop, {bool showProgressDialog = true}) async {
) async { final dialog = showProgressDialog
final dialog = createProgressDialog(context, "Please wait..."); ? createProgressDialog(context, "Please wait...")
await dialog.show(); : null;
await dialog?.show();
try { try {
await CollectionsService.instance await CollectionsService.instance
.updateShareUrl(widget.collection!, prop); .updateShareUrl(widget.collection!, prop);
await dialog.hide(); await dialog?.hide();
showShortToast(context, "Album updated"); showShortToast(context, "Album updated");
if (mounted) {
setState(() {});
}
} catch (e) { } catch (e) {
await dialog.hide(); await dialog?.hide();
await showGenericErrorDialog(context: context); await showGenericErrorDialog(context: context);
rethrow;
} }
} }
} }

View file

@ -272,6 +272,7 @@ Future<dynamic> showTextInputDialog(
bool showOnlyLoadingState = false, bool showOnlyLoadingState = false,
TextCapitalization textCapitalization = TextCapitalization.none, TextCapitalization textCapitalization = TextCapitalization.none,
bool alwaysShowSuccessState = false, bool alwaysShowSuccessState = false,
bool isPasswordInput = false,
}) { }) {
return showDialog( return showDialog(
barrierColor: backdropFaintDark, barrierColor: backdropFaintDark,
@ -298,6 +299,7 @@ Future<dynamic> showTextInputDialog(
showOnlyLoadingState: showOnlyLoadingState, showOnlyLoadingState: showOnlyLoadingState,
textCapitalization: textCapitalization, textCapitalization: textCapitalization,
alwaysShowSuccessState: alwaysShowSuccessState, alwaysShowSuccessState: alwaysShowSuccessState,
isPasswordInput: isPasswordInput,
), ),
), ),
); );