Files
video/app/lib/screens/profile/video_list_screens.dart
T
2026-10-15 20:36:59 +08:00

306 lines
10 KiB
Dart

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/network/api_exception.dart';
import '../../core/theme/app_theme.dart';
import '../../models/user_profile.dart';
import '../../models/video_item.dart';
import '../../providers/profile_provider.dart';
import '../../services/user_service.dart';
import '../../services/video_service.dart';
import '../feed/widgets/feed_overlays.dart';
/// 我的作品 / 我的收藏(双列网格,结构相同仅数据源不同)
class VideoGridScreen extends ConsumerWidget {
VideoGridScreen.works({super.key})
: title = '我的作品',
provider = myVideosProvider,
editable = true;
VideoGridScreen.favorites({super.key})
: title = '我的收藏',
provider = myFavoritesProvider,
editable = false;
final String title;
final AutoDisposeFutureProvider<PagedVideos> provider;
/// 我的作品可长按管理(编辑/删除),收藏不可
final bool editable;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncPage = ref.watch(provider);
return Scaffold(
appBar: AppBar(title: Text(title)),
body: asyncPage.when(
loading: () => const Center(
child: CircularProgressIndicator(color: AppColors.primary),
),
error: (_, __) => Center(
child: TextButton(
onPressed: () => ref.invalidate(provider),
child: const Text('加载失败,点击重试',
style: TextStyle(color: AppColors.primary)),
),
),
data: (page) => page.items.isEmpty
? const Center(
child: Text('暂无内容',
style: TextStyle(color: AppColors.textSecondary)),
)
: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.68,
),
itemCount: page.items.length,
itemBuilder: (_, index) =>
_GridCard(item: page.items[index], editable: editable),
),
),
);
}
}
class _GridCard extends ConsumerWidget {
const _GridCard({required this.item, this.editable = false});
final VideoItem item;
/// 长按弹出管理菜单(仅我的作品)
final bool editable;
/// 我的作品卡片点击:仅已发布可播放,其余状态提示原因
void _onTap(BuildContext context) {
if (item.status == -1 || item.status == 2) {
context.push('/search/play', extra: item);
return;
}
showTopToast(
context,
item.status == 3 && item.rejectReason.isNotEmpty
? '审核拒绝:${item.rejectReason}'
: '视频${item.statusText},暂不可播放',
);
}
void _showManageSheet(BuildContext context, WidgetRef ref) {
showModalBottomSheet<void>(
context: context,
backgroundColor: AppColors.surface,
builder: (sheetCtx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text('编辑信息'),
onTap: () {
Navigator.pop(sheetCtx);
context.push('/mine/works/edit', extra: item);
},
),
ListTile(
leading: const Icon(Icons.delete_outline,
color: AppColors.primary),
title: const Text('删除作品',
style: TextStyle(color: AppColors.primary)),
onTap: () {
Navigator.pop(sheetCtx);
_confirmDelete(context, ref);
},
),
],
),
),
);
}
Future<void> _confirmDelete(BuildContext context, WidgetRef ref) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppColors.surface,
title: const Text('删除作品'),
content: const Text('删除后作品立即下架且不可恢复,确定删除?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('取消',
style: TextStyle(color: AppColors.textSecondary)),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child:
const Text('删除', style: TextStyle(color: AppColors.primary)),
),
],
),
);
if (confirmed != true || !context.mounted) return;
try {
await VideoService.deleteVideo(item.videoId);
ref.invalidate(myVideosProvider);
if (context.mounted) showTopToast(context, '已删除');
} on ApiException catch (e) {
if (context.mounted) showTopToast(context, e.message);
} catch (_) {
if (context.mounted) showTopToast(context, '网络异常,请稍后重试');
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () => _onTap(context),
onLongPress: editable ? () => _showManageSheet(context, ref) : null,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: AppColors.surface,
child: item.coverUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: item.coverUrl,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => const Icon(
Icons.play_circle_outline,
color: AppColors.textWeak,
size: 40,
),
)
: const Icon(
Icons.play_circle_outline,
color: AppColors.textWeak,
size: 40,
),
),
// 作品状态角标(我的作品:待审核/拒绝/下架等)
if (item.statusText.isNotEmpty)
Positioned(
left: 6,
top: 6,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: item.status == 3
? AppColors.primary
: item.status == 2
? AppColors.secondary
: Colors.orange,
borderRadius: BorderRadius.circular(4),
),
child: Text(
item.statusText,
style: const TextStyle(
color: Colors.black,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.all(6),
child: Text(
item.title.isNotEmpty ? item.title : '未命名视频',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
),
],
),
),
);
}
}
/// 观看历史(列表:封面缩略图 + 标题 + 观看时间 + 计费方式)
class HistoryScreen extends ConsumerWidget {
const HistoryScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncList = ref.watch(myHistoryProvider);
return Scaffold(
appBar: AppBar(title: const Text('观看历史')),
body: asyncList.when(
loading: () => const Center(
child: CircularProgressIndicator(color: AppColors.primary),
),
error: (_, __) => Center(
child: TextButton(
onPressed: () => ref.invalidate(myHistoryProvider),
child: const Text('加载失败,点击重试',
style: TextStyle(color: AppColors.primary)),
),
),
data: (items) => items.isEmpty
? const Center(
child: Text('暂无观看记录',
style: TextStyle(color: AppColors.textSecondary)),
)
: ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) => _HistoryTile(item: items[index]),
),
),
);
}
}
class _HistoryTile extends StatelessWidget {
const _HistoryTile({required this.item});
final HistoryItem item;
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () => context.push('/search/play', extra: item.video),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Container(
width: 72,
height: 48,
color: AppColors.surface,
child: item.video.coverUrl.isNotEmpty
? CachedNetworkImage(
imageUrl: item.video.coverUrl,
fit: BoxFit.cover,
errorWidget: (_, __, ___) =>
const Icon(Icons.play_circle_outline, size: 24),
)
: const Icon(Icons.play_circle_outline, size: 24),
),
),
title: Text(
item.video.title.isNotEmpty ? item.video.title : '未命名视频',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${item.watchedAt.length >= 16 ? item.watchedAt.substring(5, 16) : item.watchedAt} · ${item.costLabel}',
style: const TextStyle(color: AppColors.textWeak, fontSize: 12),
),
);
}
}