main.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:flutter_cache_manager/flutter_cache_manager.dart';
  4. import 'package:veloe_kemono_party_flutter/kemono_client.dart';
  5. import 'package:veloe_kemono_party_flutter/models/creator.dart';
  6. import 'package:veloe_kemono_party_flutter/models/post.dart';
  7. import 'package:veloe_kemono_party_flutter/pages/posts_screen.dart';
  8. import 'package:veloe_kemono_party_flutter/pages/widgets/creator_card.dart';
  9. import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
  10. import 'package:veloe_kemono_party_flutter/posts_notifier.dart';
  11. // Providers
  12. final searchQueryProvider = StateProvider<String>((ref) => '');
  13. final selectedCreatorProvider = StateProvider<Creator?>((ref) => null);
  14. final postsPageProvider = StateProvider<int>((ref) => 0);
  15. final kemonoClientProvider = Provider<KemonoClient>((ref) => KemonoClient());
  16. final creatorsProvider = FutureProvider<List<Creator>>((ref) async {
  17. final query = ref.watch(searchQueryProvider);
  18. final client = ref.read(kemonoClientProvider);
  19. final creators = await client.getCreators();
  20. return creators.where((creator) =>
  21. creator.name.toLowerCase().contains(query.toLowerCase()) ||
  22. creator.service.toLowerCase().contains(query.toLowerCase())
  23. ).toList();
  24. });
  25. final postsProvider = StateNotifierProvider.autoDispose
  26. .family<PostsNotifier, AsyncValue<List<Post>>, (String, String)>((ref, args) {
  27. final (creatorId, service) = args;
  28. return PostsNotifier(
  29. creatorId: creatorId,
  30. service: service,
  31. client: ref.read(kemonoClientProvider),
  32. );
  33. });
  34. // Image Caching
  35. final imageCacheManager = CacheManager(
  36. Config(
  37. 'kemono_images',
  38. stalePeriod: const Duration(days: 30),
  39. maxNrOfCacheObjects: 1024,
  40. fileService: HttpFileService(),
  41. ),
  42. );
  43. // Navigation Items Model
  44. class NavItem {
  45. final String label;
  46. final IconData icon;
  47. final Widget page;
  48. const NavItem({
  49. required this.label,
  50. required this.icon,
  51. required this.page,
  52. });
  53. }
  54. // App Layout with Navigation
  55. class AppLayout extends ConsumerStatefulWidget {
  56. final List<NavItem> navItems;
  57. const AppLayout({super.key, required this.navItems});
  58. @override
  59. ConsumerState<AppLayout> createState() => _AppLayoutState();
  60. }
  61. class _AppLayoutState extends ConsumerState<AppLayout> {
  62. final _navIndex = StateProvider<int>((ref) => 0);
  63. Widget _buildDesktopNavRail(int selectedIndex) {
  64. return NavigationRail(
  65. selectedIndex: selectedIndex,
  66. onDestinationSelected: (index) =>
  67. ref.read(_navIndex.notifier).state = index,
  68. labelType: NavigationRailLabelType.all,
  69. destinations: widget.navItems.map((item) =>
  70. NavigationRailDestination(
  71. icon: Icon(item.icon),
  72. label: Text(item.label),
  73. )).toList(),
  74. );
  75. }
  76. Widget _buildMobileDrawer(int selectedIndex) {
  77. return Drawer(
  78. child: ListView(
  79. children: [
  80. const DrawerHeader(
  81. decoration: BoxDecoration(color: Colors.blue),
  82. child: Text('Navigation', style: TextStyle(color: Colors.white)),
  83. ),
  84. ...widget.navItems.asMap().entries.map((entry) => ListTile(
  85. leading: Icon(entry.value.icon),
  86. title: Text(entry.value.label),
  87. selected: entry.key == selectedIndex,
  88. onTap: () {
  89. ref.read(_navIndex.notifier).state = entry.key;
  90. Navigator.pop(context);
  91. },
  92. )),
  93. ],
  94. ),
  95. );
  96. }
  97. @override
  98. Widget build(BuildContext context) {
  99. final selectedIndex = ref.watch(_navIndex);
  100. final isDesktop = MediaQuery.of(context).size.width >= 600;
  101. return Scaffold(
  102. appBar: AppBar(
  103. title: Text(widget.navItems[selectedIndex].label),
  104. leading: isDesktop ? null : Builder(
  105. builder: (context) => IconButton(
  106. icon: const Icon(Icons.menu),
  107. onPressed: () => Scaffold.of(context).openDrawer(),
  108. ),
  109. ),
  110. ),
  111. drawer: isDesktop ? null : _buildMobileDrawer(selectedIndex),
  112. body: Row(
  113. children: [
  114. if (isDesktop) _buildDesktopNavRail(selectedIndex),
  115. Expanded(child: widget.navItems[selectedIndex].page),
  116. ],
  117. ),
  118. );
  119. }
  120. }
  121. class HomeScreen extends ConsumerStatefulWidget {
  122. const HomeScreen({super.key});
  123. @override
  124. ConsumerState<HomeScreen> createState() => _HomeScreenState();
  125. }
  126. class _HomeScreenState extends ConsumerState<HomeScreen> {
  127. final TextEditingController _searchController = TextEditingController();
  128. final _searchQuery = StateProvider<String>((ref) => ''); // Added search state
  129. // Modified provider with search filtering
  130. final filteredCreatorsProvider = FutureProvider.autoDispose.family<List<Creator>, String>(
  131. (ref, query) async {
  132. final originalList = await ref.watch(creatorsProvider.future);
  133. return originalList.where(
  134. (creator) => creator.name.toLowerCase().contains(query.toLowerCase())
  135. ).toList();
  136. },
  137. );
  138. @override
  139. Widget build(BuildContext context) {
  140. return DefaultTabController(
  141. length: 2,
  142. child: Scaffold(
  143. appBar: AppBar(
  144. title: _buildSearchField(), // Replaced title with search field
  145. actions: [
  146. IconButton(
  147. icon: const Icon(Icons.refresh),
  148. onPressed: () => ref.invalidate(filteredCreatorsProvider),
  149. ),
  150. ],
  151. ),
  152. body: _buildCreatorsList(ref.watch(
  153. filteredCreatorsProvider(ref.watch(_searchQuery)) // Added query param
  154. )),
  155. ),
  156. );
  157. }
  158. // New search field widget
  159. Widget _buildSearchField() {
  160. return TextField(
  161. controller: _searchController,
  162. decoration: InputDecoration(
  163. hintText: 'Search creators...',
  164. border: InputBorder.none,
  165. suffixIcon: IconButton(
  166. icon: const Icon(Icons.clear),
  167. onPressed: () {
  168. _searchController.clear();
  169. ref.read(_searchQuery.notifier).state = '';
  170. },
  171. ),
  172. ),
  173. onChanged: (value) => ref.read(_searchQuery.notifier).state = value,
  174. );
  175. }
  176. // Updated creators list with search
  177. Widget _buildCreatorsList(AsyncValue<List<Creator>> creatorsAsync) {
  178. return RefreshIndicator(
  179. onRefresh: () => ref.refresh(filteredCreatorsProvider(ref.read(_searchQuery)).future),
  180. child: creatorsAsync.when(
  181. loading: () => const Center(child: CircularProgressIndicator()),
  182. error: (error, _) => Center(child: Text(error.toString())),
  183. data: (creators) => creators.isEmpty
  184. ? const Center(child: Text('No matching creators found'))
  185. : ListView.separated(
  186. padding: const EdgeInsets.all(16),
  187. itemCount: creators.length,
  188. separatorBuilder: (_, __) => const SizedBox(height: 8),
  189. itemBuilder: (context, index) => CreatorCard(creator: creators[index]),
  190. ),
  191. ),
  192. );
  193. }
  194. Widget _buildGlobalPostsGrid() {
  195. final postsAsync = ref.watch(postsProvider(('82522', 'patreon')));
  196. return postsAsync.when(
  197. loading: () => const Center(child: CircularProgressIndicator()),
  198. error: (error, _) => Center(child: Text(error.toString())),
  199. data: (posts) => CustomScrollView(
  200. slivers: [
  201. SliverAppBar(),
  202. SliverList(
  203. delegate: SliverChildBuilderDelegate(
  204. (context, index) => PostContainer(post: posts[index]),
  205. childCount: posts.length,
  206. ),
  207. ),
  208. ],
  209. ),
  210. );
  211. }
  212. }
  213. // Main Application
  214. void main() => runApp(
  215. const ProviderScope(
  216. child: MaterialApp(
  217. home: AppLayout(
  218. navItems: [
  219. NavItem(
  220. label: 'Home',
  221. icon: Icons.home,
  222. page: HomeScreen(),
  223. ),
  224. NavItem(
  225. label: 'Global Feed (Rukis)',
  226. icon: Icons.public,
  227. page: PostsScreen( creatorId:'82522', service: 'patreon', withAppBar: false,),
  228. ),
  229. NavItem(
  230. label: 'Settings',
  231. icon: Icons.settings,
  232. page: Placeholder(), // Replace with actual SettingsScreen
  233. ),
  234. ],
  235. ),
  236. ),
  237. ),
  238. );