💄 Swipe style post shuffle

This commit is contained in:
2026-01-02 17:49:14 +08:00
parent a44552f105
commit f1f5113b01
3 changed files with 184 additions and 115 deletions

View File

@@ -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",

View File

@@ -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',

View File

@@ -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(),
),
],
),
), ),
), ),
], ],
);
},
), ),
); );
} }