// 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.goldCard, 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 Function()? onUnlock; @override State createState() => _PlayerUnlockSheetState(); } class _PlayerUnlockSheetState extends State { bool _busy = false; String? _error; Future _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), ], ), ), ), ); } }