Merge pull request #100 from krawieck/search-tab

This commit is contained in:
Marcin Wojnarowski 2021-01-10 14:54:04 +01:00 committed by GitHub
commit 114a494d7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 347 additions and 71 deletions

View File

@ -11,6 +11,7 @@ import 'pages/communities_tab.dart';
import 'pages/create_post.dart';
import 'pages/home_tab.dart';
import 'pages/profile_tab.dart';
import 'pages/search_tab.dart';
import 'stores/accounts_store.dart';
import 'stores/config_store.dart';
@ -95,7 +96,7 @@ class MyHomePage extends HookWidget {
static const List<Widget> pages = [
HomeTab(),
CommunitiesTab(),
TemporarySearchTab(), // TODO: search tab
SearchTab(),
UserProfileTab(),
];

View File

@ -36,38 +36,51 @@ class CommunitiesListPage extends StatelessWidget {
builder: (community) => Column(
children: [
const Divider(),
ListTile(
title: Text(community.name),
subtitle: community.description != null
? Opacity(
opacity: 0.5,
child: MarkdownText(
community.description,
instanceHost: community.instanceHost,
),
)
: null,
onTap: () => goToCommunity.byId(
context, community.instanceHost, community.id),
leading: community.icon != null
? CachedNetworkImage(
height: 50,
width: 50,
imageUrl: community.icon,
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover, image: imageProvider),
),
),
errorWidget: (_, __, ___) => const SizedBox(width: 50),
)
: const SizedBox(width: 50),
),
CommunitiesListItem(
community: community,
)
],
),
),
);
}
}
class CommunitiesListItem extends StatelessWidget {
final CommunityView community;
const CommunitiesListItem({Key key, @required this.community})
: assert(community != null),
super(key: key);
@override
Widget build(BuildContext context) => ListTile(
title: Text(community.name),
subtitle: community.description != null
? Opacity(
opacity: 0.7,
child: MarkdownText(
community.description,
instanceHost: community.instanceHost,
),
)
: null,
onTap: () =>
goToCommunity.byId(context, community.instanceHost, community.id),
leading: community.icon != null
? CachedNetworkImage(
height: 50,
width: 50,
imageUrl: community.icon,
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover, image: imageProvider),
),
),
errorWidget: (_, __, ___) => const SizedBox(width: 50),
)
: const SizedBox(width: 50),
);
}

View File

@ -218,10 +218,6 @@ class _AboutTab extends HookWidget {
: assert(communitiesFuture != null),
assert(instanceHost != null);
void goToUser(int id) {
print('GO TO USER $id');
}
void goToModLog() {
print('GO TO MODLOG');
}
@ -347,7 +343,7 @@ class _AboutTab extends HookWidget {
subtitle: e.bio != null
? MarkdownText(e.bio, instanceHost: instanceHost)
: null,
onTap: () => goToUser(e.id),
onTap: () => goToUser.byId(context, instanceHost, e.id),
leading: e.avatar != null
? CachedNetworkImage(
height: 50,

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import '../comment_tree.dart';
import '../hooks/stores.dart';
import '../widgets/comment.dart';
import '../widgets/post.dart';
import '../widgets/sortable_infinite_list.dart';
import 'communities_list.dart';
import 'users_list.dart';
class SearchResultsPage extends HookWidget {
final String instanceHost;
final String query;
SearchResultsPage({
@required this.instanceHost,
@required this.query,
}) : assert(instanceHost != null),
assert(query != null),
assert(instanceHost.isNotEmpty),
assert(query.isNotEmpty);
@override
Widget build(BuildContext context) => DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Looking for "$query"'),
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Comments'),
Tab(text: 'Users'),
Tab(text: 'Communities'),
],
),
),
body: TabBarView(
children: [
_SearchResultsList(
instanceHost: instanceHost,
query: query,
type: SearchType.posts),
_SearchResultsList(
instanceHost: instanceHost,
query: query,
type: SearchType.comments),
_SearchResultsList(
instanceHost: instanceHost,
query: query,
type: SearchType.users),
_SearchResultsList(
instanceHost: instanceHost,
query: query,
type: SearchType.communities),
],
),
),
);
}
class _SearchResultsList extends HookWidget {
final SearchType type;
final String query;
final String instanceHost;
const _SearchResultsList({
@required this.type,
@required this.query,
@required this.instanceHost,
}) : assert(type != null),
assert(query != null),
assert(instanceHost != null);
@override
Widget build(BuildContext context) {
final accStore = useAccountsStore();
return SortableInfiniteList(
fetcher: (page, batchSize, sort) async {
final s = await LemmyApi(instanceHost).v1.search(
q: query,
sort: sort,
type: type,
auth: accStore.defaultTokenFor(instanceHost)?.raw,
page: page,
limit: batchSize,
);
switch (s.type) {
case SearchType.comments:
return s.comments;
case SearchType.communities:
return s.communities;
case SearchType.posts:
return s.posts;
case SearchType.users:
return s.users;
default:
throw UnimplementedError();
}
},
builder: (data) {
switch (type) {
case SearchType.comments:
return Comment(
CommentTree(data as CommentView),
postCreatorId: null,
);
case SearchType.communities:
return CommunitiesListItem(community: data as CommunityView);
case SearchType.posts:
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Post(data as PostView),
);
case SearchType.users:
return UsersListItem(user: data as UserView);
default:
throw UnimplementedError();
}
},
);
}
}

