Merge pull request #146 from krawieck/saved-page

This commit is contained in:
Filip Krawczyk 2021-02-09 22:41:47 +01:00 committed by GitHub
commit efb2551f4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 69 deletions

View File

@ -1,3 +1,9 @@
## Unreleased
### Added
- Added page with saved posts/comments. It can be accessed from the profile tab under the bookmark icon
## v0.2.3 - 2021-02-09
Lemmur is now available on the [play store](https://play.google.com/store/apps/details?id=com.krawieck.lemmur) and [f-droid](https://f-droid.org/packages/com.krawieck.lemmur)

View File

@ -30,7 +30,7 @@ class CommunitiesListPage extends StatelessWidget {
),
body: SortableInfiniteList<CommunityView>(
fetcher: fetcher,
builder: (community) => Column(
itemBuilder: (community) => Column(
children: [
const Divider(),
CommunitiesListItem(

View File

@ -12,8 +12,7 @@ import '../hooks/stores.dart';
import '../util/goto.dart';
import '../widgets/bottom_modal.dart';
import '../widgets/infinite_scroll.dart';
import '../widgets/post.dart';
import '../widgets/post_list_options.dart';
import '../widgets/sortable_infinite_list.dart';
import 'add_account.dart';
import 'inbox.dart';
@ -262,13 +261,6 @@ class InfiniteHomeList extends HookWidget {
Widget build(BuildContext context) {
final accStore = useAccountsStore();
final sort = useState(SortType.active);
void changeSorting(SortType newSort) {
sort.value = newSort;
controller.clear();
}
/// fetches post from many instances at once and combines them into a single
/// list
///
@ -313,9 +305,9 @@ class InfiniteHomeList extends HookWidget {
return newPosts;
}
Future<List<PostView>> Function(int, int) fetcherFromInstance(
String instanceHost, PostListingType listingType, SortType sort) =>
(page, batchSize) => LemmyApiV2(instanceHost).run(GetPosts(
FetcherWithSorting<PostView> fetcherFromInstance(
String instanceHost, PostListingType listingType) =>
(page, batchSize, sort) => LemmyApiV2(instanceHost).run(GetPosts(
type: listingType,
sort: sort,
page: page,
@ -323,33 +315,13 @@ class InfiniteHomeList extends HookWidget {
auth: accStore.defaultTokenFor(instanceHost)?.raw,
));
return InfiniteScroll<PostView>(
prepend: Column(
children: [
PostListOptions(
sortValue: sort.value,
onSortChanged: changeSorting,
styleButton: onStyleChange != null,
),
],
),
builder: (post) => Column(
children: [
PostWidget(post),
const SizedBox(height: 20),
],
),
padding: EdgeInsets.zero,
fetchMore: selectedList.instanceHost == null
? (page, limit) =>
generalFetcher(page, limit, sort.value, selectedList.listingType)
return InfinitePostList(
fetcher: selectedList.instanceHost == null
? (page, limit, sort) =>
generalFetcher(page, limit, sort, selectedList.listingType)
: fetcherFromInstance(
selectedList.instanceHost,
selectedList.listingType,
sort.value,
),
selectedList.instanceHost, selectedList.listingType),
controller: controller,
batchSize: 20,
);
}
}

View File

@ -6,6 +6,7 @@ import '../hooks/stores.dart';
import '../util/goto.dart';
import '../widgets/radio_picker.dart';
import '../widgets/user_profile.dart';
import 'saved_page.dart';
import 'settings.dart';
/// Profile page for a logged in user. The difference between this and
@ -51,6 +52,10 @@ class UserProfileTab extends HookWidget {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
leading: IconButton(
onPressed: () => goTo(context, (context) => SavedPage()),
icon: const Icon(Icons.bookmark),
),
title: RadioPicker<String>(
title: 'account',
values: accountsStore.loggedInInstances

64
lib/pages/saved_page.dart Normal file
View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v2.dart';
import '../hooks/stores.dart';
import '../widgets/sortable_infinite_list.dart';
/// Page with saved posts/comments. Fetches such saved data from the default user
/// Assumes there is at least one logged in user
class SavedPage extends HookWidget {
@override
Widget build(BuildContext context) {
final accountStore = useAccountsStore();
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Saved'),
bottom: const TabBar(
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Comments'),
],
),
),
body: TabBarView(
children: [
InfinitePostList(
fetcher: (page, batchSize, sortType) =>
LemmyApiV2(accountStore.defaultInstanceHost)
.run(
GetUserDetails(
userId: accountStore.defaultToken.payload.id,
sort: sortType,
savedOnly: true,
page: page,
limit: batchSize,
auth: accountStore.defaultToken.raw,
),
)
.then((value) => value.posts),
),
InfiniteCommentList(
fetcher: (page, batchSize, sortType) =>
LemmyApiV2(accountStore.defaultInstanceHost)
.run(
GetUserDetails(
userId: accountStore.defaultToken.payload.id,
sort: sortType,
savedOnly: true,
page: page,
limit: batchSize,
auth: accountStore.defaultToken.raw,
),
)
.then((value) => value.comments),
),
],
),
),
);
}
}

View File

@ -103,7 +103,7 @@ class _SearchResultsList extends HookWidget {
throw UnimplementedError();
}
},
builder: (data) {
itemBuilder: (data) {
switch (type) {
case SearchType.comments:
return CommentWidget(

View File

@ -20,27 +20,45 @@ class InfiniteScrollController {
}
}
/// `ListView.builder` with asynchronous data fetching
/// `ListView.builder` with asynchronous data fetching and no `itemCount`
class InfiniteScroll<T> extends HookWidget {
/// How many items should be fetched per call
final int batchSize;
/// Widget displayed at the bottom when InfiniteScroll is fetching
final Widget loadingWidget;
final Widget Function(T data) builder;
final Future<List<T>> Function(int page, int batchSize) fetchMore;
/// Builds your widget from the fetched data
final Widget Function(T data) itemBuilder;
/// Fetches data to be displayed. It is important to respect `batchSize`,
/// if the returned list has less than `batchSize` then the InfiniteScroll
/// is considered finished
final Future<List<T>> Function(int page, int batchSize) fetcher;
final InfiniteScrollController controller;
final Widget prepend;
/// Widget to be added at the beginning of the list
final Widget leading;
/// Padding for the [ListView.builder]
final EdgeInsetsGeometry padding;
/// Widget that will be displayed if there are no items
final Widget noItems;
const InfiniteScroll({
this.batchSize = 10,
this.prepend = const SizedBox.shrink(),
this.leading = const SizedBox.shrink(),
this.padding,
this.loadingWidget =
const ListTile(title: Center(child: CircularProgressIndicator())),
@required this.builder,
@required this.fetchMore,
@required this.itemBuilder,
@required this.fetcher,
this.controller,
}) : assert(builder != null),
assert(fetchMore != null),
this.noItems = const SizedBox.shrink(),
}) : assert(itemBuilder != null),
assert(fetcher != null),
assert(batchSize > 0);
@override
@ -71,14 +89,19 @@ class InfiniteScroll<T> extends HookWidget {
},
child: ListView.builder(
padding: padding,
// +2 for the loading widget and prepend widget
// +2 for the loading widget and leading widget
itemCount: data.value.length + 2,
itemBuilder: (_, i) {
if (i == 0) {
return prepend;
return leading;
}
i -= 1;
// if we are done but we have no data it means the list is empty
if (!hasMore.current && data.value.isEmpty) {
return Center(child: noItems);
}
// reached the bottom, fetch more
if (i == data.value.length) {
// if there are no more, skip
@ -89,7 +112,7 @@ class InfiniteScroll<T> extends HookWidget {
// if it's already fetching more, skip
if (!isFetching.current) {
isFetching.current = true;
fetchMore(page, batchSize).then((newData) {
fetcher(page, batchSize).then((newData) {
// if got less than the batchSize, mark the list as done
if (newData.length < batchSize) {
hasMore.current = false;
@ -106,7 +129,7 @@ class InfiniteScroll<T> extends HookWidget {
}
// not last element, render list item
return builder(data.value[i]);
return itemBuilder(data.value[i]);
},
),
);

View File

@ -9,23 +9,31 @@ import 'infinite_scroll.dart';
import 'post.dart';
import 'post_list_options.dart';
typedef FetcherWithSorting<T> = Future<List<T>> Function(
int page, int batchSize, SortType sortType);
/// Infinite list of posts
class SortableInfiniteList<T> extends HookWidget {
final Future<List<T>> Function(int page, int batchSize, SortType sortType)
fetcher;
final Widget Function(T) builder;
final FetcherWithSorting<T> fetcher;
final Widget Function(T) itemBuilder;
final InfiniteScrollController controller;
final Function onStyleChange;
final Widget noItems;
const SortableInfiniteList({
@required this.fetcher,
@required this.builder,
@required this.itemBuilder,
this.controller,
this.onStyleChange,
this.noItems,
}) : assert(fetcher != null),
assert(builder != null);
assert(itemBuilder != null);
@override
Widget build(BuildContext context) {
final isc = useInfiniteScrollController();
final defaultController = useInfiniteScrollController();
final isc = controller ?? defaultController;
final sort = useState(SortType.active);
void changeSorting(SortType newSort) {
@ -34,50 +42,61 @@ class SortableInfiniteList<T> extends HookWidget {
}
return InfiniteScroll<T>(
prepend: PostListOptions(
leading: PostListOptions(
sortValue: sort.value,
onSortChanged: changeSorting,
styleButton: onStyleChange != null,
),
builder: builder,
itemBuilder: itemBuilder,
padding: EdgeInsets.zero,
fetchMore: (page, batchSize) => fetcher(page, batchSize, sort.value),
fetcher: (page, batchSize) => fetcher(page, batchSize, sort.value),
controller: isc,
batchSize: 20,
noItems: noItems,
);
}
}
class InfinitePostList extends StatelessWidget {
final Future<List<PostView>> Function(
int page, int batchSize, SortType sortType) fetcher;
final FetcherWithSorting<PostView> fetcher;
final InfiniteScrollController controller;
const InfinitePostList({@required this.fetcher}) : assert(fetcher != null);
const InfinitePostList({
@required this.fetcher,
this.controller,
}) : assert(fetcher != null);
Widget build(BuildContext context) => SortableInfiniteList<PostView>(
onStyleChange: () {},
builder: (post) => Column(
itemBuilder: (post) => Column(
children: [
PostWidget(post),
const SizedBox(height: 20),
],
),
fetcher: fetcher,
controller: controller,
noItems: const Text('there are no posts'),
);
}
class InfiniteCommentList extends StatelessWidget {
final Future<List<CommentView>> Function(
int page, int batchSize, SortType sortType) fetcher;
final FetcherWithSorting<CommentView> fetcher;
final InfiniteScrollController controller;
const InfiniteCommentList({@required this.fetcher}) : assert(fetcher != null);
const InfiniteCommentList({
@required this.fetcher,
this.controller,
}) : assert(fetcher != null);
Widget build(BuildContext context) => SortableInfiniteList<CommentView>(
builder: (comment) => CommentWidget(
itemBuilder: (comment) => CommentWidget(
CommentTree(comment),
postCreatorId: null,
detached: true,
),
fetcher: fetcher,
controller: controller,
noItems: const Text('there are no comments'),
);
}