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

View file

@ -26,6 +26,7 @@ class TextInputWidget extends StatefulWidget {
final bool popNavAfterSubmission;
final bool shouldSurfaceExecutionStates;
final TextCapitalization? textCapitalization;
final bool isPasswordInput;
const TextInputWidget({
required this.onSubmit,
this.label,
@ -42,6 +43,7 @@ class TextInputWidget extends StatefulWidget {
this.popNavAfterSubmission = false,
this.shouldSurfaceExecutionStates = true,
this.textCapitalization = TextCapitalization.none,
this.isPasswordInput = false,
super.key,
});
@ -53,6 +55,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
ExecutionState executionState = ExecutionState.idle;
final _textController = TextEditingController();
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
///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),
);
}
_obscureTextNotifier = ValueNotifier(widget.isPasswordInput);
_obscureTextNotifier.addListener(_safeRefresh);
super.initState();
}
@override
void dispose() {
widget.submitNotifier?.removeListener(_onSubmit);
_obscureTextNotifier.dispose();
_textController.dispose();
super.dispose();
}
@ -105,6 +111,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
inputFormatters: widget.maxLength != null
? [LengthLimitingTextInputFormatter(50)]
: null,
obscureText: _obscureTextNotifier.value,
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted),
@ -134,6 +141,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
executionState: executionState,
shouldSurfaceExecutionStates:
widget.shouldSurfaceExecutionStates,
obscureTextNotifier: _obscureTextNotifier,
),
),
),
@ -186,6 +194,12 @@ class _TextInputWidgetState extends State<TextInputWidget> {
);
}
void _safeRefresh() {
if (mounted) {
setState(() {});
}
}
void _onSubmit() async {
_debouncer.run(
() => Future(() {
@ -279,9 +293,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
class SuffixIconWidget extends StatelessWidget {
final ExecutionState executionState;
final bool shouldSurfaceExecutionStates;
final ValueNotifier? obscureTextNotifier;
const SuffixIconWidget({
required this.executionState,
required this.shouldSurfaceExecutionStates,
this.obscureTextNotifier,
super.key,
});
@ -291,7 +307,21 @@ class SuffixIconWidget extends StatelessWidget {
final colorScheme = getEnteColorScheme(context);
if (executionState == ExecutionState.idle ||
!shouldSurfaceExecutionStates) {
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) {
trailingWidget = EnteLoadingWidget(
color: colorScheme.strokeMuted,

View file

@ -81,8 +81,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
context,
{'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",
);
}
setState(() {});
},
),
),
@ -190,23 +187,33 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
value: isPasswordEnabled,
onChanged: (enablePassword) async {
if (enablePassword) {
final inputResult =
await _displayLinkPasswordInput(context);
if (inputResult != null &&
inputResult == 'ok' &&
_textFieldController.text.trim().isNotEmpty) {
final propToUpdate = await _getEncryptedPassword(
_textFieldController.text,
showTextInputDialog(
context,
title: "Set a password",
submitButtonLabel: "Lock",
hintText: "Enter password",
isPasswordInput: true,
alwaysShowSuccessState: true,
onSubmit: (String password) async {
if (password.trim().isNotEmpty) {
final propToUpdate =
await _getEncryptedPassword(
password,
);
await _updateUrlSettings(
context,
propToUpdate,
showProgressDialog: false,
);
await _updateUrlSettings(context, propToUpdate);
}
},
);
} else {
await _updateUrlSettings(
context,
{'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 {
assert(
Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
@ -331,19 +272,24 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
}
Future<void> _updateUrlSettings(
BuildContext context,
Map<String, dynamic> prop,
) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
BuildContext context, Map<String, dynamic> prop,
{bool showProgressDialog = true}) async {
final dialog = showProgressDialog
? createProgressDialog(context, "Please wait...")
: null;
await dialog?.show();
try {
await CollectionsService.instance
.updateShareUrl(widget.collection!, prop);
await dialog.hide();
await dialog?.hide();
showShortToast(context, "Album updated");
if (mounted) {
setState(() {});
}
} catch (e) {
await dialog.hide();
await dialog?.hide();
await showGenericErrorDialog(context: context);
rethrow;
}
}
}

View file

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