259 lines
8.8 KiB
Dart
259 lines
8.8 KiB
Dart
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);
|
||
_loadCategories();
|
||
if (widget.isActive) refreshAccount();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
UserState.needShowVideoMenu.removeListener(_onExtConfigChanged);
|
||
UserState.extConfigItems.removeListener(_onExtConfigChanged);
|
||
super.dispose();
|
||
}
|
||
|
||
void _onExtConfigChanged() {
|
||
if (mounted) setState(() {});
|
||
}
|
||
|
||
@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();
|
||
}
|
||
|
||
/// 当前列表:need_wait false 时用 extConfig.items;true 且选中固定分类时用 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: _categoriesLoading
|
||
? const SizedBox(
|
||
height: 40,
|
||
child: Center(child: CircularProgressIndicator()))
|
||
: HomeTabRow(
|
||
categories: _categories,
|
||
selectedId: _selectedCategory?.id ?? -1,
|
||
onTabChanged: _onTabChanged,
|
||
),
|
||
),
|
||
Expanded(
|
||
child: _showVideoMenu &&
|
||
_selectedCategory?.id != kExtCategoryId &&
|
||
_tasksLoading
|
||
? 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,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|