feat: 逻辑与页面布局分离
This commit is contained in:
1 parent
b1f6ce4bf9
commit
1e61ae7a0b
11 files changed
+1233
-907
No files matched your search
@@ -6,24 +6,16 @@ import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/models/player_port.dart';
|
||||
import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/services/engagement_controller.dart';
|
||||
import '../../core/data/services/platform_share_service.dart';
|
||||
import '../../core/data/services/playback_queue.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../discover/discover_view.dart';
|
||||
import '../drama_detail/drama_detail_view.dart';
|
||||
import '../home/home_feed_sections.dart';
|
||||
import '../home/widgets/category_swipe_region.dart';
|
||||
import '../home/widgets/home_feed_header.dart';
|
||||
import '../mine/mine_view.dart';
|
||||
import 'player_binding.dart';
|
||||
import 'player_logic.dart';
|
||||
import 'widgets/drama_comments_sheet.dart';
|
||||
import 'widgets/seek_progress_bar.dart';
|
||||
|
||||
class PlayerPage extends StatefulWidget {
|
||||
@@ -67,582 +59,46 @@ class PlayerPage extends StatefulWidget {
|
||||
class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
late final PlayerLogic logic;
|
||||
|
||||
void _notifyImmersive() => widget.onImmersiveChanged?.call(logic.immersive);
|
||||
|
||||
Future<void> _setFullscreen(bool value) async {
|
||||
if (logic.fullscreen == value || !mounted) return;
|
||||
setState(() => logic.fullscreen = value);
|
||||
_notifyImmersive();
|
||||
try {
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
value
|
||||
? [
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]
|
||||
: [DeviceOrientation.portraitUp],
|
||||
);
|
||||
await SystemChrome.setEnabledSystemUIMode(
|
||||
value ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge,
|
||||
);
|
||||
} catch (_) {
|
||||
if (value && mounted) {
|
||||
setState(() => logic.fullscreen = false);
|
||||
_notifyImmersive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _enterPip() async {
|
||||
if (logic.pip || logic.pipPreparing) return;
|
||||
if (logic.session.active is! VideoPlayerPort) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.pipUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (logic.fullscreen) await _setFullscreen(false);
|
||||
setState(() => logic.pipPreparing = true);
|
||||
_notifyImmersive();
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
bool entered = false;
|
||||
try {
|
||||
entered = await logic.pictureInPicture.enter();
|
||||
} catch (_) {
|
||||
// Android reports unavailable for devices without PiP support.
|
||||
}
|
||||
if (!mounted) return;
|
||||
if (entered) {
|
||||
setState(() {
|
||||
logic.pip = true;
|
||||
logic.pipPreparing = false;
|
||||
});
|
||||
_notifyImmersive();
|
||||
logic.session.setAppResumed(true);
|
||||
} else {
|
||||
setState(() => logic.pipPreparing = false);
|
||||
_notifyImmersive();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.pipUnavailable)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onPipChanged(bool enabled) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
logic.pip = enabled;
|
||||
logic.pipPreparing = false;
|
||||
});
|
||||
_notifyImmersive();
|
||||
logic.session.setAppResumed(
|
||||
enabled ||
|
||||
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _preparePlayer(Episode episode, PlayerPort player) async {
|
||||
if (player is! VideoPlayerPort) return;
|
||||
final drama = logic.queue.entries
|
||||
.firstWhere((e) => e.episode.id == episode.id)
|
||||
.drama;
|
||||
try {
|
||||
await logic.pendingWrite;
|
||||
final saved = await logic.history.positionFor(
|
||||
demoProfileId,
|
||||
drama.id,
|
||||
episode.id,
|
||||
);
|
||||
if (saved != null) {
|
||||
await player.controller.seekTo(
|
||||
clampResumePosition(saved, player.controller.value.duration),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// A local preference failure does not prevent playback.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _activatePlayer(Episode episode, PlayerPort player) async {
|
||||
if (player is! VideoPlayerPort) return;
|
||||
final index = logic.session.currentIndex;
|
||||
final drama = logic.queue.entryAt(index).drama;
|
||||
await player.controller.setVolume(logic.muted ? 0 : 1);
|
||||
if (logic.session.active != player) return;
|
||||
logic.trackedPlayer = player;
|
||||
logic.trackedEpisode = episode;
|
||||
logic.trackedDrama = drama;
|
||||
logic.lastProgressWrite = DateTime.now();
|
||||
var completionHandled = false;
|
||||
var completionProgressSaved = false;
|
||||
player.controller.addListener(() {
|
||||
if (logic.session.active != player || !player.controller.value.isInitialized) {
|
||||
return;
|
||||
}
|
||||
if (player.controller.value.isCompleted && !completionProgressSaved) {
|
||||
completionProgressSaved = true;
|
||||
unawaited(
|
||||
_queueProgress(drama, episode, player.controller.value.duration),
|
||||
);
|
||||
}
|
||||
if (!completionHandled &&
|
||||
player.controller.value.isCompleted &&
|
||||
logic.session.canAutoAdvance &&
|
||||
logic.session.currentIndex == index) {
|
||||
completionHandled = true;
|
||||
unawaited(_advanceAfterEnd(player, index));
|
||||
}
|
||||
final now = DateTime.now();
|
||||
if (now.difference(logic.lastProgressWrite) < const Duration(seconds: 5)) {
|
||||
return;
|
||||
}
|
||||
logic.lastProgressWrite = now;
|
||||
unawaited(
|
||||
_queueProgress(drama, episode, player.controller.value.position),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _advanceAfterEnd(VideoPlayerPort player, int index) async {
|
||||
if (!mounted || logic.session.active != player || !logic.session.canAutoAdvance) {
|
||||
return;
|
||||
}
|
||||
final next = logic.visibleIndices.indexOf(index) + 1;
|
||||
if (next <= 0 || next >= logic.visibleIndices.length) {
|
||||
logic.stoppedAtFeedEnd = true;
|
||||
logic.session.setUserWantsPlay(false);
|
||||
return;
|
||||
}
|
||||
final pages = logic.pages;
|
||||
try {
|
||||
if (!pages.hasClients) return;
|
||||
await pages.animateToPage(
|
||||
next,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
} catch (_) {
|
||||
// A category change can detach the old page controller mid-animation.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _queueProgress(Drama drama, Episode episode, Duration position) {
|
||||
logic.pendingWrite = logic.pendingWrite
|
||||
.then(
|
||||
(_) => logic.history.save(demoProfileId, drama.id, episode.id, position),
|
||||
)
|
||||
.catchError((Object _) {
|
||||
// Playback continues if local history cannot be stored.
|
||||
});
|
||||
return logic.pendingWrite;
|
||||
}
|
||||
|
||||
Future<void> _saveCurrentProgress() {
|
||||
final player = logic.trackedPlayer;
|
||||
final episode = logic.trackedEpisode;
|
||||
final drama = logic.trackedDrama;
|
||||
if (player == null ||
|
||||
episode == null ||
|
||||
drama == null ||
|
||||
!player.controller.value.isInitialized) {
|
||||
return logic.pendingWrite;
|
||||
}
|
||||
return _queueProgress(drama, episode, player.controller.value.position);
|
||||
}
|
||||
|
||||
Future<void> _selectEpisode(int index) async {
|
||||
final request = ++logic.pageRequest;
|
||||
_cancelScrub();
|
||||
// Capture the old position synchronously, persist without blocking a swipe.
|
||||
unawaited(_saveCurrentProgress());
|
||||
if (request != logic.pageRequest || !mounted) return;
|
||||
await logic.session.select(index, preloadIndex: _nextVisibleIndex(index));
|
||||
}
|
||||
|
||||
int? _nextVisibleIndex(int index) {
|
||||
final position = logic.visibleIndices.indexOf(index);
|
||||
return position >= 0 && position + 1 < logic.visibleIndices.length
|
||||
? logic.visibleIndices[position + 1]
|
||||
: null;
|
||||
}
|
||||
|
||||
void _shiftCategory(int offset) {
|
||||
if (!widget.homeMode) return;
|
||||
final next = logic.category.index + offset;
|
||||
if (next < 0) {
|
||||
widget.onMenu?.call();
|
||||
return;
|
||||
}
|
||||
if (next >= HomeFeedCategory.values.length) return;
|
||||
unawaited(_selectCategory(HomeFeedCategory.values[next]));
|
||||
}
|
||||
|
||||
Future<void> _selectCategory(HomeFeedCategory category) async {
|
||||
if (!widget.homeMode || category == logic.category) return;
|
||||
final request = ++logic.categoryRequest;
|
||||
++logic.pageRequest;
|
||||
logic.session.setRouteVisible(false);
|
||||
_cancelScrub();
|
||||
await _saveCurrentProgress();
|
||||
if (!mounted || request != logic.categoryRequest) return;
|
||||
|
||||
final oldPages = logic.pages;
|
||||
final indices = logic.homeSections.indicesFor(category);
|
||||
setState(() {
|
||||
logic.category = category;
|
||||
logic.visibleIndices = indices;
|
||||
logic.pages = PageController();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => oldPages.dispose());
|
||||
|
||||
if (indices.isEmpty) return;
|
||||
widget.onDramaChanged?.call(logic.queue.entryAt(indices.first).drama);
|
||||
await logic.session.select(
|
||||
indices.first,
|
||||
preloadIndex: _nextVisibleIndex(indices.first),
|
||||
);
|
||||
if (mounted && request == logic.categoryRequest) {
|
||||
if (logic.stoppedAtFeedEnd) {
|
||||
logic.stoppedAtFeedEnd = false;
|
||||
logic.session.setUserWantsPlay(true);
|
||||
}
|
||||
logic.session.setRouteVisible(widget.active);
|
||||
}
|
||||
}
|
||||
|
||||
void _beginScrub(VideoPlayerPort player) {
|
||||
if (logic.session.active != player) return;
|
||||
logic.scrubbingPlayer = player;
|
||||
logic.session.setOverlayOpen(true);
|
||||
}
|
||||
|
||||
void _cancelScrub() {
|
||||
if (logic.scrubbingPlayer == null) return;
|
||||
logic.scrubbingPlayer = null;
|
||||
logic.session.setOverlayOpen(false);
|
||||
}
|
||||
|
||||
Future<void> _seekTo(VideoPlayerPort player, Duration position) async {
|
||||
if (logic.scrubbingPlayer != player || logic.session.active != player) return;
|
||||
try {
|
||||
await logic.session.playbackSettled;
|
||||
if (logic.scrubbingPlayer != player || logic.session.active != player) return;
|
||||
await player.controller.seekTo(position);
|
||||
if (logic.session.active == player) await _saveCurrentProgress();
|
||||
} catch (_) {
|
||||
if (mounted && logic.session.active == player) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.seekFailed)));
|
||||
}
|
||||
} finally {
|
||||
if (logic.scrubbingPlayer == player) _cancelScrub();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _leave() async {
|
||||
await _saveCurrentProgress();
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
logic = PlayerBinding().create();
|
||||
logic.attach(widget, isMounted: () => mounted);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
logic.pictureInPicture.setStateListener(_onPipChanged);
|
||||
logic.queue = widget.feedDramas == null
|
||||
? PlaybackQueue.series(widget.drama)
|
||||
: PlaybackQueue.home(widget.feedDramas!);
|
||||
logic.homeSections = HomeFeedSections([
|
||||
for (final entry in logic.queue.entries) entry.drama,
|
||||
]);
|
||||
logic.visibleIndices = widget.homeMode
|
||||
? logic.homeSections.indicesFor(logic.category)
|
||||
: List<int>.generate(logic.queue.length, (index) => index);
|
||||
logic.pages = PageController(initialPage: widget.initialIndex);
|
||||
logic.session = PlaybackSession(
|
||||
episodes: [for (final entry in logic.queue.entries) entry.episode],
|
||||
logic.start(
|
||||
factory: context.read<PlayerFactory>(),
|
||||
canPlay: (episode) => episode.isFree,
|
||||
prepare: _preparePlayer,
|
||||
activate: _activatePlayer,
|
||||
coordinator: context.read<PlaybackCoordinator>(),
|
||||
);
|
||||
if (!widget.active || logic.visibleIndices.isEmpty) {
|
||||
logic.session.setRouteVisible(false);
|
||||
}
|
||||
unawaited(_selectEpisode(widget.initialIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PlayerPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.active != widget.active) {
|
||||
logic.session.setRouteVisible(widget.active && logic.visibleIndices.isNotEmpty);
|
||||
if (!widget.active) {
|
||||
_cancelScrub();
|
||||
unawaited(_saveCurrentProgress());
|
||||
}
|
||||
}
|
||||
if (oldWidget.resumeRequest != widget.resumeRequest) {
|
||||
logic.session.setUserWantsPlay(true);
|
||||
}
|
||||
if (oldWidget.backRequest != widget.backRequest) {
|
||||
final request = widget.backRequest;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && widget.backRequest == request) {
|
||||
if (logic.fullscreen) {
|
||||
unawaited(_setFullscreen(false));
|
||||
} else {
|
||||
_shiftCategory(-1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
logic.attach(widget, isMounted: () => mounted);
|
||||
logic.onPageUpdated(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) unawaited(_saveCurrentProgress());
|
||||
logic.session.setAppResumed(
|
||||
state == AppLifecycleState.resumed || logic.pip || logic.pipPreparing,
|
||||
);
|
||||
if (state != AppLifecycleState.resumed) _cancelScrub();
|
||||
logic.onLifecycle(state);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
logic.pictureInPicture.setStateListener(null);
|
||||
if (logic.fullscreen) {
|
||||
unawaited(
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
|
||||
);
|
||||
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge));
|
||||
}
|
||||
unawaited(_saveCurrentProgress());
|
||||
logic.session.setRouteVisible(false);
|
||||
_cancelScrub();
|
||||
unawaited(logic.session.close());
|
||||
logic.pages.dispose();
|
||||
logic.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openDetail() async {
|
||||
logic.session.setRouteVisible(false);
|
||||
await logic.session.playbackSettled;
|
||||
await _saveCurrentProgress();
|
||||
if (!mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) =>
|
||||
DramaDetailPage(drama: logic.queue.entryAt(logic.session.currentIndex).drama),
|
||||
),
|
||||
);
|
||||
if (mounted) logic.session.setRouteVisible(widget.active);
|
||||
}
|
||||
|
||||
Future<void> _showEpisodes() async {
|
||||
if (widget.feedDramas != null) {
|
||||
await _openDetail();
|
||||
return;
|
||||
}
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
logic.session.setOverlayOpen(true);
|
||||
await logic.session.playbackSettled;
|
||||
if (!mounted) {
|
||||
logic.session.setOverlayOpen(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: const Color(0xFF202024),
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.sizeOf(context).height * 0.6,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
|
||||
child: Text(
|
||||
l10n.episodes,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: logic.queue.length,
|
||||
itemBuilder: (context, index) {
|
||||
final episode = logic.queue.entryAt(index).episode;
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(0xFF3A3030),
|
||||
child: Text('${episode.number}'),
|
||||
),
|
||||
title: Text(l10n.episodeLabel(episode.number)),
|
||||
subtitle: Text(l10n.freeTestMedia),
|
||||
trailing: const Icon(Icons.play_arrow_rounded),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
unawaited(
|
||||
logic.pages.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 260),
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
logic.session.setOverlayOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showMy({int initialSection = 0}) async {
|
||||
if (widget.embedded) {
|
||||
widget.onMy?.call();
|
||||
return;
|
||||
}
|
||||
logic.session.setRouteVisible(false);
|
||||
await logic.session.playbackSettled;
|
||||
await _saveCurrentProgress();
|
||||
if (!mounted) return;
|
||||
final port = logic.session.active;
|
||||
final drama = logic.queue.entryAt(logic.session.currentIndex).drama;
|
||||
final duration = port is VideoPlayerPort
|
||||
? port.controller.value.duration
|
||||
: Duration.zero;
|
||||
final action = await Navigator.of(context).push<MyPageAction>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MyPage(
|
||||
drama: drama,
|
||||
duration: duration,
|
||||
initialSection: initialSection,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (action == MyPageAction.discover) {
|
||||
await _showDiscover();
|
||||
} else {
|
||||
logic.session.setRouteVisible(true);
|
||||
}
|
||||
if (action == MyPageAction.resume) logic.session.setUserWantsPlay(true);
|
||||
}
|
||||
|
||||
Future<void> _showSearch() async {
|
||||
if (widget.embedded) {
|
||||
widget.onSearch?.call();
|
||||
return;
|
||||
}
|
||||
await _showDiscover(focusSearch: true);
|
||||
}
|
||||
|
||||
Future<void> _showDiscover({bool focusSearch = false}) async {
|
||||
if (widget.embedded) {
|
||||
widget.onDiscover?.call();
|
||||
return;
|
||||
}
|
||||
logic.session.setRouteVisible(false);
|
||||
await logic.session.playbackSettled;
|
||||
await _saveCurrentProgress();
|
||||
if (!mounted) return;
|
||||
final port = logic.session.active;
|
||||
final duration = port is VideoPlayerPort
|
||||
? port.controller.value.duration
|
||||
: Duration.zero;
|
||||
final action = await Navigator.of(context).push<DiscoverPageAction>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => DiscoverPage(
|
||||
drama: widget.drama,
|
||||
duration: duration,
|
||||
focusSearch: focusSearch,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (action == DiscoverPageAction.detail) {
|
||||
await _openDetail();
|
||||
} else if (action == DiscoverPageAction.my ||
|
||||
action == DiscoverPageAction.watchlist) {
|
||||
await _showMy(
|
||||
initialSection: action == DiscoverPageAction.watchlist ? 1 : 0,
|
||||
);
|
||||
} else {
|
||||
logic.session.setRouteVisible(true);
|
||||
if (action == DiscoverPageAction.resume) {
|
||||
logic.session.setUserWantsPlay(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleMute() async {
|
||||
final port = logic.session.active;
|
||||
if (port is! VideoPlayerPort) return;
|
||||
final next = !logic.muted;
|
||||
await port.controller.setVolume(next ? 0 : 1);
|
||||
if (mounted) setState(() => logic.muted = next);
|
||||
}
|
||||
|
||||
Future<void> _showComments(Drama drama) async {
|
||||
logic.session.setOverlayOpen(true);
|
||||
await logic.session.playbackSettled;
|
||||
if (!mounted) {
|
||||
logic.session.setOverlayOpen(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: const Color(0xFF232328),
|
||||
showDragHandle: true,
|
||||
builder: (_) => DramaCommentsSheet(drama: drama),
|
||||
);
|
||||
} finally {
|
||||
logic.session.setOverlayOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _shareDrama(Drama drama) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
logic.session.setOverlayOpen(true);
|
||||
await logic.session.playbackSettled;
|
||||
try {
|
||||
await const PlatformShareService().shareDrama(
|
||||
title: localizedDramaTitle(l10n, drama),
|
||||
synopsis: localizedDramaSynopsis(l10n, drama),
|
||||
footer: l10n.shareFooter,
|
||||
);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.shareFailed)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
logic.session.setOverlayOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: logic,
|
||||
builder: (context, _) => _buildPlayer(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPlayer(BuildContext context) {
|
||||
final engagement = context.watch<EngagementController>();
|
||||
final category = logic.category;
|
||||
final indices = logic.visibleIndices;
|
||||
@@ -661,7 +117,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
if (category != logic.category) return;
|
||||
final index = indices[position];
|
||||
widget.onDramaChanged?.call(logic.queue.entryAt(index).drama);
|
||||
unawaited(_selectEpisode(index));
|
||||
unawaited(logic.selectEpisode(index));
|
||||
},
|
||||
itemBuilder: (context, position) =>
|
||||
_buildEpisode(context, indices[position], engagement),
|
||||
@@ -676,13 +132,13 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
: Scaffold(backgroundColor: Colors.black, body: content),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEmptyCategory(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return CategorySwipeRegion(
|
||||
key: const Key('home-empty-swipe'),
|
||||
onShift: _shiftCategory,
|
||||
onShift: logic.shiftCategory,
|
||||
child: ColoredBox(
|
||||
color: const Color(0xFF111214),
|
||||
child: SafeArea(
|
||||
@@ -692,10 +148,10 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
l10n: l10n,
|
||||
category: logic.category,
|
||||
onCategorySelected: (value) =>
|
||||
unawaited(_selectCategory(value)),
|
||||
onCategoryShift: _shiftCategory,
|
||||
onSearch: _showSearch,
|
||||
onMy: _showMy,
|
||||
unawaited(logic.selectCategory(value)),
|
||||
onCategoryShift: logic.shiftCategory,
|
||||
onSearch: () => logic.showSearch(context),
|
||||
onMy: () => logic.showMy(context),
|
||||
onMenu: widget.onMenu,
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -761,7 +217,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
if (widget.homeMode) {
|
||||
video = CategorySwipeRegion(
|
||||
key: selected ? const Key('home-video-swipe') : null,
|
||||
onShift: _shiftCategory,
|
||||
onShift: logic.shiftCategory,
|
||||
child: video,
|
||||
);
|
||||
}
|
||||
@@ -773,7 +229,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
if (widget.homeMode && !ready && !logic.immersive)
|
||||
CategorySwipeRegion(
|
||||
key: selected ? const Key('home-video-swipe') : null,
|
||||
onShift: _shiftCategory,
|
||||
onShift: logic.shiftCategory,
|
||||
child: const ColoredBox(color: Colors.black),
|
||||
)
|
||||
else
|
||||
@@ -815,12 +271,12 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
_FullscreenControls(
|
||||
l10n: l10n,
|
||||
controller: ready ? videoPort!.controller : null,
|
||||
onClose: () => unawaited(_setFullscreen(false)),
|
||||
onClose: () => unawaited(logic.setFullscreen(false)),
|
||||
onPip: logic.pictureInPicture.isPlatformSupported
|
||||
? () => unawaited(_enterPip())
|
||||
? () => unawaited(logic.enterPip(context))
|
||||
: null,
|
||||
onScrubStart: ready ? () => _beginScrub(videoPort!) : null,
|
||||
onSeek: ready ? (position) => _seekTo(videoPort!, position) : null,
|
||||
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
||||
onSeek: ready ? (position) => logic.seekTo(context, videoPort!, position) : null,
|
||||
)
|
||||
else if (widget.homeMode)
|
||||
_StitchHomeOverlay(
|
||||
@@ -832,25 +288,25 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
saved: engagement.isSaved(drama.id),
|
||||
muted: logic.muted,
|
||||
category: logic.category,
|
||||
onCategorySelected: (value) => unawaited(_selectCategory(value)),
|
||||
onCategoryShift: _shiftCategory,
|
||||
onDetail: _openDetail,
|
||||
onDiscover: _showDiscover,
|
||||
onEpisodes: _showEpisodes,
|
||||
onMy: _showMy,
|
||||
onSearch: _showSearch,
|
||||
onCategorySelected: (value) => unawaited(logic.selectCategory(value)),
|
||||
onCategoryShift: logic.shiftCategory,
|
||||
onDetail: () => logic.openDetail(context),
|
||||
onDiscover: () => logic.showDiscover(context),
|
||||
onEpisodes: () => logic.showEpisodes(context),
|
||||
onMy: () => logic.showMy(context),
|
||||
onSearch: () => logic.showSearch(context),
|
||||
onMenu: widget.onMenu,
|
||||
onMute: _toggleMute,
|
||||
onMute: logic.toggleMute,
|
||||
onLike: () => engagement.toggleLiked(drama.id),
|
||||
onSave: () => engagement.toggleSaved(drama.id),
|
||||
onComments: () => _showComments(drama),
|
||||
onShare: () => _shareDrama(drama),
|
||||
onFullscreen: () => unawaited(_setFullscreen(true)),
|
||||
onComments: () => logic.showComments(context, drama),
|
||||
onShare: () => logic.shareDrama(context, drama),
|
||||
onFullscreen: () => unawaited(logic.setFullscreen(true)),
|
||||
onPip: logic.pictureInPicture.isPlatformSupported
|
||||
? () => unawaited(_enterPip())
|
||||
? () => unawaited(logic.enterPip(context))
|
||||
: null,
|
||||
onScrubStart: ready ? () => _beginScrub(videoPort!) : null,
|
||||
onSeek: ready ? (position) => _seekTo(videoPort!, position) : null,
|
||||
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
||||
onSeek: ready ? (position) => logic.seekTo(context, videoPort!, position) : null,
|
||||
showBottomNav: !widget.embedded,
|
||||
)
|
||||
else
|
||||
@@ -864,7 +320,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
children: [
|
||||
if (!widget.homeMode)
|
||||
IconButton(
|
||||
onPressed: () => unawaited(_leave()),
|
||||
onPressed: () => unawaited(logic.leave(context)),
|
||||
tooltip: l10n.backToSeries,
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
)
|
||||
@@ -886,7 +342,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.language,
|
||||
onPressed: _showMy,
|
||||
onPressed: () => logic.showMy(context),
|
||||
icon: const Icon(Icons.language_rounded),
|
||||
),
|
||||
],
|
||||
@@ -906,7 +362,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
const SizedBox(width: 22),
|
||||
TextButton(
|
||||
onPressed: _openDetail,
|
||||
onPressed: () => logic.openDetail(context),
|
||||
child: Text(
|
||||
l10n.discoverTab,
|
||||
style: const TextStyle(
|
||||
@@ -922,7 +378,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: FilledButton(
|
||||
onPressed: () => unawaited(_selectEpisode(index)),
|
||||
onPressed: () => unawaited(logic.selectEpisode(index)),
|
||||
child: Text('${l10n.playbackError} ${l10n.retry}'),
|
||||
),
|
||||
),
|
||||
@@ -968,7 +424,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
tooltip: l10n.exploreSeries,
|
||||
onPressed: _openDetail,
|
||||
onPressed: () => logic.openDetail(context),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black45,
|
||||
foregroundColor: Colors.white,
|
||||
@@ -978,7 +434,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
const SizedBox(height: 8),
|
||||
IconButton.filledTonal(
|
||||
tooltip: l10n.episodes,
|
||||
onPressed: _showEpisodes,
|
||||
onPressed: () => logic.showEpisodes(context),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black45,
|
||||
foregroundColor: Colors.white,
|
||||
@@ -1011,7 +467,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextButton.icon(
|
||||
onPressed: _showEpisodes,
|
||||
onPressed: () => logic.showEpisodes(context),
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: Text(
|
||||
'${l10n.episodeProgress(episode.number, drama.episodes.length)} · ${l10n.episodes}',
|
||||
@@ -1022,9 +478,9 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
valueListenable: ready ? videoPort!.controller : null,
|
||||
semanticsLabel: l10n.seekVideo,
|
||||
showTimes: false,
|
||||
onScrubStart: ready ? () => _beginScrub(videoPort!) : null,
|
||||
onScrubStart: ready ? () => logic.beginScrub(videoPort!) : null,
|
||||
onSeek: ready
|
||||
? (position) => _seekTo(videoPort!, position)
|
||||
? (position) => logic.seekTo(context, videoPort!, position)
|
||||
: null,
|
||||
),
|
||||
if (widget.homeMode) ...[
|
||||
@@ -1041,12 +497,12 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
_BottomAction(
|
||||
icon: Icons.explore_outlined,
|
||||
label: l10n.discoverTab,
|
||||
onTap: _showDiscover,
|
||||
onTap: () => logic.showDiscover(context),
|
||||
),
|
||||
_BottomAction(
|
||||
icon: Icons.person_outline_rounded,
|
||||
label: l10n.myTab,
|
||||
onTap: _showMy,
|
||||
onTap: () => logic.showMy(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user