mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 15:48:14 +02:00
feat(player): channel list sidebar accessible from the live OSD
Add a visible playlist button in both players (native + lite) that opens the TiviMate-style channel sidebar (channels of the current category). The sidebar existed in the native player but was only reachable via an edge swipe or the M/Menu key; the lite player had none. - New OSD button next to channel zapping (live only) - Lite player: full sidebar port (swipe from right edge, M/Menu key) - D-pad support: focus lands on the current channel when the list opens, arrows navigate, OK switches channel, Back/Menu closes - Auto-hide timer (6s) restarted on D-pad focus and touch scroll - Bump version to 1.8.0+15 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EG1frWE7suYF7hp4q2sDiR
This commit is contained in:
1 parent
c096532fec
commit
3c37c6a6e6
3 files changed
+386
-19
No files matched your search
@@ -73,6 +73,11 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
final FocusNode _sliderFocusNode = FocusNode();
|
||||
final FocusNode _backFocusNode = FocusNode();
|
||||
final FocusNode _audioFocusNode = FocusNode();
|
||||
final FocusNode _channelListButtonFocusNode = FocusNode();
|
||||
|
||||
/// Focus D-pad de la chaîne active dans la sidebar : permet d'atterrir
|
||||
/// directement dessus à l'ouverture de la liste.
|
||||
final FocusNode _channelListItemFocusNode = FocusNode();
|
||||
|
||||
bool _isLoading = true;
|
||||
bool _isFirstLoad = true; // true = black overlay; false = silent spinner
|
||||
@@ -605,6 +610,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
_sliderFocusNode.dispose();
|
||||
_backFocusNode.dispose();
|
||||
_audioFocusNode.dispose();
|
||||
_channelListButtonFocusNode.dispose();
|
||||
_channelListItemFocusNode.dispose();
|
||||
|
||||
// Restore normal orientation
|
||||
SystemChrome.setPreferredOrientations([
|
||||
@@ -1054,15 +1061,36 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
setState(() => _showChannelList = !_showChannelList);
|
||||
if (_showChannelList) {
|
||||
_scrollToCurrentChannel();
|
||||
_channelListTimer?.cancel();
|
||||
_channelListTimer = Timer(const Duration(seconds: 6), () {
|
||||
if (mounted) setState(() => _showChannelList = false);
|
||||
_restartChannelListTimer();
|
||||
// Focus direct sur la chaîne en cours : la télécommande navigue dans la
|
||||
// liste sans étape intermédiaire. Second essai après l'animation de
|
||||
// scroll (300 ms), l'item actif pouvant ne pas encore être construit.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _showChannelList) {
|
||||
_channelListItemFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 400), () {
|
||||
if (mounted &&
|
||||
_showChannelList &&
|
||||
!_channelListItemFocusNode.hasFocus) {
|
||||
_channelListItemFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_channelListTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re)arme l'auto-fermeture de la sidebar. Relancé à chaque interaction
|
||||
/// (focus D-pad, scroll) pour ne pas fermer la liste sous les doigts.
|
||||
void _restartChannelListTimer() {
|
||||
_channelListTimer?.cancel();
|
||||
_channelListTimer = Timer(const Duration(seconds: 6), () {
|
||||
if (mounted) setState(() => _showChannelList = false);
|
||||
});
|
||||
}
|
||||
|
||||
/// Scroll channel list to keep current channel visible
|
||||
void _scrollToCurrentChannel() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -1111,6 +1139,21 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
try {
|
||||
final key = event.logicalKey;
|
||||
|
||||
// Sidebar ouverte : Back/Menu la ferment, le reste part au système de
|
||||
// Focus (navigation + OK sur un item). On relance juste l'auto-fermeture.
|
||||
if (_showChannelList) {
|
||||
if (key == LogicalKeyboardKey.escape ||
|
||||
key == LogicalKeyboardKey.goBack ||
|
||||
key == LogicalKeyboardKey.contextMenu ||
|
||||
key == LogicalKeyboardKey.keyM) {
|
||||
_channelListTimer?.cancel();
|
||||
setState(() => _showChannelList = false);
|
||||
return true;
|
||||
}
|
||||
_restartChannelListTimer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Explicit Play
|
||||
if (key == LogicalKeyboardKey.mediaPlay) {
|
||||
if (!_isPlaying) _player.play();
|
||||
@@ -1224,16 +1267,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
}
|
||||
}
|
||||
|
||||
// OK / Select when channel list is open → switch to focused channel
|
||||
if (_showChannelList &&
|
||||
(key == LogicalKeyboardKey.enter ||
|
||||
key == LogicalKeyboardKey.select ||
|
||||
key == LogicalKeyboardKey.numpadEnter)) {
|
||||
// Channel list handles its own tap, this closes it
|
||||
setState(() => _showChannelList = false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Menu key or 'M' → toggle channel list sidebar
|
||||
if (key == LogicalKeyboardKey.contextMenu ||
|
||||
key == LogicalKeyboardKey.keyM) {
|
||||
@@ -1597,6 +1630,19 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Channel list sidebar (liste des chaînes de la catégorie)
|
||||
if (widget.channels != null && widget.channels!.isNotEmpty)
|
||||
TVFocusable(
|
||||
focusNode: _channelListButtonFocusNode,
|
||||
onPressed: _toggleChannelList,
|
||||
onFocus: () => _noteFocus(_channelListButtonFocusNode),
|
||||
child: const Icon(
|
||||
Icons.playlist_play_rounded,
|
||||
color: AppColors.onSurface,
|
||||
size: 36,
|
||||
),
|
||||
),
|
||||
|
||||
// Disable Deinterlace Button
|
||||
if (widget.forceDeinterlace) ...[
|
||||
const SizedBox(width: 16),
|
||||
@@ -1945,15 +1991,28 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
),
|
||||
// Channel list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) {
|
||||
_restartChannelListTimer();
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: _channelListScrollController,
|
||||
itemCount: channels.length,
|
||||
itemExtent: 64.0,
|
||||
itemBuilder: (context, index) {
|
||||
final ch = channels[index];
|
||||
final isActive = index == _currentIndex;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
return TVFocusable(
|
||||
// Le focusNode externe n'est posé que sur la chaîne
|
||||
// active ; la clé force un remontage quand isActive
|
||||
// change (TVFocusable ne gère pas didUpdateWidget).
|
||||
key: ValueKey('sidebar-ch-$index-$isActive'),
|
||||
focusNode: isActive ? _channelListItemFocusNode : null,
|
||||
scale: 1.0,
|
||||
borderRadius: BorderRadius.zero,
|
||||
onFocus: _restartChannelListTimer,
|
||||
onPressed: () {
|
||||
_channelListTimer?.cancel();
|
||||
_switchChannel(index);
|
||||
},
|
||||
@@ -2041,6 +2100,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user