import 'package:flutter/material.dart'; import '../../core/api/services/image_api.dart'; import '../../core/auth/auth_service.dart'; import '../../core/user/account_refresh.dart'; import '../../core/user/user_state.dart'; import '../../core/theme/app_spacing.dart'; import '../../shared/widgets/top_nav_bar.dart'; import 'models/category_item.dart'; import 'models/ext_config_item.dart'; import 'models/task_item.dart'; import 'widgets/home_tab_row.dart'; import 'widgets/video_card.dart'; /// 固定「pets」分类 id,用于展示 extConfig.items const int kExtCategoryId = -1; /// AI Video App home screen - tab 来自分类接口,Grid 来自任务列表或 extConfig.items class HomeScreen extends StatefulWidget { const HomeScreen({super.key, this.isActive = true}); final bool isActive; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { List _categories = []; CategoryItem? _selectedCategory; List _tasks = []; bool _categoriesLoading = true; bool _tasksLoading = false; int? _activeCardIndex; @override void initState() { super.initState(); UserState.needShowVideoMenu.addListener(_onExtConfigChanged); UserState.extConfigItems.addListener(_onExtConfigChanged); AuthService.isLoginComplete.addListener(_onExtConfigChanged); UserState.homeReloadNonce.addListener(_onHomeReloadNonce); _loadCategories(); if (widget.isActive) refreshAccount(); } @override void dispose() { UserState.needShowVideoMenu.removeListener(_onExtConfigChanged); UserState.extConfigItems.removeListener(_onExtConfigChanged); AuthService.isLoginComplete.removeListener(_onExtConfigChanged); UserState.homeReloadNonce.removeListener(_onHomeReloadNonce); super.dispose(); } void _onExtConfigChanged() { if (mounted) setState(() {}); } void _onHomeReloadNonce() { if (!mounted) return; _loadCategories(); } @override void didUpdateWidget(covariant HomeScreen oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive) { refreshAccount(); } } /// 仅 need_wait === true 时展示 Video 分类栏;其他(false/null/未解析)只显示图片列表 bool get _showVideoMenu => UserState.needShowVideoMenu.value == true; List get _parsedExtItems { final raw = UserState.extConfigItems.value; if (raw == null || raw.isEmpty) return []; return raw .map((e) => e is Map ? ExtConfigItem.fromJson(e) : ExtConfigItem.fromJson(Map.from(e as Map))) .toList(); } /// 是否处于首次加载中(分类/任务/extConfig 尚未就绪) /// 登录未完成时不显示页面加载指示器,由登录遮罩负责 bool get _isListLoading { if (!AuthService.isLoginComplete.value) return false; if (_showVideoMenu) { if (_categoriesLoading) return true; if (_selectedCategory?.id == kExtCategoryId) { return UserState.extConfigItems.value == null; } return _tasksLoading; } return UserState.extConfigItems.value == null; } /// 分类栏加载中时是否显示加载指示器(登录未完成时不显示) bool get _showCategoriesLoading => AuthService.isLoginComplete.value && _categoriesLoading; /// 当前列表:need_wait false 时用 extConfig.items;true 且选中固定分类时用 extConfig.items;否则用 _tasks List get _displayTasks { if (!_showVideoMenu) { return _extItemsToTaskItems(_parsedExtItems); } if (_selectedCategory?.id == kExtCategoryId) { return _extItemsToTaskItems(_parsedExtItems); } return _tasks; } static List _extItemsToTaskItems(List items) { return items .map((e) => TaskItem( templateName: e.title, title: e.title, previewImageUrl: e.image, previewVideoUrl: null, taskType: e.title, ext: e.detail, credits480p: e.cost, )) .toList(); } Future _loadCategories() async { setState(() => _categoriesLoading = true); await AuthService.loginComplete; if (!mounted) return; final res = await ImageApi.getCategoryList(); if (mounted) { if (res.isSuccess && res.data is List) { final list = (res.data as List) .map((e) => CategoryItem.fromJson(e as Map)) .toList(); if (UserState.needShowVideoMenu.value == true) { list.add(const CategoryItem(id: kExtCategoryId, name: 'pets', icon: null)); } setState(() { _categories = list; _selectedCategory = list.isNotEmpty ? list.first : null; if (_selectedCategory != null) { if (_selectedCategory!.id == kExtCategoryId) { _tasks = []; _tasksLoading = false; } else { _loadTasks(_selectedCategory!.id); } } }); } else { setState(() => _categories = []); } setState(() => _categoriesLoading = false); } } Future _loadTasks(int categoryId) async { setState(() => _tasksLoading = true); final res = await ImageApi.getImg2VideoTasks(insignia: categoryId); if (mounted) { if (res.isSuccess && res.data is List) { final list = (res.data as List) .map((e) => TaskItem.fromJson(e as Map)) .toList(); setState(() { _tasks = list; _activeCardIndex = null; }); } else { setState(() => _tasks = []); } setState(() => _tasksLoading = false); } } void _onTabChanged(CategoryItem c) { setState(() => _selectedCategory = c); if (c.id == kExtCategoryId) { setState(() { _tasks = []; _tasksLoading = false; }); } else { _loadTasks(c.id); } } static const _placeholderImage = 'https://images.unsplash.com/photo-1763929272543-0df093e4f659?w=400'; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFFAFAFA), appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: TopNavBar( title: 'PetsHero AI', credits: UserCreditsData.of(context)?.creditsDisplay ?? '--', onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'), ), ), body: Column( children: [ // 仅 need_wait == true 时展示顶部分类栏(Pencil: tabRow bK6o6) if (_showVideoMenu) Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.screenPadding, vertical: AppSpacing.xs, ), child: _showCategoriesLoading ? const SizedBox( height: 40, child: Center(child: CircularProgressIndicator())) : HomeTabRow( categories: _categories, selectedId: _selectedCategory?.id ?? -1, onTabChanged: _onTabChanged, ), ), Expanded( child: _isListLoading ? const Center(child: CircularProgressIndicator()) : LayoutBuilder( builder: (context, constraints) { final tasks = _displayTasks; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 390), child: GridView.builder( padding: const EdgeInsets.fromLTRB( AppSpacing.screenPadding, AppSpacing.xl, AppSpacing.screenPadding, AppSpacing.screenPaddingLarge, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 165 / 248, mainAxisSpacing: AppSpacing.xl, crossAxisSpacing: AppSpacing.xl, ), itemCount: tasks.length, itemBuilder: (context, index) { final task = tasks[index]; final credits = task.credits480p != null ? task.credits480p.toString() : '50'; return VideoCard( imageUrl: task.previewImageUrl ?? _placeholderImage, videoUrl: task.previewVideoUrl, credits: credits, isActive: _activeCardIndex == index, onPlayRequested: () => setState(() => _activeCardIndex = index), onStopRequested: () => setState(() => _activeCardIndex = null), onGenerateSimilar: () => Navigator.of(context).pushNamed( '/generate', arguments: task, ), ); }, ), ), ); }, ), ), ], ), ); } }