This commit is contained in:
lz_db
2026-10-25 11:46:44 +08:00
parent 893dfed0ed
commit 494f667719
63 changed files with 5213 additions and 239 deletions
+122 -8
View File
@@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/media/cover_cache.dart';
import '../../core/theme/app_theme.dart';
import '../../models/video_item.dart';
import '../../providers/feed_provider.dart';
import '../../services/upload_service.dart';
import '../../services/video_service.dart';
@@ -19,13 +21,25 @@ class CategoryScreen extends ConsumerStatefulWidget {
class _CategoryScreenState extends ConsumerState<CategoryScreen> {
List<VideoCategory> _categories = const [];
List<VideoSource> _sources = const [];
int? _selected; // null = 全部
int? _selectedSource; // null = 全部来源
List<VideoItem> _items = const [];
String? _cursor;
bool _loading = true;
bool _loadingMore = false;
bool _failed = false;
/// 当前选中的分类是否为系列型(如 AI短剧):按「剧」聚合,一部剧一张卡片
bool get _seriesMode {
final id = _selected;
if (id == null) return false;
for (final c in _categories) {
if (c.categoryId == id) return c.hasSeries;
}
return false;
}
final _scroll = ScrollController();
@override
@@ -56,13 +70,19 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
try {
final results = await Future.wait([
UploadService.fetchCategories(),
VideoService.fetchFeed(type: 'hot', categoryId: _selected, limit: 10),
VideoService.fetchSources(),
VideoService.fetchFeed(
type: 'hot',
categoryId: _selected,
sourceId: _selectedSource,
limit: 10),
]);
if (!mounted) return;
setState(() {
_categories = results[0] as List<VideoCategory>;
_items = (results[1] as FeedPage).items;
_cursor = (results[1] as FeedPage).nextCursor;
_sources = results[1] as List<VideoSource>;
_items = (results[2] as FeedPage).items;
_cursor = (results[2] as FeedPage).nextCursor;
_loading = false;
});
} catch (_) {
@@ -80,6 +100,12 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
await _reload();
}
Future<void> _selectSource(int? id) async {
if (id == _selectedSource) return;
setState(() => _selectedSource = id);
await _reload();
}
Future<void> _reload() async {
setState(() {
_loading = true;
@@ -87,7 +113,11 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
});
try {
final page = await VideoService.fetchFeed(
type: 'hot', categoryId: _selected, limit: 10);
type: 'hot',
categoryId: _selected,
sourceId: _selectedSource,
group: _seriesMode ? 'series' : null,
limit: 10);
if (!mounted) return;
setState(() {
_items = page.items;
@@ -108,7 +138,12 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
setState(() => _loadingMore = true);
try {
final page = await VideoService.fetchFeed(
type: 'hot', cursor: _cursor, categoryId: _selected, limit: 10);
type: 'hot',
cursor: _cursor,
categoryId: _selected,
sourceId: _selectedSource,
group: _seriesMode ? 'series' : null,
limit: 10);
if (!mounted) return;
setState(() {
_items = [..._items, ...page.items];
@@ -157,6 +192,19 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
],
),
),
// 来源筛选条(麻豆 / 91 ...,多于一个来源时才显示)
if (_sources.length > 1)
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_sourceChip('全部来源', null),
for (final s in _sources) _sourceChip(s.name, s.sourceId),
],
),
),
Expanded(child: _buildBody()),
],
),
@@ -180,6 +228,26 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
);
}
Widget _sourceChip(String label, int? id) {
final active = id == _selectedSource;
return Padding(
padding: const EdgeInsets.only(right: 8, top: 4),
child: FilterChip(
label: Text(label, style: const TextStyle(fontSize: 12)),
selected: active,
onSelected: (_) => _selectSource(id),
selectedColor: AppColors.primary.withValues(alpha: 0.18),
backgroundColor: AppColors.surface,
side: BorderSide(
color: active ? AppColors.primary : AppColors.textWeak.withValues(alpha: 0.3)),
labelStyle: TextStyle(
color: active ? AppColors.primary : AppColors.textSecondary,
),
visualDensity: VisualDensity.compact,
),
);
}
Widget _buildBody() {
if (_loading && _items.isEmpty) {
return const Center(
@@ -229,15 +297,21 @@ class _CategoryScreenState extends ConsumerState<CategoryScreen> {
}
}
class _CategoryCard extends StatelessWidget {
class _CategoryCard extends ConsumerWidget {
const _CategoryCard({required this.item});
final VideoItem item;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () => context.push('/search/play', extra: item),
// 进入播放页前停掉首页 Feed 的播放器池,返回时再恢复,避免「双重声音」
onTap: () {
// 挂起首页池(保存断点 + 停所有槽位),恢复由 FeedScreen.didPopNext 统一处理
final strategy = ref.read(activeFeedStrategyProvider);
ref.read(feedProvider(strategy).notifier).pool.suspend();
context.push('/search/play', extra: item);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
@@ -248,6 +322,7 @@ class _CategoryCard extends StatelessWidget {
child: item.coverUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: item.coverUrl,
cacheManager: CoverCacheManager.I,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const Icon(
Icons.play_circle_outline,
@@ -282,6 +357,45 @@ class _CategoryCard extends StatelessWidget {
),
),
),
// 来源角标(麻豆 / 91 ...)
if (item.sourceName.isNotEmpty)
Positioned(
left: 6,
top: 6,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(4),
),
child: Text(
item.sourceName,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600),
),
),
),
// 系列角标「全N集」:仅系列型分类(AI短剧)聚合卡片显示
if (item.totalEpisodes > 0)
Positioned(
left: 6,
bottom: 28,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'全${item.totalEpisodes}集',
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
),
// 标题
Positioned(
left: 0,