1
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user