💄 Swipe style post shuffle
This commit is contained in:
@@ -924,6 +924,7 @@
|
|||||||
"fileHash": "File Hash",
|
"fileHash": "File Hash",
|
||||||
"exifData": "EXIF Data",
|
"exifData": "EXIF Data",
|
||||||
"postShuffle": "Shuffle Posts",
|
"postShuffle": "Shuffle Posts",
|
||||||
|
"swipeToExplore": "Swipe to explore",
|
||||||
"leveling": "Leveling",
|
"leveling": "Leveling",
|
||||||
"levelingHistory": "Leveling History",
|
"levelingHistory": "Leveling History",
|
||||||
"stellarProgram": "Stellar Program",
|
"stellarProgram": "Stellar Program",
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
builder: (context, state) => const AboutScreen(),
|
builder: (context, state) => const AboutScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// File routes
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'fileDetail',
|
name: 'fileDetail',
|
||||||
path: '/files/:id',
|
path: '/files/:id',
|
||||||
@@ -184,6 +185,34 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return const SizedBox.shrink();
|
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(
|
GoRoute(
|
||||||
name: 'postDetail',
|
name: 'postDetail',
|
||||||
path: '/posts/:id',
|
path: '/posts/:id',
|
||||||
@@ -234,35 +263,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
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(
|
GoRoute(
|
||||||
name: 'discoveryRealms',
|
name: 'discoveryRealms',
|
||||||
path: '/discovery/realms',
|
path: '/discovery/realms',
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/post/post_list.dart';
|
import 'package:island/pods/post/post_list.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/post/post_item.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';
|
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 {
|
class PostShuffleScreen extends HookConsumerWidget {
|
||||||
const PostShuffleScreen({super.key});
|
const PostShuffleScreen({super.key});
|
||||||
|
|
||||||
@@ -24,100 +32,159 @@ class PostShuffleScreen extends HookConsumerWidget {
|
|||||||
final postListState = ref.watch(postListProvider(cfg));
|
final postListState = ref.watch(postListProvider(cfg));
|
||||||
final postListNotifier = ref.watch(postListProvider(cfg).notifier);
|
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(() {
|
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(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('postShuffle').tr()),
|
appBar: AppBar(title: const Text('postShuffle').tr()),
|
||||||
body: Stack(
|
body: Builder(
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom:
|
|
||||||
kBottomControlHeight + MediaQuery.of(context).padding.bottom,
|
|
||||||
),
|
|
||||||
child: Builder(
|
|
||||||
key: ValueKey(postListState.value?.items.length ?? 0),
|
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final items = postListState.value?.items ?? [];
|
if (items.isEmpty) {
|
||||||
if (items.isNotEmpty) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
return CardSwiper(
|
}
|
||||||
controller: cardSwiperController,
|
|
||||||
cardsCount: items.length,
|
return Stack(
|
||||||
isLoop: false,
|
children: [
|
||||||
cardBuilder:
|
PageView.builder(
|
||||||
(
|
controller: pageController,
|
||||||
context,
|
scrollDirection: Axis.vertical,
|
||||||
index,
|
itemCount: items.length,
|
||||||
horizontalOffsetPercentage,
|
itemBuilder: (context, index) {
|
||||||
verticalOffsetPercentage,
|
return SingleChildScrollView(
|
||||||
) {
|
|
||||||
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(
|
child: PostActionableItem(
|
||||||
item: items[index],
|
item: items[index],
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 8,
|
||||||
),
|
vertical: 8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onEnd: () async {
|
|
||||||
if (!postListNotifier.fetchedAll) {
|
|
||||||
postListNotifier.fetchFurther();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
decoration: BoxDecoration(
|
||||||
padding: EdgeInsets.only(
|
gradient: LinearGradient(
|
||||||
bottom: MediaQuery.of(context).padding.bottom,
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.black.withOpacity(0.3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 12,
|
||||||
|
bottom: 16,
|
||||||
),
|
),
|
||||||
height: kBottomControlHeight,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
cardSwiperController.undo();
|
final currentPage = pageController.page?.round() ?? 0;
|
||||||
|
if (currentPage > 0) {
|
||||||
|
pageController.animateToPage(
|
||||||
|
currentPage - 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.arrow_left_alt),
|
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(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
cardSwiperController.swipe(CardSwiperDirection.right);
|
final currentPage = pageController.page?.round() ?? 0;
|
||||||
|
if (currentPage < items.length - 1) {
|
||||||
|
pageController.animateToPage(
|
||||||
|
currentPage + 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
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(all: 8).center(),
|
),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user