374 lines
11 KiB
Dart
374 lines
11 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../core/data/models/drama.dart';
|
|
import '../../../core/data/models/play_ticket.dart';
|
|
import '../../../core/formatting/error_text.dart';
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../l10n/app_localizations.dart';
|
|
|
|
const double _cardRadius = 20;
|
|
const double _optionRadius = 14;
|
|
const double _sheetWidth = 360;
|
|
|
|
/// 视频区占位提示:由后端票据决定是解锁、无片源还是会员限制。
|
|
class PlayerBlockedNotice extends StatelessWidget {
|
|
/// 创建不可播放提示。
|
|
const PlayerBlockedNotice({
|
|
super.key,
|
|
required this.l10n,
|
|
required this.episode,
|
|
required this.accessType,
|
|
required this.unlockCoin,
|
|
required this.unlocking,
|
|
required this.error,
|
|
required this.onOpenUnlock,
|
|
});
|
|
|
|
/// 当前语言包。
|
|
final AppLocalizations l10n;
|
|
|
|
/// 当前剧集。
|
|
final Episode episode;
|
|
|
|
/// 后端下发的内容访问类型。
|
|
final String? accessType;
|
|
|
|
/// 金币解锁价格。
|
|
final int unlockCoin;
|
|
|
|
/// 是否正在解锁。
|
|
final bool unlocking;
|
|
|
|
/// 最近一次解锁失败信息。
|
|
final String? error;
|
|
|
|
/// 打开内容解锁面板。
|
|
final VoidCallback? onOpenUnlock;
|
|
|
|
bool get _needsUnlock =>
|
|
accessType == PlayTicket.coinRequired ||
|
|
accessType == PlayTicket.vipRequired;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final title = _needsUnlock
|
|
? l10n.lockedTitle(episode.number)
|
|
: accessType == PlayTicket.noMedia || !episode.locked
|
|
? l10n.noMediaNotice
|
|
: l10n.lockedTitle(episode.number);
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: _sheetWidth),
|
|
child: Container(
|
|
key: const Key('player-unlock-notice'),
|
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
|
padding: const EdgeInsets.fromLTRB(20, 22, 20, 18),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.card,
|
|
borderRadius: BorderRadius.circular(_cardRadius),
|
|
boxShadow: const [
|
|
BoxShadow(
|
|
color: AppColors.cardShadow,
|
|
blurRadius: 18,
|
|
offset: Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
_needsUnlock ? Icons.lock_rounded : Icons.movie_filter_outlined,
|
|
color: _needsUnlock ? AppColors.blue : AppColors.muted,
|
|
size: 30,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
title,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
color: AppColors.onSurface,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
if (_needsUnlock && unlockCoin > 0) ...[
|
|
const SizedBox(height: 10),
|
|
_coinLabel(l10n),
|
|
],
|
|
if (error != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
errorText(l10n, error, l10n.unlockFailed),
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
color: AppColors.badgePinkText,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
if (_needsUnlock) ...[
|
|
const SizedBox(height: 14),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton(
|
|
key: const Key('player-open-unlock'),
|
|
onPressed: unlocking ? null : onOpenUnlock,
|
|
child: unlocking
|
|
? const SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Text(l10n.unlockEpisode),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _coinLabel(AppLocalizations l10n) => DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.homeCrownGold,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
child: Text(
|
|
l10n.walletBalanceShort(unlockCoin),
|
|
style: const TextStyle(
|
|
color: AppColors.onGold,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 内容解锁面板:金币可用,广告与会员在契约缺失时明确禁用。
|
|
class PlayerUnlockSheet extends StatefulWidget {
|
|
/// 创建内容解锁面板。
|
|
const PlayerUnlockSheet({
|
|
super.key,
|
|
required this.l10n,
|
|
required this.episodeNumber,
|
|
required this.coinCost,
|
|
required this.balance,
|
|
required this.onUnlock,
|
|
});
|
|
|
|
/// 当前语言包。
|
|
final AppLocalizations l10n;
|
|
|
|
/// 当前集号。
|
|
final int episodeNumber;
|
|
|
|
/// 金币解锁价格。
|
|
final int coinCost;
|
|
|
|
/// 当前金币余额;未知时隐藏。
|
|
final int? balance;
|
|
|
|
/// 金币解锁动作,成功后返回 true。
|
|
final Future<bool> Function()? onUnlock;
|
|
|
|
@override
|
|
State<PlayerUnlockSheet> createState() => _PlayerUnlockSheetState();
|
|
}
|
|
|
|
class _PlayerUnlockSheetState extends State<PlayerUnlockSheet> {
|
|
bool _busy = false;
|
|
String? _error;
|
|
|
|
Future<void> _unlock() async {
|
|
final action = widget.onUnlock;
|
|
if (action == null || _busy) return;
|
|
setState(() {
|
|
_busy = true;
|
|
_error = null;
|
|
});
|
|
final success = await action();
|
|
if (!mounted) return;
|
|
if (success) {
|
|
Navigator.of(context).pop(true);
|
|
return;
|
|
}
|
|
setState(() {
|
|
_busy = false;
|
|
_error = widget.l10n.unlockFailed;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = widget.l10n;
|
|
return Container(
|
|
key: const Key('player-unlock-panel'),
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.card,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(_cardRadius)),
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
l10n.lockedTitle(widget.episodeNumber),
|
|
style: const TextStyle(
|
|
color: AppColors.onSurface,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
key: const Key('player-unlock-close'),
|
|
tooltip: l10n.close,
|
|
onPressed: () => Navigator.pop(context),
|
|
icon: const Icon(Icons.close_rounded),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
_UnlockOption(
|
|
optionKey: const Key('player-unlock-coins'),
|
|
icon: Icons.toll_rounded,
|
|
title: l10n.unlockWithCoins,
|
|
subtitle: widget.balance == null
|
|
? l10n.walletBalanceShort(widget.coinCost)
|
|
: '${l10n.walletBalanceShort(widget.coinCost)} · '
|
|
'${l10n.walletBalanceShort(widget.balance!)}',
|
|
enabled: widget.onUnlock != null && !_busy,
|
|
busy: _busy,
|
|
onTap: _unlock,
|
|
),
|
|
const SizedBox(height: 10),
|
|
_UnlockOption(
|
|
optionKey: const Key('player-unlock-ad'),
|
|
icon: Icons.ondemand_video_rounded,
|
|
title: l10n.watchAdToUnlock,
|
|
subtitle: l10n.comingSoon,
|
|
enabled: false,
|
|
onTap: null,
|
|
),
|
|
const SizedBox(height: 10),
|
|
_UnlockOption(
|
|
optionKey: const Key('player-unlock-vip'),
|
|
icon: Icons.workspace_premium_rounded,
|
|
title: l10n.unlockWithVip,
|
|
subtitle: l10n.comingSoon,
|
|
enabled: false,
|
|
onTap: null,
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
_error!,
|
|
style: const TextStyle(
|
|
color: AppColors.badgePinkText,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
l10n.unlockMethodsVary,
|
|
style: const TextStyle(color: AppColors.muted, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _UnlockOption extends StatelessWidget {
|
|
const _UnlockOption({
|
|
required this.optionKey,
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.enabled,
|
|
this.busy = false,
|
|
this.onTap,
|
|
});
|
|
|
|
final Key optionKey;
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final bool enabled;
|
|
final bool busy;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
key: optionKey,
|
|
color: enabled ? AppColors.quick : AppColors.raised,
|
|
borderRadius: BorderRadius.circular(_optionRadius),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(_optionRadius),
|
|
onTap: enabled ? onTap : null,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: enabled ? AppColors.blue : AppColors.subtle,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
color: enabled ? AppColors.onSurface : AppColors.subtle,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(
|
|
color: AppColors.muted,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (busy)
|
|
const SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
else if (enabled)
|
|
const Icon(Icons.chevron_right_rounded, color: AppColors.blue),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|