Merge pull request #208 from krawieck/feature/better-keyboard

This commit is contained in:
Filip Krawczyk 2021-04-11 22:04:24 +02:00 committed by GitHub
commit 6df83943fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 300 additions and 206 deletions

View File

@ -3,13 +3,18 @@
### Changed
- Disable commenting on locked posts
### Fixed
- When writing a comment the parent text is now selectable
- Text of a post is now selectable
- Enhanced keyboard experience
- appropriate keyboard types are opened
- correct capitalization
- added text input hints for things like password managers
- Account actions in settings are more obvious to access: long press an account/instance to see possible actions such as setting as default or removal
### Added
- When writing a comment, the parent text is now selectable
- Text of a post is now selectable
- Tapping outside of a text input hides the keyboard
## v0.4.1 - 2021-04-06
### Fixed

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:provider/provider.dart';
import 'hooks/stores.dart';
@ -41,7 +42,8 @@ class MyApp extends HookWidget {
Widget build(BuildContext context) {
final configStore = useConfigStore();
return MaterialApp(
return KeyboardDismisser(
child: MaterialApp(
title: 'lemmur',
supportedLocales: L10n.supportedLocales,
localizationsDelegates: L10n.localizationsDelegates,
@ -50,6 +52,7 @@ class MyApp extends HookWidget {
locale: configStore.locale,
theme: lightTheme,
home: const MyHomePage(),
),
);
}
}

View File

@ -24,6 +24,7 @@ class AddAccountPage extends HookWidget {
final usernameController = useListenable(useTextEditingController());
final passwordController = useListenable(useTextEditingController());
final passwordFocusNode = useFocusNode();
final accountsStore = useAccountsStore();
final loading = useDelayedLoading();
@ -54,12 +55,20 @@ class AddAccountPage extends HookWidget {
loading.cancel();
}
final handleSubmit =
usernameController.text.isEmpty || passwordController.text.isEmpty
? null
: loading.pending
? () {}
: handleOnAdd;
return Scaffold(
appBar: AppBar(
leading: const CloseButton(),
title: const Text('Add account'),
),
body: ListView(
body: AutofillGroup(
child: ListView(
padding: const EdgeInsets.all(15),
children: [
if (icon.value == null)
@ -105,26 +114,30 @@ class AddAccountPage extends HookWidget {
},
),
),
// TODO: add support for password managers
TextField(
autofocus: true,
controller: usernameController,
decoration:
InputDecoration(labelText: L10n.of(context)!.email_or_username),
autofillHints: const [
AutofillHints.email,
AutofillHints.username
],
onSubmitted: (_) => passwordFocusNode.requestFocus(),
decoration: InputDecoration(
labelText: L10n.of(context)!.email_or_username),
),
const SizedBox(height: 5),
TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(labelText: L10n.of(context)!.password),
focusNode: passwordFocusNode,
onSubmitted: (_) => handleSubmit?.call(),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.visiblePassword,
decoration:
InputDecoration(labelText: L10n.of(context)!.password),
),
ElevatedButton(
onPressed: usernameController.text.isEmpty ||
passwordController.text.isEmpty
? null
: loading.pending
? () {}
: handleOnAdd,
onPressed: handleSubmit,
child: !loading.loading
? const Text('Sign in')
: SizedBox(
@ -145,6 +158,7 @@ class AddAccountPage extends HookWidget {
),
],
),
),
);
}
}

View File

@ -47,7 +47,9 @@ class AddInstancePage extends HookWidget {
instanceController.removeListener(debounce);
};
}, []);
final inst = normalizeInstanceHost(instanceController.text);
handleOnAdd() async {
try {
await accountsStore.addInstance(inst, assumeValid: true);
@ -59,6 +61,8 @@ class AddInstancePage extends HookWidget {
}
}
final handleAdd = isSite.value == true ? handleOnAdd : null;
return Scaffold(
appBar: AppBar(
leading: const CloseButton(),
@ -97,6 +101,9 @@ class AddInstancePage extends HookWidget {
child: TextField(
autofocus: true,
controller: instanceController,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
onSubmitted: (_) => handleAdd?.call(),
autocorrect: false,
decoration: const InputDecoration(labelText: 'instance url'),
),
@ -108,7 +115,7 @@ class AddInstancePage extends HookWidget {
child: SizedBox(
height: 40,
child: ElevatedButton(
onPressed: isSite.value == true ? handleOnAdd : null,
onPressed: handleAdd,
child: !debounce.loading
? const Text('Add')
: SizedBox(

View File

@ -66,6 +66,9 @@ class CreatePostPage extends HookWidget {
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
final loggedInAction = useLoggedInAction(selectedInstance.value);
final titleFocusNode = useFocusNode();
final bodyFocusNode = useFocusNode();
final allCommunitiesSnap = useMemoFuture(
() => LemmyApiV3(selectedInstance.value)
.run(ListCommunities(
@ -155,77 +158,6 @@ class CreatePostPage extends HookWidget {
}
}
// TODO: use lazy autocomplete
final communitiesDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)))),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedCommunity.value?.community.id,
hint: Text(L10n.of(context)!.community),
onChanged: (communityId) => selectedCommunity.value =
allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId),
items: communitiesList(),
),
),
);
final url = Row(children: [
Expanded(
child: TextField(
enabled: pictrsDeleteToken.value == null,
controller: urlController,
decoration: InputDecoration(
labelText: L10n.of(context)!.url,
suffixIcon: const Icon(Icons.link),
),
),
),
const SizedBox(width: 5),
IconButton(
icon: imageUploadLoading.value
? const CircularProgressIndicator()
: Icon(pictrsDeleteToken.value == null
? Icons.add_photo_alternate
: Icons.close),
onPressed: pictrsDeleteToken.value == null
? loggedInAction(uploadPicture)
: () => removePicture(pictrsDeleteToken.value!),
tooltip:
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
)
]);
final title = TextField(
controller: titleController,
minLines: 1,
maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context)!.title),
);
final body = IndexedStack(
index: showFancy.value ? 1 : 0,
children: [
TextField(
controller: bodyController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 5,
decoration: InputDecoration(labelText: L10n.of(context)!.body),
),
Padding(
padding: const EdgeInsets.all(16),
child: MarkdownText(
bodyController.text,
instanceHost: selectedInstance.value,
),
),
],
);
handleSubmit(Jwt token) async {
if (selectedCommunity.value == null || titleController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
@ -256,6 +188,88 @@ class CreatePostPage extends HookWidget {
delayed.cancel();
}
// TODO: use lazy autocomplete
final communitiesDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)))),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: selectedCommunity.value?.community.id,
hint: Text(L10n.of(context)!.community),
onChanged: (communityId) => selectedCommunity.value =
allCommunitiesSnap.data
?.firstWhere((e) => e.community.id == communityId),
items: communitiesList(),
),
),
);
final url = Row(children: [
Expanded(
child: TextField(
enabled: pictrsDeleteToken.value == null,
controller: urlController,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
onSubmitted: (_) => titleFocusNode.requestFocus(),
decoration: InputDecoration(
labelText: L10n.of(context)!.url,
suffixIcon: const Icon(Icons.link),
),
),
),
const SizedBox(width: 5),
IconButton(
icon: imageUploadLoading.value
? const CircularProgressIndicator()
: Icon(pictrsDeleteToken.value == null
? Icons.add_photo_alternate
: Icons.close),
onPressed: pictrsDeleteToken.value == null
? loggedInAction(uploadPicture)
: () => removePicture(pictrsDeleteToken.value!),
tooltip:
pictrsDeleteToken.value == null ? 'Add picture' : 'Delete picture',
)
]);
final title = TextField(
controller: titleController,
focusNode: titleFocusNode,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.sentences,
onSubmitted: (_) => bodyFocusNode.requestFocus(),
minLines: 1,
maxLines: 2,
decoration: InputDecoration(labelText: L10n.of(context)!.title),
);
final body = IndexedStack(
index: showFancy.value ? 1 : 0,
children: [
TextField(
controller: bodyController,
focusNode: bodyFocusNode,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
onSubmitted: (_) =>
delayed.pending ? () {} : loggedInAction(handleSubmit),
maxLines: null,
minLines: 5,
decoration: InputDecoration(labelText: L10n.of(context)!.body),
),
Padding(
padding: const EdgeInsets.all(16),
child: MarkdownText(
bodyController.text,
instanceHost: selectedInstance.value,
),
),
],
);
return Scaffold(
appBar: AppBar(
leading: const CloseButton(),

View File

@ -91,6 +91,13 @@ class _ManageAccount extends HookWidget {
final deleteAccountPasswordController = useTextEditingController();
final bioFocusNode = useFocusNode();
final emailFocusNode = useFocusNode();
final matrixUserFocusNode = useFocusNode();
final newPasswordFocusNode = useFocusNode();
final verifyPasswordFocusNode = useFocusNode();
final oldPasswordFocusNode = useFocusNode();
final token = accountsStore.tokenFor(user.instanceHost, user.person.name)!;
handleSubmit() async {
@ -155,6 +162,8 @@ class _ManageAccount extends HookWidget {
const SizedBox(height: 10),
TextField(
controller: deleteAccountPasswordController,
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration:
InputDecoration(hintText: L10n.of(context)!.password),
@ -219,37 +228,64 @@ class _ManageAccount extends HookWidget {
),
const SizedBox(height: 8),
Text(L10n.of(context)!.display_name, style: theme.textTheme.headline6),
TextField(controller: displayNameController),
TextField(
controller: displayNameController,
onSubmitted: (_) => bioFocusNode.requestFocus(),
),
const SizedBox(height: 8),
Text(L10n.of(context)!.bio, style: theme.textTheme.headline6),
TextField(
controller: bioController,
focusNode: bioFocusNode,
textCapitalization: TextCapitalization.sentences,
onSubmitted: (_) => emailFocusNode.requestFocus(),
minLines: 4,
maxLines: 10,
),
const SizedBox(height: 8),
Text(L10n.of(context)!.email, style: theme.textTheme.headline6),
TextField(controller: emailController),
TextField(
focusNode: emailFocusNode,
controller: emailController,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => matrixUserFocusNode.requestFocus(),
),
const SizedBox(height: 8),
Text(L10n.of(context)!.matrix_user, style: theme.textTheme.headline6),
TextField(controller: matrixUserController),
TextField(
focusNode: matrixUserFocusNode,
controller: matrixUserController,
onSubmitted: (_) => newPasswordFocusNode.requestFocus(),
),
const SizedBox(height: 8),
Text(L10n.of(context)!.new_password, style: theme.textTheme.headline6),
TextField(
focusNode: newPasswordFocusNode,
controller: newPasswordController,
autofillHints: const [AutofillHints.newPassword],
keyboardType: TextInputType.visiblePassword,
obscureText: true,
onSubmitted: (_) => verifyPasswordFocusNode.requestFocus(),
),
const SizedBox(height: 8),
Text(L10n.of(context)!.verify_password,
style: theme.textTheme.headline6),
TextField(
focusNode: verifyPasswordFocusNode,
controller: newPasswordVerifyController,
autofillHints: const [AutofillHints.newPassword],
keyboardType: TextInputType.visiblePassword,
obscureText: true,
onSubmitted: (_) => oldPasswordFocusNode.requestFocus(),
),
const SizedBox(height: 8),
Text(L10n.of(context)!.old_password, style: theme.textTheme.headline6),
TextField(
focusNode: oldPasswordFocusNode,
controller: oldPasswordController,
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.visiblePassword,
obscureText: true,
),
const SizedBox(height: 8),

View File

@ -31,16 +31,26 @@ class SearchTab extends HookWidget {
);
}
handleSearch() => searchInputController.text.isNotEmpty
? goTo(
context,
(context) => SearchResultsPage(
instanceHost: instanceHost.value!,
query: searchInputController.text,
),
)
: null;
return Scaffold(
appBar: AppBar(),
body: GestureDetector(
onTapDown: (_) => primaryFocus?.unfocus(),
child: ListView(
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
TextField(
controller: searchInputController,
keyboardType: TextInputType.text,
textAlign: TextAlign.center,
onSubmitted: (_) => handleSearch(),
decoration: InputDecoration(hintText: L10n.of(context)!.search),
),
const SizedBox(height: 5),
@ -62,17 +72,11 @@ class SearchTab extends HookWidget {
),
if (searchInputController.text.isNotEmpty)
ElevatedButton(
onPressed: () => goTo(
context,
(c) => SearchResultsPage(
instanceHost: instanceHost.value!,
query: searchInputController.text,
)),
onPressed: handleSearch,
child: Text(L10n.of(context)!.search),
)
],
),
),
);
}
}

View File

@ -86,6 +86,7 @@ class WriteMessagePage extends HookWidget {
TextField(
controller: bodyController,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
maxLines: null,
minLines: 5,
autofocus: true,

View File

@ -97,6 +97,8 @@ class WriteComment extends HookWidget {
children: [
TextField(
controller: controller,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
autofocus: true,
minLines: 5,
maxLines: null,

View File

@ -392,6 +392,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
keyboard_dismisser:
dependency: "direct main"
description:
name: keyboard_dismisser
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
latinize:
dependency: transitive
description:

View File

@ -47,6 +47,7 @@ dependencies:
intl: ^0.17.0
matrix4_transform: ^2.0.0
json_annotation: ^4.0.1
keyboard_dismisser: ^2.0.0
flutter:
sdk: flutter