浏览代码

added navbar

Veloe 3 月之前
父节点
当前提交
64d453f325
共有 6 个文件被更改,包括 293 次插入186 次删除
  1. 57 0
      lib/kemono_client.dart
  2. 97 182
      lib/main.dart
  3. 4 2
      lib/pages/posts_screen.dart
  4. 83 0
      lib/pages/widgets/creator_card.dart
  5. 0 2
      lib/pages/widgets/post_container.dart
  6. 52 0
      lib/posts_notifier.dart

+ 57 - 0
lib/kemono_client.dart

@@ -0,0 +1,57 @@
+import 'dart:convert';
+import 'package:dio/dio.dart';
+import 'package:veloe_kemono_party_flutter/models/creator.dart';
+import 'package:veloe_kemono_party_flutter/models/post.dart';
+
+class KemonoClient {
+  final Dio _dio = Dio(BaseOptions(baseUrl: 'https://kemono.su/'));
+
+  Future<List<Creator>> getCreators() async {
+    try {
+  final response = await _dio.get('/api/v1/creators.txt');
+  
+  if (response.statusCode != 200) {
+    throw DioException(
+      requestOptions: response.requestOptions,
+      response: response,
+      error: 'Unexpected status code: ${response.statusCode}'
+    );
+  }
+
+  final parsedData = switch(response.data) {
+    String s => jsonDecode(s),
+    List<dynamic> list => list,
+    Map<String,dynamic> map => [map],
+    _ => throw FormatException('Unprocessable data')
+  };
+
+  return (parsedData as List).map((x) => Creator.fromJson(x)).toList();
+} on DioException catch (e) {
+  throw Exception('Network error: ${e.message}');
+} on FormatException catch (e) {
+  throw Exception('Data format error: ${e.message}');
+}
+  }
+
+  Future<List<Post>> getPosts(String creatorId, String service, int start) async {
+    try {
+      final response = await _dio.get(
+        '/api/v1/$service/user/$creatorId',
+        queryParameters: {'o': start},
+      );
+
+      final parsedData = switch(response.data) {
+          String s => jsonDecode(s),
+          List<dynamic> list => list,
+          Map<String,dynamic> map => [map],
+          _ => throw FormatException('Unprocessable data')
+        };
+
+      return (parsedData as List).map((x) => Post.fromJson(x)).toList();
+    } on DioException catch (e) {
+      throw Exception('Posts load failed: ${e.message}');
+    } on Exception catch (e) {
+        throw Exception('Oops something go wrong}');
+    }
+  }
+}

+ 97 - 182
lib/main.dart

@@ -1,13 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:dio/dio.dart';
-import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:veloe_kemono_party_flutter/kemono_client.dart';
 import 'package:veloe_kemono_party_flutter/models/creator.dart';
 import 'package:veloe_kemono_party_flutter/models/post.dart';
 import 'package:veloe_kemono_party_flutter/pages/posts_screen.dart';
+import 'package:veloe_kemono_party_flutter/pages/widgets/creator_card.dart';
 import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
-import 'dart:convert';
+
+import 'package:veloe_kemono_party_flutter/posts_notifier.dart';
 
 // Providers
 final searchQueryProvider = StateProvider<String>((ref) => '');
@@ -37,109 +38,6 @@ final postsProvider = StateNotifierProvider.autoDispose
   );
 });
 
