306 lines
10 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|