feat(app): 完成 P0 钱包解锁、下载落盘与手机号登录

This commit is contained in:
Zhou Weidong committed 2026-10-19 23:46:47 +08:00
1 parent de941c841c
commit 136ee74618
47 files changed
+3264 -70

No files matched your search

+74 -1
View File
@@ -10,9 +10,11 @@ import 'package:video_player/video_player.dart';
import '../../core/data/models/drama.dart';
import '../../core/data/models/play_ticket.dart';
import '../../core/data/services/favorite_controller.dart';
import '../../core/data/services/download_controller.dart';
import '../../core/data/services/interaction_controller.dart';
import '../../core/data/services/playback_session.dart';
import '../../core/data/services/video_player_port.dart';
import '../../core/data/services/wallet_controller.dart';
import '../../core/formatting/error_text.dart';
import '../../core/formatting/localized_content.dart';
import '../../core/platform/haptics.dart';
@@ -48,6 +50,8 @@ class PlayerPage extends StatefulWidget {
this.feedDramas,
this.onDramaChanged,
this.onImmersiveChanged,
this.wallet,
this.downloads,
});
final Drama drama;
@@ -64,6 +68,8 @@ class PlayerPage extends StatefulWidget {
final List<Drama>? feedDramas;
final ValueChanged<Drama>? onDramaChanged;
final ValueChanged<bool>? onImmersiveChanged;
final WalletController? wallet;
final DownloadController? downloads;
@override
State<PlayerPage> createState() => _PlayerPageState();
@@ -80,6 +86,8 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
logic = PlayerBinding().create(
history: context.read<WatchHistoryRepository>(),
playback: context.read<PlaybackRepository>(),
wallet: widget.wallet,
downloads: widget.downloads,
);
logic.attach(widget, isMounted: () => mounted);
WidgetsBinding.instance.addObserver(this);
@@ -285,6 +293,11 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
episode: episode,
accessType: logic.blockedTicket?.accessType,
unlockCoin: logic.blockedTicket?.unlockCoin ?? 0,
unlocking: logic.unlockingEpisodeId == episode.id,
error: logic.unlockError,
onUnlock: logic.wallet == null
? null
: () => _unlockEpisode(context, index),
),
// 没有票据信息时的兜底(例如详情里就没地址、也没走票据流程)
if (selected &&
@@ -484,6 +497,21 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
setState(() => _playbackSpeed = selected);
await logic.setPlaybackSpeed(selected);
}
Future<void> _unlockEpisode(BuildContext context, int index) async {
if (!await ensureSignedIn(context)) return;
if (!context.mounted) return;
final ok = await logic.unlockEpisode(index);
if (!context.mounted || ok) return;
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorText(l10n, logic.unlockError, l10n.unlockFailed),
),
),
);
}
}
/// 不可播放提示:文案由后端返回的 `accessType` 驱动(契约 §1.4),前端不自行推断。
@@ -493,12 +521,18 @@ class _BlockedNotice extends StatelessWidget {
required this.episode,
required this.accessType,
required this.unlockCoin,
required this.unlocking,
required this.error,
required this.onUnlock,
});
final AppLocalizations l10n;
final Episode episode;
final String? accessType;
final int unlockCoin;
final bool unlocking;
final String? error;
final VoidCallback? onUnlock;
@override
Widget build(BuildContext context) {
@@ -521,7 +555,12 @@ class _BlockedNotice extends StatelessWidget {
coinLabel: needsUnlock && unlockCoin > 0
? '$unlockCoin ${l10n.coins}'
: null,
footnote: needsUnlock ? l10n.unlockComingSoon : null,
footnote: needsUnlock && error != null
? errorText(l10n, error, l10n.unlockFailed)
: null,
actionLabel: needsUnlock ? l10n.unlockWithCoins : null,
busy: unlocking,
onAction: needsUnlock ? onUnlock : null,
),
),
);
@@ -535,12 +574,18 @@ class _UnlockCard extends StatelessWidget {
required this.title,
required this.coinLabel,
required this.footnote,
this.actionLabel,
this.busy = false,
this.onAction,
});
final bool locked;
final String title;
final String? coinLabel;
final String? footnote;
final String? actionLabel;
final bool busy;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
@@ -601,6 +646,34 @@ class _UnlockCard extends StatelessWidget {
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
],
if (actionLabel != null) ...[
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
height: 40,
child: FilledButton(
key: const Key('unlock-with-coins'),
onPressed: busy ? null : onAction,
style: FilledButton.styleFrom(
backgroundColor: AppColors.gold,
foregroundColor: AppColors.onGold,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: busy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(
actionLabel!,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
),
],
],
),
),