130
lib/pages/search_tab.dart Normal file
View File

@ -0,0 +1,130 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../hooks/stores.dart';
import '../util/goto.dart';
import '../widgets/bottom_modal.dart';
import 'search_results.dart';
class SearchTab extends HookWidget {
const SearchTab();
@override
Widget build(BuildContext context) {
final searchInputController = useTextEditingController();
final accStore = useAccountsStore();
final instanceHost = useState(accStore.instances.first);
useValueListenable(searchInputController);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
body: GestureDetector(
onTapDown: (_) => primaryFocus.unfocus(),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
TextField(
controller: searchInputController,
textAlign: TextAlign.center,
decoration: InputDecoration(
fillColor: Colors.grey,
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: 'search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Text('instance:',
style: Theme.of(context).textTheme.subtitle1),
),
Expanded(
child: SelectInstanceButton(
instanceHost: instanceHost.value,
onChange: (s) => instanceHost.value = s,
),
),
],
),
if (searchInputController.text.isNotEmpty)
ElevatedButton(
onPressed: () => goTo(
context,
(c) => SearchResultsPage(
instanceHost: instanceHost.value,
query: searchInputController.text,
)),
child: const Text('search'),
)
],
),
),
);
}
}
class SelectInstanceButton extends HookWidget {
final ValueChanged<String> onChange;
final String instanceHost;
const SelectInstanceButton(
{@required this.onChange, @required this.instanceHost})
: assert(instanceHost != null);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final accStore = useAccountsStore();
return OutlinedButton(
onPressed: () async {
final val = await showModalBottomSheet<String>(
backgroundColor: Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => BottomModal(
child: Column(
children: [
for (final inst in accStore.instances)
ListTile(
leading: inst == instanceHost
? Icon(
Icons.radio_button_on,
color: theme.accentColor,
)
: const Icon(Icons.radio_button_off),
title: Text(inst),
onTap: () => Navigator.of(context).pop(inst),
)
],
),
));
if (val != null) {
onChange?.call(val);
}
},
style: OutlinedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
padding: const EdgeInsets.symmetric(horizontal: 15),
primary: theme.textTheme.bodyText1.color,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(instanceHost),
const Icon(Icons.arrow_drop_down),
],
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import '../util/goto.dart';
import '../widgets/markdown_text.dart';
/// Infinite list of Users fetched by the given fetcher
@ -13,11 +14,6 @@ class UsersListPage extends StatelessWidget {
: assert(users != null),
super(key: key);
// TODO: go to user
void goToUser(BuildContext context, int id) {
print('GO TO USER $id');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -31,38 +27,49 @@ class UsersListPage extends StatelessWidget {
iconTheme: theme.iconTheme,
),
body: ListView.builder(
itemBuilder: (context, i) => ListTile(
title: Text((users[i].preferredUsername == null ||
users[i].preferredUsername.isEmpty)
? '@${users[i].name}'
: users[i].preferredUsername),
subtitle: users[i].bio != null
? Opacity(
opacity: 0.5,
child: MarkdownText(
users[i].bio,
instanceHost: users[i].instanceHost,
),
)
: null,
onTap: () => goToUser(context, users[i].id),
leading: users[i].avatar != null
? CachedNetworkImage(
height: 50,
width: 50,
imageUrl: users[i].avatar,
errorWidget: (_, __, ___) =>
const SizedBox(height: 50, width: 50),
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover, image: imageProvider),
),
))
: const SizedBox(width: 50),
),
itemBuilder: (context, i) => UsersListItem(user: users[i]),
itemCount: users.length,
));
}
}
class UsersListItem extends StatelessWidget {
final UserView user;
const UsersListItem({Key key, @required this.user})
: assert(user != null),
super(key: key);
@override
Widget build(BuildContext context) => ListTile(
title: Text(
(user.preferredUsername == null || user.preferredUsername.isEmpty)
? '@${user.name}'
: user.preferredUsername),
subtitle: user.bio != null
? Opacity(
opacity: 0.5,
child: MarkdownText(
user.bio,
instanceHost: user.instanceHost,
),
)
: null,
onTap: () => goToUser.byId(context, user.instanceHost, user.id),
leading: user.avatar != null
? CachedNetworkImage(
height: 50,
width: 50,
imageUrl: user.avatar,
errorWidget: (_, __, ___) =>
const SizedBox(height: 50, width: 50),
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover, image: imageProvider),
),
))
: const SizedBox(width: 50),
);
}