Repository navigation
Add Android TV / D-pad navigation support - #2377
daviddaco1 wants to merge 4 commits into
Conversation
Adapts the existing UI for Android TV/Google TV remote control navigation without introducing new frameworks (still Voyager/Injekt/Compose Foundation). - isTvUi() device-mode detection (mirrors the existing isTabletUi() pattern) - Modifier.tvFocusable() draws a D-pad focus indicator driven by the interactionSource each clickable/selectable already owns, instead of stacking a second focusable() node (which Compose skips, hiding the indicator or making elements unreachable) - NavigationRail always visible on TV; library cards, chapter/episode rows, settings widgets, and category rows are D-pad focusable and navigable - Swipe actions on chapters/episodes are disabled on TV (long-press selection covers the same actions); category reordering gets explicit up/down buttons as a D-pad-reachable alternative to drag handles - MainActivity nudges initial focus onto each newly pushed Voyager screen, since Compose doesn't do this automatically - PlayerActivity handles MEDIA_PLAY_PAUSE/DPAD_CENTER/ENTER and no longer swallows DPAD_UP/DOWN once controls are shown, so Compose focus can navigate the on-screen controls; play/pause auto-focuses when controls appear - AdaptiveSheet forces the centered (non-swipe) layout on TV and focuses its first element on open - Manifest: optional touchscreen/leanback uses-feature, LEANBACK_LAUNCHER category, placeholder TV banner Known issue: the Settings main menu list (Appearance/Library/Reader/...) doesn't yet get initial D-pad focus on its first item; needs the same tvFocusable treatment as BasePreferenceWidget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add TvUiMode preference (Automatic/Always/Never), mirroring TabletUiMode, with a new onboarding step to choose Mobile vs Android TV - Combine the manual override into isTvUi(), keeping swipe gestures on chapters/episodes tied to real touch hardware regardless of the override - Add tvFocusable focus indicators across Extensions, Updates, Tracker, Settings, and onboarding screens that were reachable by D-pad but had no visible focus - Fix missing focusGroup() on several settings LazyColumns that blocked D-pad navigation entirely (Settings menus, Player Settings, dialogs) - Fix duplicate combinedClickable in extension list items
…ter to be nullable in BrowseAnimeSourceToolbar
|
Hi! I was experimenting with Android TV support on my own before I noticed this PR. Most of the implementation on my branch was done with the help of OpenAI Codex. I tested several workflows manually on both TV and phone emulators, although I’m not confident enough with the codebase to properly review all of the implementation myself. My branch is here. It includes some fixes for player submenu navigation, remote seeking, manga reader controls, and downloads that might be useful to compare against your work. If it would help, I’d be happy to test your branch and report any reproducible issues I find. I’d rather contribute to this effort than maintain a separate implementation of the same thing. |
|
@myasn1k, thank you very much for your interest, the branch was born to try to use it in an Android TV Box, I was surprised that I was not prepared for button navigation, my branch implements a TV mode that allows button navigation to be functional and I have even arrived to play videos, I am still correcting things but I would be very grateful for any help. |
|
Thank you for your work on Aniyomi. This PR won't be merged for now because I can't maintain it right now. If you want, you can submit this PR to Quickdesh/Animiru, which is still actively maintained. |
|
@jmir1 thanks for the reply! |
I'll gladly send a pr. |
I will close this pr, I will bring the changes of that fork and merge them with my changes if everything goes well, I do the pr and I label you. |
|
This PR will be closed to contribute to the fork https://github.com/Quickdesh/Animiru |
Summary
isTvUi()device-mode check (mirrors the existingisTabletUi()pattern) instead of a dedicated flavor/module, since the difference between mobile and TV is layout + focus, not business logic.Modifier.tvFocusable()draws the D-pad focus indicator by observing theinteractionSourceeach clickable/selectable/combinedClickable already owns, rather than adding a secondfocusable()node (Compose skips a focusable ancestor/descendant pair, which was hiding the indicator or making some elements unreachable — this was the core fix threaded through the diff).TvUiModepreference (Automatic/Always/Never), mirroring the existingTabletUiModepattern exactly (sameUiPreferencesshape, same Settings > Appearance row, same "requires restart" toast). Lets users override the automatic hardware detection for TV boxes/hybrid devices where it's unreliable.isTvUi()now combines the preference with hardware detection;AUTOMATICpreserves today's behavior for all existing installs.DeviceModeStep, between Theme and Storage) asking Mobile/Touch vs Android TV, persistingNever/Alwaysrespectively (Automaticstays the unasked default).LazyColumns across Settings (main menu, Player Settings menu, search, extension stores/repos, script editor, clear-database, tri-state dialog) that were missingModifier.focusGroup()— without it Compose has no coherent D-pad traversal order between items, so these lists were effectively unnavigable by remote.combinedClickabletwice (once via an explicit modifier, once insideBaseBrowseItem), leaving two overlapping focus/click targets on the same row.AdaptiveSheet), onboarding, and the video player's on-screen controls.MainActivitynudges initial focus onto each newly pushed Voyager screen (Compose doesn't do this on its own), andPlayerActivitynow handlesMEDIA_PLAY_PAUSE/DPAD_CENTER/ENTERand stops swallowingDPAD_UP/DOWNonce controls are visible, so Compose can move focus between the on-screen buttons.touchscreen/leanbackuses-feature,LEANBACK_LAUNCHERcategory, placeholder TV banner drawable (not a final design asset).Resolved from earlier review
The Settings main menu (and several other menus/dialogs) not receiving D-pad navigation was actually a missing
focusGroup()on the underlyingLazyColumns, not just an initial-focus issue — fixed at the sharedScrollbarLazyColumn/FastScrollLazyColumn/LazyColumnWithActioncomponents plus the handful of screens using a plainLazyColumndirectly.Test plan
./gradlew spotlessCheck./gradlew assembleDebug./gradlew testReleaseUnitTest./gradlew assembleRelease