feat(app): 完成 P0 钱包解锁、下载落盘与手机号登录
This commit is contained in:
1 parent
de941c841c
commit
136ee74618
47 files changed
+3264
-70
No files matched your search
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user