Merge pull request #864 from ente-io/manage_link_fixes
New TextInputDialog for enabling password lock on public link
This commit is contained in:
commit
72bcfa989f
4 changed files with 69 additions and 88 deletions
|
@ -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),
|
||||||
|
|
|
@ -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) {
|
||||||
|
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();
|
trailingWidget = const SizedBox.shrink();
|
||||||
|
}
|
||||||
} else if (executionState == ExecutionState.inProgress) {
|
} else if (executionState == ExecutionState.inProgress) {
|
||||||
trailingWidget = EnteLoadingWidget(
|
trailingWidget = EnteLoadingWidget(
|
||||||
color: colorScheme.strokeMuted,
|
color: colorScheme.strokeMuted,
|
||||||
|
|
|
@ -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 {
|
||||||
|
if (password.trim().isNotEmpty) {
|
||||||
|
final propToUpdate =
|
||||||
|
await _getEncryptedPassword(
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
await _updateUrlSettings(
|
||||||
|
context,
|
||||||
|
propToUpdate,
|
||||||
|
showProgressDialog: false,
|
||||||
);
|
);
|
||||||
await _updateUrlSettings(context, propToUpdate);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue