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
@@ -3,6 +3,7 @@ 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/network/api_exception.dart';
import '../../../core/theme/app_theme.dart';
import '../../../models/comment_item.dart';
@@ -423,7 +424,7 @@ class CommentTile extends StatelessWidget {
radius: compact ? 12 : 16,
backgroundColor: AppColors.background,
backgroundImage: comment.avatarUrl.isNotEmpty
? CachedNetworkImageProvider(comment.avatarUrl)
? CachedNetworkImageProvider(comment.avatarUrl, cacheManager: CoverCacheManager.I)
: null,
child: comment.avatarUrl.isEmpty
? Text(
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import '../../../core/media/download_manager.dart';
import '../../../core/network/api_exception.dart';
import '../../../core/player/feed_player_pool.dart';
import '../../../core/theme/app_theme.dart';
import '../../../models/video_item.dart';
@@ -54,6 +55,15 @@ class _FeedVideoPageState extends ConsumerState<FeedVideoPage> {
static const _rates = [1.0, 1.5, 2.0, 3.0];
/// 站点管理员邮箱(长按弹窗显示「下架视频」)
static const String _kSiteAdminEmail = '256kb@gmail.com';
bool get _isSiteAdmin {
final u = ref.read(authProvider).user;
return u != null &&
(u.email ?? '').toLowerCase() == _kSiteAdminEmail;
}
@override
void initState() {
super.initState();
@@ -70,6 +80,8 @@ class _FeedVideoPageState extends ConsumerState<FeedVideoPage> {
_seriesData != null && (_seriesData!['has_series'] as bool? ?? false);
String get _seriesTitle => _seriesData?['series']?['title'] as String? ?? '';
String get _seriesSourceName =>
_seriesData?['series']?['source_name'] as String? ?? '';
List<dynamic> get _episodes => (_seriesData?['episodes'] as List?) ?? const [];
@@ -197,6 +209,21 @@ class _FeedVideoPageState extends ConsumerState<FeedVideoPage> {
},
),
// 下架视频:仅站点管理员可见(按 email 判定)
if (_isSiteAdmin)
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.remove_circle_outline,
color: Colors.redAccent),
title: const Text('下架视频',
style: TextStyle(color: Colors.redAccent)),
subtitle: const Text('从首页/搜索/分类/系列移除该视频'),
onTap: () {
Navigator.of(sheetContext).pop();
_confirmOffline();
},
),
// 下载(已下载/下载中置灰)
Builder(builder: (_) {
final dm = ref.read(downloadManagerProvider);
@@ -237,6 +264,44 @@ class _FeedVideoPageState extends ConsumerState<FeedVideoPage> {
await Share.share(text, subject: widget.item.title);
}
// ---------- 下架(管理员) ----------
Future<void> _confirmOffline() async {
final targetId = widget.item.videoId;
final title = widget.item.title;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('下架该视频?'),
content: Text(
'《$title》\n\n下架后将立刻从首页、搜索、分类、系列列表中移除,可在后台恢复。',
maxLines: 5, overflow: TextOverflow.ellipsis,
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
child: const Text('下架'),
),
],
),
);
if (confirmed != true || !mounted) return;
try {
await VideoService.offlineVideo(targetId);
if (!mounted) return;
showTopToast(context, '已下架');
} catch (e) {
if (!mounted) return;
showTopToast(context, e is ApiException ? e.message : '下架失败,请重试');
}
}
void _showSeriesSheet() {
showModalBottomSheet<void>(
context: context,
@@ -278,10 +343,32 @@ class _FeedVideoPageState extends ConsumerState<FeedVideoPage> {
maxLines: 1, overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'共 ${_episodes.length} 集',
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
Row(
children: [
if (_seriesSourceName.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(3),
),
child: Text(
_seriesSourceName,
style: const TextStyle(
fontSize: 10,
color: AppColors.primary,
fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 6),
],
Text(
'共 ${_episodes.length} 集',
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
),
],
),
],
),
@@ -3,6 +3,7 @@ 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/auth_provider.dart';
@@ -91,7 +92,7 @@ class _AuthorAvatar extends StatelessWidget {
radius: 24,
backgroundColor: AppColors.surface,
backgroundImage: item.author.avatarUrl.isNotEmpty
? CachedNetworkImageProvider(item.author.avatarUrl)
? CachedNetworkImageProvider(item.author.avatarUrl, cacheManager: CoverCacheManager.I)
: null,
child: item.author.avatarUrl.isEmpty
? Text(
@@ -32,15 +32,41 @@ class _VideoInfoSectionState extends State<VideoInfoSection> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 昵称
Text(
'@${item.author.nickname}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w700,
shadows: [Shadow(color: Colors.black54, blurRadius: 4)],
),
// 昵称 + 来源徽标
Row(
children: [
Flexible(
child: Text(
'@${item.author.nickname}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w700,
shadows: [Shadow(color: Colors.black54, blurRadius: 4)],
),
),
),
if (item.sourceName.isNotEmpty) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(4),
),
child: Text(
item.sourceName,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
],
),
const SizedBox(height: 8),