✨ Shuffle mode swiper
This commit is contained in:
@ -2,10 +2,9 @@ import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
|
||||
import '../providers/content/posts.dart';
|
||||
|
||||
class PostListController {
|
||||
class PostListController extends GetxController {
|
||||
/// The polling source modifier.
|
||||
/// - `0`: default recommendations
|
||||
/// - `1`: shuffle mode
|
||||
@ -46,14 +45,25 @@ class PostListController {
|
||||
}
|
||||
|
||||
RxBool isBusy = false.obs;
|
||||
RxBool isPreparing = false.obs;
|
||||
|
||||
RxInt focusCursor = 0.obs;
|
||||
Post get focusPost => postList[focusCursor.value];
|
||||
|
||||
RxInt postTotal = 0.obs;
|
||||
RxList<Post> postList = RxList.empty(growable: true);
|
||||
|
||||
RxInt nextPageKey = 0.obs;
|
||||
RxBool hasMore = true.obs;
|
||||
|
||||
Future<void> reloadAllOver() async {
|
||||
isPreparing.value = true;
|
||||
|
||||
focusCursor.value = 0;
|
||||
nextPageKey.value = 0;
|
||||
postList.clear();
|
||||
hasMore.value = true;
|
||||
|
||||
_resetPagingController();
|
||||
final result = await loadMore();
|
||||
if (result != null && hasMore.value) {
|
||||
@ -62,19 +72,26 @@ class PostListController {
|
||||
pagingController.appendLastPage(result);
|
||||
}
|
||||
_initPagingController();
|
||||
|
||||
isPreparing.value = false;
|
||||
}
|
||||
|
||||
Future<List<Post>?> loadMore() async {
|
||||
final result = await _loadPosts(nextPageKey.value);
|
||||
|
||||
if (result != null && result.length >= 10) {
|
||||
nextPageKey.value = nextPageKey.value + result.length;
|
||||
postList.addAll(result);
|
||||
nextPageKey.value += result.length;
|
||||
hasMore.value = true;
|
||||
} else if (result != null) {
|
||||
nextPageKey.value = nextPageKey.value + result.length;
|
||||
postList.addAll(result);
|
||||
nextPageKey.value += result.length;
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
final idx = <dynamic>{};
|
||||
postList.retainWhere((x) => idx.add(x.id));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -98,12 +115,14 @@ class PostListController {
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
|
||||
if (out != null) postList.addAll(out.cast<Post>());
|
||||
postTotal.value = result.count;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/feed/feed_list.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@ -19,19 +20,20 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final PostListController _postController = PostListController();
|
||||
|
||||
late final PostListController _postController;
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Get.lazyPut(() => PostListController());
|
||||
super.initState();
|
||||
_postController = Get.find();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
switch (_tabController.index) {
|
||||
case 0:
|
||||
case 1:
|
||||
if (_postController.mode.value == _tabController.index) break;
|
||||
if (_postController.mode.value == _tabController.index) return;
|
||||
_postController.mode.value = _tabController.index;
|
||||
_postController.reloadAllOver();
|
||||
}
|
||||
@ -42,57 +44,69 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PostCreatePopup(),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
title: AppBarTitle('home'.tr),
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: 'postListNews'.tr),
|
||||
Tab(text: 'postListShuffle'.tr),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PostCreatePopup(),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
title: AppBarTitle('home'.tr),
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
FeedListWidget(controller: _postController.pagingController),
|
||||
]),
|
||||
),
|
||||
CustomScrollView(slivers: [
|
||||
FeedListWidget(controller: _postController.pagingController),
|
||||
]),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: 'postListNews'.tr),
|
||||
Tab(text: 'postListShuffle'.tr),
|
||||
],
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
body: Obx(() {
|
||||
if (_postController.isPreparing.isTrue) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
FeedListWidget(
|
||||
controller: _postController.pagingController),
|
||||
]),
|
||||
),
|
||||
PostShuffleSwiper(controller: _postController),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -182,7 +182,10 @@ class _PostItemState extends State<PostItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAttachment = item.body['attachments']?.isNotEmpty ?? false;
|
||||
final List<int> attachments = item.body['attachments'] is List
|
||||
? item.body['attachments']?.cast<int>()
|
||||
: List.empty();
|
||||
final hasAttachment = attachments.isNotEmpty;
|
||||
|
||||
if (widget.isCompact) {
|
||||
return Column(
|
||||
@ -199,7 +202,7 @@ class _PostItemState extends State<PostItem> {
|
||||
bottom: hasAttachment ? 4 : 0,
|
||||
),
|
||||
buildFooter().paddingOnly(left: 16),
|
||||
if (item.body['attachments']?.isNotEmpty ?? false)
|
||||
if (attachments.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
@ -209,7 +212,7 @@ class _PostItemState extends State<PostItem> {
|
||||
).paddingOnly(right: 6),
|
||||
Text(
|
||||
'postAttachmentTip'.trParams(
|
||||
{'count': item.body['attachments']!.length.toString()},
|
||||
{'count': attachments.length.toString()},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
)
|
||||
@ -293,8 +296,7 @@ class _PostItemState extends State<PostItem> {
|
||||
),
|
||||
child: AttachmentList(
|
||||
parentId: widget.item.id.toString(),
|
||||
attachmentsId:
|
||||
item.body['attachments']?.cast<int>() ?? List.empty(),
|
||||
attachmentsId: attachments,
|
||||
divided: true,
|
||||
),
|
||||
),
|
||||
|
62
lib/widgets/posts/post_shuffle_swiper.dart
Normal file
62
lib/widgets/posts/post_shuffle_swiper.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/widgets/posts/post_single_display.dart';
|
||||
|
||||
class PostShuffleSwiper extends StatefulWidget {
|
||||
final PostListController controller;
|
||||
|
||||
const PostShuffleSwiper({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
State<PostShuffleSwiper> createState() => _PostShuffleSwiperState();
|
||||
}
|
||||
|
||||
class _PostShuffleSwiperState extends State<PostShuffleSwiper> {
|
||||
final CardSwiperController _swiperController = CardSwiperController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => CardSwiper(
|
||||
initialIndex: 0,
|
||||
isLoop: false,
|
||||
controller: _swiperController,
|
||||
cardsCount: widget.controller.postTotal.value,
|
||||
numberOfCardsDisplayed: 2,
|
||||
allowedSwipeDirection: const AllowedSwipeDirection.symmetric(
|
||||
horizontal: true,
|
||||
),
|
||||
cardBuilder: (context, index, percentThresholdX, percentThresholdY) {
|
||||
if (widget.controller.postList.length <= index) {
|
||||
return Card(
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
).paddingAll(24),
|
||||
);
|
||||
}
|
||||
final element = widget.controller.postList[index];
|
||||
return PostSingleDisplay(
|
||||
key: Key('p${element.id}'),
|
||||
item: element,
|
||||
);
|
||||
},
|
||||
padding: const EdgeInsets.all(24),
|
||||
onSwipe: (prevIndex, currIndex, dir) {
|
||||
if (prevIndex + 2 >= widget.controller.postList.length) {
|
||||
// Automatically load more
|
||||
widget.controller.loadMore();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
).paddingOnly(bottom: MediaQuery.of(context).padding.bottom),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_swiperController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
24
lib/widgets/posts/post_single_display.dart
Normal file
24
lib/widgets/posts/post_single_display.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
|
||||
class PostSingleDisplay extends StatelessWidget {
|
||||
final Post item;
|
||||
|
||||
const PostSingleDisplay({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Card(
|
||||
child: SingleChildScrollView(
|
||||
child: PostItem(
|
||||
item: item,
|
||||
).paddingSymmetric(horizontal: 10, vertical: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user