main.dart 8.1 KB

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