Merge pull request #146 from krawieck/saved-page
This commit is contained in:
commit
efb2551f4f
|
@ -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)
|
||||
|
|
|
@ -30,7 +30,7 @@ class CommunitiesListPage extends StatelessWidget {
|
|||
),
|
||||
body: SortableInfiniteList<CommunityView>(
|
||||
fetcher: fetcher,
|
||||
builder: (community) => Column(
|
||||
itemBuilder: (community) => Column(
|
||||
children: [
|
||||
const Divider(),
|
||||
CommunitiesListItem(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -103,7 +103,7 @@ class _SearchResultsList extends HookWidget {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
},
|
||||
builder: (data) {
|
||||
itemBuilder: (data) {
|
||||
switch (type) {
|
||||
case SearchType.comments:
|
||||
return CommentWidget(
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue