636 lines
22 KiB
Dart
636 lines
22 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../core/data/models/drama.dart';
|
|
import '../../core/data/repositories/comment_repository.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/navigation/app_route_observer.dart';
|
|
import '../../core/platform/haptics.dart';
|
|
import '../../core/theme/app_colors.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../core/widgets/auth_gate.dart';
|
|
import '../../core/widgets/download_action.dart';
|
|
import '../../l10n/app_localizations.dart';
|
|
import '../home/home_feed_channel.dart';
|
|
import 'comments_auth.dart';
|
|
import 'player_binding.dart';
|
|
import 'player_logic.dart';
|
|
import 'widgets/home_feed_header.dart';
|
|
import 'widgets/home_feed_overlay.dart';
|
|
import 'widgets/exit_clear_screen_button.dart';
|
|
import 'widgets/player_center_toggle.dart';
|
|
import 'widgets/player_more_sheet.dart';
|
|
import 'widgets/player_overlay.dart';
|
|
import 'widgets/player_overlay_models.dart';
|
|
import 'widgets/player_speed_sheet.dart';
|
|
import 'widgets/player_unlock_sheet.dart';
|
|
import 'widgets/player_video_surface.dart';
|
|
import '../../core/data/repositories/watch_history_repository.dart';
|
|
import '../../core/data/repositories/playback_repository.dart';
|
|
|
|
part 'player_view_actions.dart';
|
|
|
|
const double _boostPlaybackSpeed = 2.0;
|
|
|
|
class PlayerPage extends StatefulWidget {
|
|
const PlayerPage({
|
|
super.key,
|
|
required this.drama,
|
|
required this.initialIndex,
|
|
this.homeMode = false,
|
|
this.embedded = false,
|
|
this.active = true,
|
|
this.resumeRequest = 0,
|
|
this.backRequest = 0,
|
|
this.onDiscover,
|
|
this.onMy,
|
|
this.onSearch,
|
|
this.onMenu,
|
|
this.feedDramas,
|
|
this.feedChannel,
|
|
this.headerChannel,
|
|
this.onDramaChanged,
|
|
this.onImmersiveChanged,
|
|
this.onOpenFullPlayer,
|
|
this.onRefreshFeed,
|
|
this.onLoadMore,
|
|
this.onChannelSelected,
|
|
this.wallet,
|
|
this.downloads,
|
|
});
|
|
|
|
final Drama drama;
|
|
final int initialIndex;
|
|
final bool homeMode;
|
|
final bool embedded;
|
|
final bool active;
|
|
final int resumeRequest;
|
|
final int backRequest;
|
|
final VoidCallback? onDiscover;
|
|
final VoidCallback? onMy;
|
|
final VoidCallback? onSearch;
|
|
final VoidCallback? onMenu;
|
|
final List<Drama>? feedDramas;
|
|
final HomeFeedChannel? feedChannel;
|
|
final HomeFeedChannel? headerChannel;
|
|
final ValueChanged<Drama>? onDramaChanged;
|
|
final ValueChanged<bool>? onImmersiveChanged;
|
|
final Future<void> Function(Drama drama, Episode episode)? onOpenFullPlayer;
|
|
final Future<bool> Function()? onRefreshFeed;
|
|
final Future<void> Function()? onLoadMore;
|
|
final ValueChanged<HomeFeedChannel>? onChannelSelected;
|
|
final WalletController? wallet;
|
|
final DownloadController? downloads;
|
|
|
|
@override
|
|
State<PlayerPage> createState() => _PlayerPageState();
|
|
}
|
|
|
|
class _PlayerPageState extends State<PlayerPage>
|
|
with WidgetsBindingObserver, RouteAware {
|
|
late final PlayerLogic logic;
|
|
PageRoute<dynamic>? _subscribedRoute;
|
|
bool _chromeVisible = true;
|
|
double _playbackSpeed = 1.0;
|
|
bool _boosting = false;
|
|
double? _speedBeforeBoost;
|
|
bool? _chromeBeforeBoost;
|
|
|
|
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
|
final Map<String, int> _favoriteDelta = {};
|
|
final Map<String, int> _commentCounts = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
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);
|
|
logic.start(
|
|
factory: context.read<PlayerFactory>(),
|
|
coordinator: context.read<PlaybackCoordinator>(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant PlayerPage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
logic.attach(widget, isMounted: () => mounted);
|
|
final channelChanged = oldWidget.feedChannel != widget.feedChannel;
|
|
final feedChanged =
|
|
_feedSignature(oldWidget.feedDramas) !=
|
|
_feedSignature(widget.feedDramas);
|
|
if (widget.homeMode &&
|
|
widget.feedDramas != null &&
|
|
(feedChanged || channelChanged)) {
|
|
final feedDramas = widget.feedDramas!;
|
|
final keepDramaId = channelChanged ? null : logic.currentDramaId;
|
|
final resetToFirst = channelChanged;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
logic.updateFeed(
|
|
feedDramas,
|
|
keepDramaId: keepDramaId,
|
|
resetToFirst: resetToFirst,
|
|
);
|
|
});
|
|
}
|
|
logic.onPageUpdated(oldWidget);
|
|
}
|
|
|
|
String _feedSignature(List<Drama>? dramas) =>
|
|
dramas == null ? '' : dramas.map((drama) => drama.id).join('|');
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
final route = ModalRoute.of(context);
|
|
if (route is! PageRoute<dynamic> || route == _subscribedRoute) return;
|
|
if (_subscribedRoute != null) appRouteObserver.unsubscribe(this);
|
|
_subscribedRoute = route;
|
|
appRouteObserver.subscribe(this, route);
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
logic.onLifecycle(state);
|
|
}
|
|
|
|
@override
|
|
void didPushNext() {
|
|
logic.onRouteCovered();
|
|
}
|
|
|
|
@override
|
|
void didPopNext() {
|
|
logic.onRouteRevealed();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
appRouteObserver.unsubscribe(this);
|
|
logic.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _updatePlaybackSpeed(double value) {
|
|
setState(() => _playbackSpeed = value);
|
|
}
|
|
|
|
void _hideChrome() {
|
|
setState(() => _chromeVisible = false);
|
|
}
|
|
|
|
/// 长按两侧:切 2 倍速并清屏,记下进入前的状态用于松手恢复。
|
|
void _startBoost() {
|
|
if (_boosting) return;
|
|
// 暂停时长按两侧先恢复播放,再进入 2 倍速。
|
|
if (!logic.session.userWantsPlay) logic.session.setUserWantsPlay(true);
|
|
setState(() {
|
|
_boosting = true;
|
|
_speedBeforeBoost = _playbackSpeed;
|
|
_chromeBeforeBoost = _chromeVisible;
|
|
_playbackSpeed = _boostPlaybackSpeed;
|
|
_chromeVisible = false;
|
|
});
|
|
unawaited(logic.setPlaybackSpeed(_boostPlaybackSpeed));
|
|
}
|
|
|
|
/// 松手或手势取消:恢复长按前的倍速与清屏状态。
|
|
void _endBoost() {
|
|
if (!_boosting) return;
|
|
final speed = _speedBeforeBoost ?? _playbackSpeed;
|
|
final chrome = _chromeBeforeBoost ?? true;
|
|
setState(() {
|
|
_boosting = false;
|
|
_speedBeforeBoost = null;
|
|
_chromeBeforeBoost = null;
|
|
_playbackSpeed = speed;
|
|
_chromeVisible = chrome;
|
|
});
|
|
unawaited(logic.setPlaybackSpeed(speed));
|
|
}
|
|
|
|
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
|
|
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
|
|
setState(() {
|
|
_favoriteDelta[dramaId] =
|
|
(_favoriteDelta[dramaId] ?? 0) + (nowSaved ? 1 : -1);
|
|
});
|
|
}
|
|
|
|
Future<void> _openComments(
|
|
BuildContext context,
|
|
Drama drama,
|
|
Episode episode,
|
|
) async {
|
|
final comments = context.read<CommentRepository>();
|
|
await openCommentsWithAuth(
|
|
context,
|
|
drama,
|
|
show: (context, drama) =>
|
|
logic.showComments(context, drama, episodeId: episode.id),
|
|
);
|
|
try {
|
|
final count = await comments.count(drama.id, episodeId: episode.id);
|
|
if (!mounted) return;
|
|
setState(() => _commentCounts[episode.id] = count);
|
|
} catch (_) {
|
|
// 评论面板已经关闭,计数刷新失败时保留旧值即可。
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: logic,
|
|
builder: (context, _) => _buildPlayer(context),
|
|
);
|
|
}
|
|
|
|
Widget _buildPlayer(BuildContext context) {
|
|
final interactions = context.watch<InteractionController>();
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final indices = logic.visibleIndices;
|
|
final pages = logic.pages;
|
|
final content = AnimatedBuilder(
|
|
animation: logic.session,
|
|
builder: (context, _) => PageView.builder(
|
|
controller: pages,
|
|
scrollDirection: Axis.vertical,
|
|
allowImplicitScrolling: true,
|
|
itemCount: indices.length,
|
|
onPageChanged: (position) {
|
|
final index = indices[position];
|
|
widget.onDramaChanged?.call(logic.queue.entryAt(index).drama);
|
|
unawaited(logic.selectEpisode(index));
|
|
if (position >= indices.length - 3) {
|
|
final loadMore = widget.onLoadMore;
|
|
if (loadMore != null) unawaited(loadMore());
|
|
}
|
|
},
|
|
itemBuilder: (context, position) =>
|
|
_buildEpisode(context, indices[position], interactions),
|
|
),
|
|
);
|
|
final body = Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
content,
|
|
if (widget.homeMode && !_boosting && !logic.pip && !logic.pipPreparing)
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: HomeFeedHeader(
|
|
l10n: l10n,
|
|
channel:
|
|
widget.headerChannel ??
|
|
widget.feedChannel ??
|
|
HomeFeedChannel.recommend,
|
|
onChannelSelected: widget.onChannelSelected ?? (_) {},
|
|
onRefresh: () async {
|
|
final ok = await widget.onRefreshFeed?.call() ?? true;
|
|
if (context.mounted && !ok) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(l10n.catalogError)));
|
|
}
|
|
return ok;
|
|
},
|
|
onSearch: widget.onSearch ?? () => logic.showDiscover(context),
|
|
onMore: _openCurrentMore,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
key: const Key('player-system-overlay'),
|
|
value: SystemUiOverlayStyle.light.copyWith(
|
|
statusBarColor: Colors.transparent,
|
|
systemNavigationBarColor: Colors.black,
|
|
systemNavigationBarDividerColor: Colors.transparent,
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
|
systemNavigationBarContrastEnforced: false,
|
|
),
|
|
child: Theme(
|
|
data: AppTheme.light(),
|
|
child: widget.embedded
|
|
? body
|
|
: Scaffold(
|
|
key: const Key('player-scaffold'),
|
|
backgroundColor: Colors.transparent,
|
|
resizeToAvoidBottomInset: false,
|
|
body: body,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openCurrentMore() {
|
|
final entry = logic.queue.entryAt(logic.session.currentIndex);
|
|
_openMore(context, entry.episode);
|
|
}
|
|
|
|
Widget _buildEpisode(
|
|
BuildContext context,
|
|
int index,
|
|
InteractionController interactions,
|
|
) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final entry = logic.queue.entryAt(index);
|
|
final drama = entry.drama;
|
|
final episode = widget.homeMode
|
|
? logic.displayEpisode(drama, entry.episode)
|
|
: entry.episode;
|
|
final selected = index == logic.session.currentIndex;
|
|
// Selection changes before the prepared slot is promoted. Use its controller
|
|
// throughout the handoff instead of briefly falling back to a placeholder.
|
|
final port =
|
|
(selected ? logic.session.active : null) ??
|
|
logic.session.preparedPlayerAt(index);
|
|
final videoPort = port is VideoPlayerPort ? port : null;
|
|
final ready = videoPort?.controller.value.isInitialized ?? false;
|
|
final theater = logic.pip || logic.pipPreparing;
|
|
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
if (widget.homeMode)
|
|
const ColoredBox(
|
|
key: Key('home-feed-black-background'),
|
|
color: Colors.black,
|
|
),
|
|
PlayerVideoSurface(
|
|
controller: videoPort?.controller,
|
|
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
|
theater: theater,
|
|
onTap: () =>
|
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
|
),
|
|
if (widget.homeMode && _chromeVisible && !logic.chromeHidden)
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
key: const Key('home-feed-video-toggle'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () =>
|
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
|
),
|
|
),
|
|
// 票据三态之一:起播前正在向后端要签名地址
|
|
if (selected && logic.ticketLoading)
|
|
const Center(
|
|
child: CircularProgressIndicator(
|
|
key: Key('player-ticket-loading'),
|
|
color: AppColors.blue,
|
|
),
|
|
),
|
|
// 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载
|
|
if (selected && !logic.ticketLoading && logic.ticketError != null)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 36),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.wifi_off_rounded,
|
|
color: Colors.white70,
|
|
size: 36,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
errorText(l10n, logic.ticketError, l10n.playbackError),
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 12),
|
|
FilledButton(
|
|
onPressed: () => unawaited(logic.retryTicket()),
|
|
child: Text(l10n.retry),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (selected && logic.session.loading)
|
|
const Center(
|
|
child: CircularProgressIndicator(
|
|
key: Key('player-session-loading'),
|
|
color: AppColors.blue,
|
|
),
|
|
),
|
|
if (selected && !ready && logic.session.error != null)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 36),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline_rounded,
|
|
color: Colors.white70,
|
|
size: 36,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
l10n.playbackError,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.white70),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// 没有票据信息时的兜底(例如详情里就没地址、也没走票据流程)
|
|
if (selected &&
|
|
!ready &&
|
|
logic.blockedEpisodeId != episode.id &&
|
|
logic.session.error == null &&
|
|
logic.session.isLocked)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 36),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.movie_filter_outlined,
|
|
color: Colors.white70,
|
|
size: 36,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
episode.locked
|
|
? l10n.lockedTitle(episode.number)
|
|
: l10n.noMediaNotice,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.white70),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (logic.pip || logic.pipPreparing)
|
|
const SizedBox.expand()
|
|
else if (widget.homeMode)
|
|
HomeFeedOverlay(
|
|
l10n: l10n,
|
|
drama: drama,
|
|
episode: episode,
|
|
controller: ready ? videoPort!.controller : null,
|
|
visible: _chromeVisible,
|
|
boosting: _boosting,
|
|
onBoostStart: _startBoost,
|
|
onBoostEnd: _endBoost,
|
|
onTogglePlay: () =>
|
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
|
liked: interactions.isEpisodeLiked(episode.id),
|
|
likeCount:
|
|
interactions.episodeLikeCount(episode.id) ?? episode.likeCount,
|
|
favoriteCount: drama.favoriteCount == null
|
|
? null
|
|
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
|
commentCount: _commentCounts[episode.id] ?? episode.commentCount,
|
|
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
|
onLike: () async {
|
|
if (await ensureSignedIn(context)) {
|
|
if (!context.mounted) return;
|
|
unawaited(context.read<Haptics>().lightImpact());
|
|
final result = await interactions.toggleEpisodeLike(episode.id);
|
|
if (!result.isSuccess && context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
result.errorMessage ??
|
|
AppLocalizations.of(context)!.likeFailed,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
onSave: () => unawaited(_toggleSave(context, drama)),
|
|
onComments: () => unawaited(_openComments(context, drama, episode)),
|
|
onShare: () => logic.shareDrama(context, drama),
|
|
onDetail: () => unawaited(logic.openDetail(context)),
|
|
onOpenFullPlayer: () async {
|
|
await widget.onOpenFullPlayer?.call(drama, episode);
|
|
if (!mounted || !widget.active) return;
|
|
logic.session.setUserWantsPlay(true);
|
|
},
|
|
onMore: _openCurrentMore,
|
|
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
|
onSeek: ready
|
|
? (position) => logic.seekTo(context, videoPort!, position)
|
|
: null,
|
|
)
|
|
else
|
|
PlayerPlaybackOverlay(
|
|
data: PlayerOverlayData(
|
|
l10n: l10n,
|
|
drama: drama,
|
|
episode: episode,
|
|
controller: ready ? videoPort!.controller : null,
|
|
liked: interactions.isEpisodeLiked(episode.id),
|
|
likeCount:
|
|
interactions.episodeLikeCount(episode.id) ??
|
|
episode.likeCount,
|
|
commentCount: _commentCounts[episode.id] ?? episode.commentCount,
|
|
favoriteCount: drama.favoriteCount == null
|
|
? null
|
|
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
|
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
|
playing: logic.session.userWantsPlay,
|
|
playbackSpeed: _playbackSpeed,
|
|
totalEpisodes: logic.visibleIndices.length,
|
|
boosting: _boosting,
|
|
),
|
|
actions: PlayerOverlayActions(
|
|
onBack: widget.embedded
|
|
? null
|
|
: () => unawaited(logic.leave(context)),
|
|
onTogglePlay: () =>
|
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
|
onClear: () => setState(() => _chromeVisible = false),
|
|
onExitClear: () => setState(() => _chromeVisible = true),
|
|
onDetail: () => unawaited(logic.openDetail(context)),
|
|
onSpeed: () => unawaited(_openSpeedSheet(context)),
|
|
onEpisodes: () => logic.showEpisodes(context),
|
|
onComments: () =>
|
|
unawaited(_openComments(context, drama, episode)),
|
|
onLike: () =>
|
|
unawaited(_toggleLike(context, interactions, episode)),
|
|
onSave: () => unawaited(_toggleSave(context, drama)),
|
|
onShare: () => logic.shareDrama(context, drama),
|
|
onMore: () => _openMore(context, episode),
|
|
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
|
onSeek: ready
|
|
? (position) => logic.seekTo(context, videoPort!, position)
|
|
: null,
|
|
onLongPressCenter: () => _openMore(context, episode),
|
|
onBoostStart: _startBoost,
|
|
onBoostEnd: _endBoost,
|
|
),
|
|
controlsVisible: _chromeVisible,
|
|
),
|
|
// 票据三态之三:后端判定不可播放 —— 用 accessType 驱动文案,不在前端猜
|
|
if (selected && !ready && logic.blockedEpisodeId == episode.id)
|
|
PlayerBlockedNotice(
|
|
l10n: l10n,
|
|
episode: episode,
|
|
accessType: logic.blockedTicket?.accessType,
|
|
unlockCoin: logic.blockedTicket?.unlockCoin ?? 0,
|
|
unlocking: logic.unlockingEpisodeId == episode.id,
|
|
error: logic.unlockError,
|
|
onOpenUnlock: logic.wallet == null
|
|
? null
|
|
: () => _openUnlockSheet(context, index, episode),
|
|
),
|
|
if (widget.homeMode && !_chromeVisible && !_boosting)
|
|
Positioned(
|
|
right: 12,
|
|
bottom: 56,
|
|
child: ExitClearScreenButton(
|
|
key: const Key('home-feed-exit-clear-screen'),
|
|
tooltip: l10n.exitClearScreen,
|
|
onPressed: () => setState(() => _chromeVisible = true),
|
|
),
|
|
),
|
|
if (widget.homeMode &&
|
|
!logic.chromeHidden &&
|
|
selected &&
|
|
!logic.session.userWantsPlay)
|
|
Center(
|
|
child: PlayerCenterToggle(
|
|
key: const Key('home-feed-center-play'),
|
|
playing: false,
|
|
onTap: () =>
|
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|