add
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user