💄 Swipe style post shuffle
This commit is contained in:
@@ -924,6 +924,7 @@
|
||||
"fileHash": "File Hash",
|
||||
"exifData": "EXIF Data",
|
||||
"postShuffle": "Shuffle Posts",
|
||||
"swipeToExplore": "Swipe to explore",
|
||||
"leveling": "Leveling",
|
||||
"levelingHistory": "Leveling History",
|
||||
"stellarProgram": "Stellar Program",
|
||||
|
||||
@@ -169,6 +169,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
builder: (context, state) => const AboutScreen(),
|
||||
),
|
||||
|
||||
// File routes
|
||||
GoRoute(
|
||||
name: 'fileDetail',
|
||||
path: '/files/:id',
|
||||
@@ -184,6 +185,34 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
// Post routes
|
||||
GoRoute(
|
||||
name: 'postShuffle',
|
||||
path: '/posts/shuffle',
|
||||
builder: (context, state) => const PostShuffleScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postCategories',
|
||||
path: '/posts/categories',
|
||||
builder: (context, state) => const PostCategoriesListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postCategoryDetail',
|
||||
path: '/posts/categories/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(slug: slug, isCategory: true);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postTagDetail',
|
||||
path: '/posts/tags/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(slug: slug, isCategory: false);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postDetail',
|
||||
path: '/posts/:id',
|
||||
@@ -234,35 +263,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postShuffle',
|
||||
path: '/posts/shuffle',
|
||||
builder: (context, state) => const PostShuffleScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postCategories',
|
||||
path: '/posts/categories',
|
||||
builder: (context, state) => const PostCategoriesListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postCategoryDetail',
|
||||
path: '/posts/categories/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(slug: slug, isCategory: true);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postTagDetail',
|
||||
path: '/posts/tags/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(
|
||||
slug: slug,
|
||||
isCategory: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
name: 'discoveryRealms',
|
||||
path: '/discovery/realms',
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
const kShufflePostListId = 'shuffle';
|
||||
|
||||
class _ShufflePageNotifier extends Notifier<int> {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void updatePage(int page) => state = page;
|
||||
}
|
||||
|
||||
final _shufflePageProvider = NotifierProvider<_ShufflePageNotifier, int>(
|
||||
_ShufflePageNotifier.new,
|
||||
);
|
||||
|
||||
class PostShuffleScreen extends HookConsumerWidget {
|
||||
const PostShuffleScreen({super.key});
|
||||
|
||||
@@ -24,100 +32,159 @@ class PostShuffleScreen extends HookConsumerWidget {
|
||||
final postListState = ref.watch(postListProvider(cfg));
|
||||
final postListNotifier = ref.watch(postListProvider(cfg).notifier);
|
||||
|
||||
final cardSwiperController = useMemoized(() => CardSwiperController(), []);
|
||||
final savedPage = ref.watch(_shufflePageProvider);
|
||||
final pageNotifier = ref.watch(_shufflePageProvider.notifier);
|
||||
|
||||
final pageController = usePageController(initialPage: savedPage);
|
||||
|
||||
useEffect(() {
|
||||
return cardSwiperController.dispose;
|
||||
return pageController.dispose;
|
||||
}, []);
|
||||
|
||||
const kBottomControlHeight = 80.0;
|
||||
final items = postListState.value?.items ?? [];
|
||||
|
||||
useEffect(() {
|
||||
void listener() {
|
||||
if (!pageController.hasClients) return;
|
||||
final page = pageController.page?.round() ?? 0;
|
||||
if (page != savedPage) {
|
||||
pageNotifier.updatePage(page);
|
||||
}
|
||||
if (page >= items.length - 3 && !postListNotifier.fetchedAll) {
|
||||
postListNotifier.fetchFurther();
|
||||
}
|
||||
}
|
||||
|
||||
pageController.addListener(listener);
|
||||
return () => pageController.removeListener(listener);
|
||||
}, [items.length, postListNotifier.fetchedAll]);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('postShuffle').tr()),
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
kBottomControlHeight + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: Builder(
|
||||
key: ValueKey(postListState.value?.items.length ?? 0),
|
||||
builder: (context) {
|
||||
final items = postListState.value?.items ?? [];
|
||||
if (items.isNotEmpty) {
|
||||
return CardSwiper(
|
||||
controller: cardSwiperController,
|
||||
cardsCount: items.length,
|
||||
isLoop: false,
|
||||
cardBuilder:
|
||||
(
|
||||
context,
|
||||
index,
|
||||
horizontalOffsetPercentage,
|
||||
verticalOffsetPercentage,
|
||||
) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 540),
|
||||
child: SingleChildScrollView(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: PostActionableItem(
|
||||
item: items[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onEnd: () async {
|
||||
if (!postListNotifier.fetchedAll) {
|
||||
postListNotifier.fetchFurther();
|
||||
}
|
||||
},
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
if (items.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: pageController,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SingleChildScrollView(
|
||||
child: PostActionableItem(
|
||||
item: items[index],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
},
|
||||
),
|
||||
height: kBottomControlHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
cardSwiperController.undo();
|
||||
},
|
||||
icon: const Icon(Symbols.arrow_left_alt),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.3),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
cardSwiperController.swipe(CardSwiperDirection.right);
|
||||
},
|
||||
icon: const Icon(Symbols.arrow_right_alt),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
),
|
||||
],
|
||||
).padding(all: 8).center(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final currentPage = pageController.page?.round() ?? 0;
|
||||
if (currentPage > 0) {
|
||||
pageController.animateToPage(
|
||||
currentPage - 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_double_arrow_up,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
size: 20,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 4,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'swipeToExplore'.tr(),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 4,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final currentPage = pageController.page?.round() ?? 0;
|
||||
if (currentPage < items.length - 1) {
|
||||
pageController.animateToPage(
|
||||
currentPage + 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.keyboard_double_arrow_down,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
size: 20,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 4,
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user