-class PostsNotifier extends StateNotifier<AsyncValue<List<Post>>> {
-  final String creatorId;
-  final String service;
-  final KemonoClient client;
-  int _currentPage = 0;
-
-  PostsNotifier({
-    required this.creatorId,
-    required this.service,
-    required this.client,
-  }) : super(const AsyncValue.loading()) {
-    loadInitial();
-  }
-
-  Future<void> loadInitial() async {
-    state = const AsyncValue.loading();
-    try {
-      final posts = await client.getPosts(creatorId, service, 0);
-      state = AsyncValue.data(posts);
-      _currentPage = 1;
-    } catch (e) {
-      state = AsyncValue.error(e, StackTrace.current);
-    }
-  }
-
-  Future<void> loadNext() async {
-    if (state.isLoading) return;
-    
-    final previousPosts = state.valueOrNull ?? [];
-    // Add explicit type parameter
-    state = AsyncValue<List<Post>>.loading().copyWithPrevious(state);
-    
-    try {
-      final newPosts = await client.getPosts(
-        creatorId, 
-        service, 
-        _currentPage * 50
-      );
-      print('Loading page $_currentPage with offset ${_currentPage * 20}');
-      state = AsyncValue.data([...previousPosts, ...newPosts]);
-      _currentPage++;
-    } catch (e) {
-      // Specify type for error state
-      state = AsyncValue<List<Post>>.error(e, StackTrace.current)
-        .copyWithPrevious(AsyncValue.data(previousPosts));
-    }
-  }
-}
-
-// API Client
-class KemonoClient {
-  final Dio _dio = Dio(BaseOptions(baseUrl: 'https://kemono.su/'));
-
-  Future<List<Creator>> getCreators() async {
-    try {
-  final response = await _dio.get('/api/v1/creators.txt');
-  
-  if (response.statusCode != 200) {
-    throw DioException(
-      requestOptions: response.requestOptions,
-      response: response,
-      error: 'Unexpected status code: ${response.statusCode}'
-    );
-  }
-
-  final parsedData = switch(response.data) {
-    String s => jsonDecode(s),
-    List<dynamic> list => list,
-    Map<String,dynamic> map => [map],
-    _ => throw FormatException('Unprocessable data')
-  };
-
-  return (parsedData as List).map((x) => Creator.fromJson(x)).toList();
-} on DioException catch (e) {
-  throw Exception('Network error: ${e.message}');
-} on FormatException catch (e) {
-  throw Exception('Data format error: ${e.message}');
-}
-  }
-
-  Future<List<Post>> getPosts(String creatorId, String service, int start) async {
-    try {
-      final response = await _dio.get(
-        '/api/v1/$service/user/$creatorId',
-        queryParameters: {'o': start},
-      );
-
-      final parsedData = switch(response.data) {
-          String s => jsonDecode(s),
-          List<dynamic> list => list,
-          Map<String,dynamic> map => [map],
-          _ => throw FormatException('Unprocessable data')
-        };
-
-      return (parsedData as List).map((x) => Post.fromJson(x)).toList();
-    } on DioException catch (e) {
-      throw Exception('Posts load failed: ${e.message}');
-    } on Exception catch (e) {
-        throw Exception('Oops something go wrong}');
-    }
-  }
-}
-
 // Image Caching
 final imageCacheManager = CacheManager(
   Config(
@@ -149,85 +47,94 @@ final imageCacheManager = CacheManager(
     fileService: HttpFileService(),
   ),
 );
+// Navigation Items Model
+class NavItem {
+  final String label;
+  final IconData icon;
+  final Widget page;
+
+  const NavItem({
+    required this.label,
+    required this.icon,
+    required this.page,
+  });
+}
 
-class ResizedImage extends StatelessWidget {
-  final String imageUrl;
-  final BoxFit fit;
-
-  const ResizedImage({super.key, required this.imageUrl, this.fit = BoxFit.cover});
+// App Layout with Navigation
+class AppLayout extends ConsumerStatefulWidget {
+  final List<NavItem> navItems;
+  
+  const AppLayout({super.key, required this.navItems});
 
   @override
-  Widget build(BuildContext context) {
-    final screenSize = MediaQuery.of(context).size;
-    return CachedNetworkImage(
-      cacheManager: imageCacheManager,
-      imageUrl: imageUrl,
-      imageBuilder: (context, imageProvider) => Image(
-        image: ResizeImage(
-          imageProvider,
-          width: screenSize.width.toInt(),
-        ),
-        fit: fit,
-      ),
-      placeholder: (context, url) => const CircularProgressIndicator(),
-      errorWidget: (context, url, error) => const Icon(Icons.error),
-    );
-  }
+  ConsumerState<AppLayout> createState() => _AppLayoutState();
 }
 
-// UI Components
-class CreatorCard extends ConsumerWidget {
-  final Creator creator;
-  
-  const CreatorCard({super.key, required this.creator});
+class _AppLayoutState extends ConsumerState<AppLayout> {
+  final _navIndex = StateProvider<int>((ref) => 0);
+
+  Widget _buildDesktopNavRail(int selectedIndex) {
+    return NavigationRail(
+      selectedIndex: selectedIndex,
+      onDestinationSelected: (index) => 
+          ref.read(_navIndex.notifier).state = index,
+      labelType: NavigationRailLabelType.all,
+      destinations: widget.navItems.map((item) => 
+          NavigationRailDestination(
+            icon: Icon(item.icon),
+            label: Text(item.label),
+          )).toList(),
+    );
+  }
 
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    return Card(
-      child: ListTile(
-        leading: Hero(
-          tag: 'creator-${creator.id}',
-          child: CircleAvatar(
-            backgroundColor: Colors.grey[200],
-            child: ResizedImage(
-              imageUrl: creator.iconUrl,
-              fit: BoxFit.contain,
-            ),
+  Widget _buildMobileDrawer(int selectedIndex) {
+    return Drawer(
+      child: ListView(
+        children: [
+          const DrawerHeader(
+            decoration: BoxDecoration(color: Colors.blue),
+            child: Text('Navigation', style: TextStyle(color: Colors.white)),
           ),
-        ),
-        title: Text(creator.name),
-        subtitle: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Text(creator.service.toUpperCase()),
-            Text('Last updated: ${_formatDate(creator.updated)}'),
-          ],
-        ),
-        onTap: () => _showCreatorPosts(context, ref),
+          ...widget.navItems.asMap().entries.map((entry) => ListTile(
+            leading: Icon(entry.value.icon),
+            title: Text(entry.value.label),
+            selected: entry.key == selectedIndex,
+            onTap: () {
+              ref.read(_navIndex.notifier).state = entry.key;
+              Navigator.pop(context);
+            },
+          )),
+        ],
       ),
     );
   }
 
-  String _formatDate(int timestamp) {
-    return 
-      DateTime.fromMillisecondsSinceEpoch((timestamp * 1000).toInt()).toString();
-  }
-
-  void _showCreatorPosts(BuildContext context, WidgetRef ref) {
-    ref.read(selectedCreatorProvider.notifier).state = creator;
-    Navigator.push(
-      context,
-      MaterialPageRoute(
-        builder: (context) => PostsScreen(
-          creatorId: creator.id,
-          service: creator.service,
+  @override
+  Widget build(BuildContext context) {
+    final selectedIndex = ref.watch(_navIndex);
+    final isDesktop = MediaQuery.of(context).size.width >= 600;
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.navItems[selectedIndex].label),
+        leading: isDesktop ? null : Builder(
+          builder: (context) => IconButton(
+            icon: const Icon(Icons.menu),
+            onPressed: () => Scaffold.of(context).openDrawer(),
+          ),
         ),
       ),
+      drawer: isDesktop ? null : _buildMobileDrawer(selectedIndex),
+      body: Row(
+        children: [
+          if (isDesktop) _buildDesktopNavRail(selectedIndex),
+          Expanded(child: widget.navItems[selectedIndex].page),
+        ],
+      ),
     );
   }
 }
 
-// Main Screens
 class HomeScreen extends ConsumerStatefulWidget {
   const HomeScreen({super.key});
 
@@ -262,21 +169,10 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
               onPressed: () => ref.invalidate(filteredCreatorsProvider),
             ),
           ],
-          bottom: const TabBar(
-            tabs: [
-              Tab(icon: Icon(Icons.people_outline), text: 'Creators'),
-              Tab(icon: Icon(Icons.grid_view), text: 'Posts'),
-            ],
-          ),
         ),
-        body: TabBarView(
-          children: [
-            _buildCreatorsList(ref.watch(
+        body: _buildCreatorsList(ref.watch(
               filteredCreatorsProvider(ref.watch(_searchQuery)) // Added query param
-            )),
-            _buildGlobalPostsGrid(),
-          ],
-        ),
+        )),
       ),
     );
   }
@@ -340,10 +236,29 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
   }
 }
 
+// Main Application
 void main() => runApp(
   const ProviderScope(
     child: MaterialApp(
-      home: HomeScreen(),
+      home: AppLayout(
+        navItems: [
+          NavItem(
+            label: 'Home',
+            icon: Icons.home,
+            page: HomeScreen(),
+          ),
+          NavItem(
+            label: 'Global Feed (Rukis)',
+            icon: Icons.public,
+            page: PostsScreen( creatorId:'82522', service: 'patreon', withAppBar: false,),
+          ),
+          NavItem(
+            label: 'Settings',
+            icon: Icons.settings,
+            page: Placeholder(), // Replace with actual SettingsScreen
+          ),
+        ],
+      ),
     ),
   ),
 );

+ 4 - 2
lib/pages/posts_screen.dart

@@ -6,11 +6,13 @@ import 'package:veloe_kemono_party_flutter/pages/widgets/post_container.dart';
 class PostsScreen extends ConsumerStatefulWidget {
   final String creatorId;
   final String service;
+  final bool withAppBar;
 
   const PostsScreen({
     super.key,
     required this.creatorId,
     required this.service,
+    this.withAppBar = true
   });
 
   @override
@@ -32,13 +34,13 @@ class _PostsScreenState extends ConsumerState<PostsScreen> with PaginationMixin
     final postsAsync = ref.watch(postsProvider((widget.creatorId, widget.service)));
 
     return Scaffold(
-      appBar: AppBar(
+      appBar: widget.withAppBar ? AppBar(
         title: const Text('Posts'),
         leading: IconButton(
           icon: const Icon(Icons.arrow_back),
           onPressed: () => Navigator.pop(context),
         ),
-      ),
+      ) : null,
       body: postsAsync.when(
         loading: () => const Center(child: CircularProgressIndicator()),
         error: (error, _) => Center(

+ 83 - 0
lib/pages/widgets/creator_card.dart

@@ -0,0 +1,83 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:veloe_kemono_party_flutter/main.dart';
+import 'package:veloe_kemono_party_flutter/models/creator.dart';
+import 'package:veloe_kemono_party_flutter/pages/posts_screen.dart';
+
+class ResizedImage extends StatelessWidget {
+  final String imageUrl;
+  final BoxFit fit;
+
+  const ResizedImage({super.key, required this.imageUrl, this.fit = BoxFit.cover});
+
+  @override
+  Widget build(BuildContext context) {
+    final screenSize = MediaQuery.of(context).size;
+    return CachedNetworkImage(
+      cacheManager: imageCacheManager,
+      imageUrl: imageUrl,
+      imageBuilder: (context, imageProvider) => Image(
+        image: ResizeImage(
+          imageProvider,
+          width: screenSize.width.toInt(),
+        ),
+        fit: fit,
+      ),
+      placeholder: (context, url) => const CircularProgressIndicator(),
+      errorWidget: (context, url, error) => const Icon(Icons.error),
+    );
+  }
+}
+
+// UI Components
+class CreatorCard extends ConsumerWidget {
+  final Creator creator;
+  
+  const CreatorCard({super.key, required this.creator});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return Card(
+      child: ListTile(
+        leading: Hero(
+          tag: 'creator-${creator.id}',
+          child: CircleAvatar(
+            backgroundColor: Colors.grey[200],
+            child: ResizedImage(
+              imageUrl: creator.iconUrl,
+              fit: BoxFit.contain,
+            ),
+          ),
+        ),
+        title: Text(creator.name),
+        subtitle: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(creator.service.toUpperCase()),
+            Text('Last updated: ${_formatDate(creator.updated)}'),
+          ],
+        ),
+        onTap: () => _showCreatorPosts(context, ref),
+      ),
+    );
+  }
+
+  String _formatDate(int timestamp) {
+    return 
+      DateTime.fromMillisecondsSinceEpoch((timestamp * 1000).toInt()).toString();
+  }
+
+  void _showCreatorPosts(BuildContext context, WidgetRef ref) {
+    ref.read(selectedCreatorProvider.notifier).state = creator;
+    Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => PostsScreen(
+          creatorId: creator.id,
+          service: creator.service,
+        ),
+      ),
+    );
+  }
+}

+ 0 - 2
lib/pages/widgets/post_container.dart

@@ -227,12 +227,10 @@ class _PostContentSection extends StatelessWidget {
 // URL handling method (add to parent class)
 Future<bool> _handleUrlLaunch(BuildContext context, String url) async {
   try {
-    if (await canLaunchUrl(Uri.parse(url))) {
       await launchUrl(
         Uri.parse(url),
         mode: LaunchMode.externalApplication,
       );
-    }
   } catch (e) {
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(content: Text('Could not open link: ${e.toString()}')),

+ 52 - 0
lib/posts_notifier.dart

@@ -0,0 +1,52 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:veloe_kemono_party_flutter/kemono_client.dart';
+import 'package:veloe_kemono_party_flutter/models/post.dart';
+
+class PostsNotifier extends StateNotifier<AsyncValue<List<Post>>> {
+  final String creatorId;
+  final String service;
+  final KemonoClient client;
+  int _currentPage = 0;
+
+  PostsNotifier({
+    required this.creatorId,
+    required this.service,
+    required this.client,
+  }) : super(const AsyncValue.loading()) {
+    loadInitial();
+  }
+
+  Future<void> loadInitial() async {
+    state = const AsyncValue.loading();
+    try {
+      final posts = await client.getPosts(creatorId, service, 0);
+      state = AsyncValue.data(posts);
+      _currentPage = 1;
+    } catch (e) {
+      state = AsyncValue.error(e, StackTrace.current);
+    }
+  }
+
+  Future<void> loadNext() async {
+    if (state.isLoading) return;
+    
+    final previousPosts = state.valueOrNull ?? [];
+    // Add explicit type parameter
+    state = AsyncValue<List<Post>>.loading().copyWithPrevious(state);
+    
+    try {
+      final newPosts = await client.getPosts(
+        creatorId, 
+        service, 
+        _currentPage * 50
+      );
+      print('Loading page $_currentPage with offset ${_currentPage * 20}');
+      state = AsyncValue.data([...previousPosts, ...newPosts]);
+      _currentPage++;
+    } catch (e) {
+      // Specify type for error state
+      state = AsyncValue<List<Post>>.error(e, StackTrace.current)
+        .copyWithPrevious(AsyncValue.data(previousPosts));
+    }
+  }
+}