petsHero-AI/lib/features/home/home_screen.dart
2026-03-24 18:45:27 +08:00

284 lines
9.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<CategoryItem> _categories = [];
CategoryItem? _selectedCategory;
List<TaskItem> _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<ExtConfigItem> get _parsedExtItems {
final raw = UserState.extConfigItems.value;
if (raw == null || raw.isEmpty) return [];
return raw
.map((e) => e is Map<String, dynamic>
? ExtConfigItem.fromJson(e)
: ExtConfigItem.fromJson(Map<String, dynamic>.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.itemstrue 且选中固定分类时用 extConfig.items否则用 _tasks
List<TaskItem> get _displayTasks {
if (!_showVideoMenu) {
return _extItemsToTaskItems(_parsedExtItems);
}
if (_selectedCategory?.id == kExtCategoryId) {
return _extItemsToTaskItems(_parsedExtItems);
}
return _tasks;
}
static List<TaskItem> _extItemsToTaskItems(List<ExtConfigItem> 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<void> _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<String, dynamic>))
.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<void> _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<String, dynamic>))
.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,
),
);
},
),
),
);
},
),
),
],
),
);
}
}