// quota_card.dart — 免费版每日额度卡 // // 三态展示(额度全账户共享): // ① 连接中(countdown != null):显示剩余倒计时 mm:ss + 进度条随之收缩。 // ② 未连接·有余额:显示今日剩余分钟 + 进度条;移动端附「看广告加时」。 // ③ 未连接·已耗尽:显示「今日已用完」+ 移动端「看广告加时」/ 桌面「升级会员」。 // 状态由 quota_provider(剩余)+ connection_provider(倒计时)注入,本地仅展示。 import 'package:flutter/material.dart'; import '../l10n/app_text.dart'; import '../pangolin_theme.dart'; import '../state/quota_provider.dart'; import 'pangolin_icons.dart'; class QuotaCard extends StatelessWidget { const QuotaCard({ super.key, required this.quota, required this.t, required this.onWatchAd, this.countdown, this.isDesktop = false, }); final FreeQuotaState quota; final AppText t; /// 额度耗尽时的加时入口(移动端占位广告 / 桌面升级提示)。 final VoidCallback onWatchAd; /// 连接期剩余倒计时;null = 未连接(展示今日剩余分钟)。 final Duration? countdown; /// 桌面端(Windows/macOS):免费硬 10 分钟不可延,不显示看广告按钮。 final bool isDesktop; static String _mmss(Duration d) { final m = d.inMinutes.remainder(60).toString().padLeft(2, '0'); final s = d.inSeconds.remainder(60).toString().padLeft(2, '0'); return '$m:$s'; } @override Widget build(BuildContext context) { final c = context.pangolin; final connected = countdown != null; final exhausted = quota.isExhausted && !connected; // 进度与主值:连接期用倒计时,未连接用剩余分钟。 final double progress; final String label; final String value; if (connected) { final totalSec = quota.totalMinutes * 60; progress = totalSec == 0 ? 0 : (countdown!.inSeconds / totalSec).clamp(0.0, 1.0); label = t.quotaLeftLabel; value = _mmss(countdown!); } else { progress = quota.progress; label = exhausted ? t.quotaUsedUp : t.quotaToday; value = exhausted ? '' : '${quota.remainingMinutes} ${t.minutes}'; } final low = connected ? countdown!.inMinutes < 3 : quota.isLow; final barColor = exhausted ? c.danger : (low ? c.warning : c.accent); return Container( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ Icon(PangolinIcons.clock, size: 15, color: exhausted ? c.danger : c.accent), const SizedBox(width: 7), // 标签固定、绝不省略(此前用 Flexible 让「今日剩余」被挤成「今日…」)。 Text(label, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600, fontSize: 12)), if (value.isNotEmpty) ...[ const SizedBox(width: 7), // 数值可作为最后收缩项(极端窄屏/三位数分钟才会省略)。 Flexible( child: Text(value, overflow: TextOverflow.ellipsis, style: PangolinText.mono.copyWith(color: c.fg1, fontSize: 14, fontWeight: FontWeight.w600)), ), ], const SizedBox(width: 8), const Spacer(), // 短标签「免费版」——每日额度由卡片本身表达,不再挤进右上角。 Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), decoration: BoxDecoration(color: c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), child: Text(t.planFreeTag, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600, fontSize: 10.5)), ), ]), const SizedBox(height: 9), // 进度条:剩余比例;≤3 分钟 warning、耗尽 danger(满宽轨道 + 比例填充)。 ClipRRect( borderRadius: BorderRadius.circular(3), child: SizedBox( height: 6, child: Stack(children: [ Positioned.fill(child: ColoredBox(color: c.bgSubtle)), FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: progress, child: AnimatedContainer( duration: PangolinMotion.base, curve: PangolinMotion.easeOut, decoration: BoxDecoration(color: barColor, borderRadius: BorderRadius.circular(3)), ), ), ]), ), ), // 加时按钮:未连接时显示。移动端「看广告加时」;桌面仅耗尽时给「升级会员」。 if (!connected && (!isDesktop || exhausted)) ...[ const SizedBox(height: 11), _ActionButton( label: isDesktop ? t.upgrade : t.watchAdMore, icon: isDesktop ? PangolinIcons.zap : PangolinIcons.playCircle, onTap: onWatchAd, ), ], ], ), ); } } class _ActionButton extends StatelessWidget { const _ActionButton({required this.label, required this.icon, required this.onTap}); final String label; final IconData icon; final VoidCallback onTap; @override Widget build(BuildContext context) { final c = context.pangolin; return Material( color: c.accentSubtle, shape: StadiumBorder(side: BorderSide(color: c.accentBorder, width: 1.5)), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: SizedBox( height: 38, child: Center( child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: c.accent), const SizedBox(width: 7), Text(label, style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w700, fontSize: 13)), ]), ), ), ), ); } }