diff --git a/lib/presentation/login/pages/login_page.dart b/lib/presentation/login/pages/login_page.dart index d3a11b8..3dbfac2 100644 --- a/lib/presentation/login/pages/login_page.dart +++ b/lib/presentation/login/pages/login_page.dart @@ -24,6 +24,8 @@ class _LocalLoginViewState extends State { @override Widget build(BuildContext context) { + bool keyboardIsOpen = MediaQuery.of(context).viewInsets.bottom != 0; + return Stack( children: [ Scaffold( @@ -41,31 +43,30 @@ class _LocalLoginViewState extends State { const LocalPathInput(), ], ), - floatingActionButton: BlocBuilder( - builder: (BuildContext context, TodoFileState state) { - return Visibility( - visible: state is! TodoFileLoading, - child: FloatingActionButton.extended( - heroTag: 'localUsage', - icon: const Icon(Icons.done), - label: const Text('Apply'), - tooltip: 'Apply', - onPressed: () async { - try { - setState(() => loading = true); - await context.read().loginLocal( - localTodoFile: File( - '${state.localPath}${Platform.pathSeparator}${state.todoFilename}', - ), - ); - } finally { - setState(() => loading = false); - } + floatingActionButton: keyboardIsOpen + ? null + : BlocBuilder( + builder: (BuildContext context, TodoFileState state) { + return FloatingActionButton.extended( + heroTag: 'localUsage', + icon: const Icon(Icons.done), + label: const Text('Apply'), + tooltip: 'Apply', + onPressed: () async { + try { + setState(() => loading = true); + await context.read().loginLocal( + localTodoFile: File( + '${state.localPath}${Platform.pathSeparator}${state.todoFilename}', + ), + ); + } finally { + setState(() => loading = false); + } + }, + ); }, ), - ); - }, - ), ), if (loading) const Opacity( @@ -375,98 +376,104 @@ class RemotePathInput extends StatefulWidget { } class _RemotePathInputState extends State { - late TextEditingController controller; + final TextEditingController controller = TextEditingController(); + final Debouncer debounce = Debouncer(milliseconds: 1000); @override void initState() { super.initState(); - controller = TextEditingController(); } @override void dispose() { controller.dispose(); + debounce.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + // Initial value + controller.text = context.read().state.remotePath; return BlocBuilder( builder: (BuildContext context, TodoFileState state) { - controller.text = state.remotePath; return ListTile( - leading: const Icon(Icons.folder), - title: TextFormField( - controller: controller, - style: Theme.of(context).textTheme.bodyMedium, - decoration: const InputDecoration( - labelText: 'Remote path', - hintText: defaultRemoteTodoPath, + leading: const Icon(Icons.folder), + title: TextFormField( + controller: controller, + style: Theme.of(context).textTheme.bodyMedium, + decoration: const InputDecoration( + labelText: 'Remote path', + hintText: defaultRemoteTodoPath, + ), + onChanged: (String value) async { + debounce.run(() async => await _save(context, value)); + }, ), - onChanged: (String value) => - context.read().updateRemotePath(value), - ), - trailing: state is! TodoFileLoading - ? IconButton( - icon: const Icon(Icons.help_outline), - onPressed: () => InfoDialog.dialog( - context: context, - title: 'Remote path', - message: - 'This path is appended to the base url of the server connection. This makes it possible to define a user-defined path for the todo files.', - ), - ) - : IconButton( - icon: const Icon(Icons.save), - onPressed: () async { - if (controller.text.isEmpty) { - SnackBarHandler.info( - context, - 'Empty remote path is not allowed. Using default one.', - ); - await context - .read() - .saveRemotePath(defaultRemoteTodoPath); - } else { - await context - .read() - .saveRemotePath(controller.text); - } - }, - ), - ); + trailing: IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () => InfoDialog.dialog( + context: context, + title: 'Remote path', + message: + 'This path is appended to the base url of the server connection. This makes it possible to define a user-defined path for the todo files.', + ), + )); }, ); } + + Future _save(BuildContext context, String value) async { + if (value.isEmpty) { + SnackBarHandler.info( + context, + 'Empty remote path is not allowed. Using default one.', + ); + await context.read().saveRemotePath(defaultRemoteTodoPath); + controller.value = controller.value.copyWith( + text: defaultRemoteTodoPath, + selection: + const TextSelection.collapsed(offset: defaultRemoteTodoPath.length), + ); + } else { + await context.read().saveRemotePath(value); + controller.value = controller.value.copyWith( + text: value, + selection: TextSelection.collapsed(offset: value.length), + ); + } + } } class TodoFilenameInput extends StatefulWidget { const TodoFilenameInput({super.key}); @override - State createState() => _LocalFilenameInputState(); + State createState() => _TodoFilenameInputState(); } -class _LocalFilenameInputState extends State { - late TextEditingController controller; +class _TodoFilenameInputState extends State { + final TextEditingController controller = TextEditingController(); + final Debouncer debounce = Debouncer(milliseconds: 1000); @override void initState() { super.initState(); - controller = TextEditingController(); } @override void dispose() { controller.dispose(); + debounce.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + // Initial value + controller.text = context.read().state.todoFilename; return BlocBuilder( builder: (BuildContext context, TodoFileState state) { - controller.text = state.todoFilename; return ListTile( leading: const Icon(Icons.description), title: TextFormField( @@ -476,31 +483,35 @@ class _LocalFilenameInputState extends State { labelText: 'Todo filename', hintText: defaultTodoFilename, ), - onChanged: (String value) => - context.read().updateTodoFilename(value), + onChanged: (String value) async { + debounce.run(() async => await _save(context, value)); + }, ), - trailing: state is! TodoFileLoading - ? null - : IconButton( - icon: const Icon(Icons.save), - onPressed: () async { - if (controller.text.isEmpty) { - SnackBarHandler.info( - context, - 'Empty todo filename is not allowed. Using default one.', - ); - await context - .read() - .saveLocalFilename(defaultTodoFilename); - } else { - await context - .read() - .saveLocalFilename(controller.text); - } - }, - ), ); }, ); } + + Future _save(BuildContext context, String value) async { + if (value.isEmpty) { + SnackBarHandler.info( + context, + 'Empty todo filename is not allowed. Using default one.', + ); + await context + .read() + .saveLocalFilename(defaultTodoFilename); + controller.value = controller.value.copyWith( + text: defaultTodoFilename, + selection: + const TextSelection.collapsed(offset: defaultTodoFilename.length), + ); + } else { + await context.read().saveLocalFilename(value); + controller.value = controller.value.copyWith( + text: value, + selection: TextSelection.collapsed(offset: value.length), + ); + } + } } diff --git a/lib/presentation/todo/widgets/todo_text_field.dart b/lib/presentation/todo/widgets/todo_text_field.dart index 4ae93b5..b1530a1 100644 --- a/lib/presentation/todo/widgets/todo_text_field.dart +++ b/lib/presentation/todo/widgets/todo_text_field.dart @@ -39,7 +39,7 @@ class _TodoStringTextFieldState extends State { text: state.todo.description, selection: TextSelection.fromPosition( TextPosition( - offset: base > state.todo.description.length + offset: base < 0 || base > state.todo.description.length ? state.todo.description.length : base, ), diff --git a/lib/presentation/todo_file/todo_file_cubit.dart b/lib/presentation/todo_file/todo_file_cubit.dart index 9084336..8e0a90b 100644 --- a/lib/presentation/todo_file/todo_file_cubit.dart +++ b/lib/presentation/todo_file/todo_file_cubit.dart @@ -95,17 +95,14 @@ class TodoFileCubit extends Cubit { } } - Future updateLocalPath(String? value) async { - if (value != null) { - log.fine('Updating \'localPath\'.'); - emit(state.load(localPath: value)); - } - } - Future saveLocalPath(String? value) async { try { if (value != null) { + emit(state.load()); log.fine('Saving setting \'localPath\'.'); + if (!value.endsWith('/')) { + value = '$value/'; + } await repository.updateOrInsert( Setting(key: 'localPath', value: value), ); @@ -121,17 +118,12 @@ class TodoFileCubit extends Cubit { } } - Future updateTodoFilename(String? value) async { - if (value != null) { - log.fine('Updating \'todoFilename\'.'); - emit(state.load(todoFilename: value)); - } - } - Future saveLocalFilename(String? value) async { try { if (value != null) { + emit(state.load()); log.fine('Saving setting \'todoFilename\'.'); + await repository.updateOrInsert( Setting(key: 'todoFilename', value: value), ); @@ -147,17 +139,14 @@ class TodoFileCubit extends Cubit { } } - Future updateRemotePath(String? value) async { - if (value != null) { - log.fine('Updating \'remotePath\'.'); - emit(state.load(remotePath: value)); - } - } - Future saveRemotePath(String? value) async { try { if (value != null) { + emit(state.load()); log.fine('Saving setting \'remotePath\'.'); + if (!value.endsWith('/')) { + value = '$value/'; + } await repository.updateOrInsert( Setting(key: 'remotePath', value: value), ); @@ -168,25 +157,9 @@ class TodoFileCubit extends Cubit { } } - Future resetTodoFileSettings() async { - try { - log.fine('Resetting todofile settings.'); - for (var k in [ - 'todoFilename', - 'localFilename', // @todo: Keep for backwards compatibility. - 'localPath', - 'remotePath', - ]) { - log.fine('Deleting setting \'$k\'.'); - await repository.delete(key: k); - } - } on Exception catch (e) { - emit(state.error(message: e.toString())); - } - } - Future resetToDefaults() async { try { + emit(state.load()); log.fine('Resetting to the defaults.'); await resetTodoFileSettings(); emit( @@ -201,4 +174,21 @@ class TodoFileCubit extends Cubit { emit(state.error(message: e.toString())); } } + + Future resetTodoFileSettings() async { + try { + log.fine('Resetting todofile settings.'); + for (var k in [ + 'todoFilename', + 'localFilename', // @todo: Keep for backwards compatibility. + 'localPath', + 'remotePath', + ]) { + log.fine('Deleting setting \'$k\'.'); + await repository.delete(key: k); + } + } on Exception catch (e) { + emit(state.error(message: e.toString())); + } + } } diff --git a/lib/presentation/todo_file/todo_file_state.dart b/lib/presentation/todo_file/todo_file_state.dart index a42f4ad..7b26ff9 100644 --- a/lib/presentation/todo_file/todo_file_state.dart +++ b/lib/presentation/todo_file/todo_file_state.dart @@ -68,7 +68,7 @@ sealed class TodoFileState extends Equatable { @override String toString() => - 'TodoFileState { localFile $localPath/$todoFilename remoteFile: $remotePath/$todoFilename }'; + 'TodoFileState { localFile $localPath$todoFilename remoteFile: $remotePath$todoFilename }'; } final class TodoFileLoading extends TodoFileState { @@ -95,7 +95,7 @@ final class TodoFileLoading extends TodoFileState { @override String toString() => - 'TodoFileLoading { localFile $localPath/$todoFilename remoteFile: $remotePath/$todoFilename }'; + 'TodoFileLoading { localFile $localPath$todoFilename remoteFile: $remotePath$todoFilename }'; } final class TodoFileReady extends TodoFileState { @@ -122,7 +122,7 @@ final class TodoFileReady extends TodoFileState { @override String toString() => - 'TodoFileReady { localFile $localPath/$todoFilename remoteFile: $remotePath/$todoFilename }'; + 'TodoFileReady { localFile $localPath$todoFilename remoteFile: $remotePath$todoFilename }'; } final class TodoFileError extends TodoFileState { @@ -163,5 +163,5 @@ final class TodoFileError extends TodoFileState { @override String toString() => - 'TodoFileError { message $message localFile $localPath/$todoFilename remoteFile: $remotePath/$todoFilename }'; + 'TodoFileError { message $message localFile $localPath$todoFilename remoteFile: $remotePath/$todoFilename }'; }