Skip to content

Make player controls D-pad friendly#167

Draft
google-labs-jules[bot] wants to merge 79 commits intomainfrom
feat/dpad-friendly-player-controls-277104891523565276
Draft

Make player controls D-pad friendly#167
google-labs-jules[bot] wants to merge 79 commits intomainfrom
feat/dpad-friendly-player-controls-277104891523565276

Conversation

@google-labs-jules
Copy link

This change refactors the video player's control layout to be navigable and operable using a D-pad or keyboard arrow keys, drawing inspiration from Android TV's Leanback library.

The BottomControl widget is refactored into a three-tier vertical layout (primary controls, progress bar, secondary controls) to support vertical D-pad navigation. A new FocusableBtn widget is introduced, which provides clear visual feedback when focused. All existing buttons have been replaced with this new focusable version.

The PlayerFocus widget is enhanced to intercept arrow key events and use Flutter's built-in focus traversal system to navigate between the focusable widgets. The progress bar is also made focusable and handles left/right arrow keys to seek backward and forward.

Fixes #163


PR created automatically by Jules for task 277104891523565276 started by @UcnacDx2

google-labs-jules bot and others added 30 commits November 28, 2025 04:42
- Modified the `setAccount` method to automatically select the first non-anonymous account as the default for all account types.
- Removed the account selection dialog that appeared after a successful login, streamlining the user experience.
Auto-select Default Account After Login
Reorders the `DynamicsTabType` enum to move the 'All' tab to the end of the list. This aligns with user feedback to prioritize specific content categories over the general 'All' view, improving the user experience.

The tab order is now: 投稿 → 番剧 → 专栏 → UP → 全部.

No UI code changes were necessary as the tab bar and tab view are dynamically generated from the enum's values.
Sets the initial index of the TabController on the login page to 2, making the QR code scanning tab the default view when the page is loaded. This improves user experience for those who primarily use QR code login.
Calls `_handleTabChange()` in the `onInit` method to ensure that the QR code is generated and displayed when the login page is first loaded with the QR code tab as the default. This fixes a bug where the QR code would not appear until the user manually switched tabs.
Simplifies the `canPop` logic in the `PopScope` widget to only check for fullscreen mode. This resolves an issue where the Android TV remote could not navigate back from the video detail page.

The previous logic was too restrictive for devices that are always in landscape mode. This change improves the user experience on both Android TV and mobile devices by making the back button behavior more predictable.
This commit addresses a regression introduced in the previous commit. While the back button navigation was fixed, the "OK" button on the Android TV remote lost its play/pause functionality.

The `PlayerFocus` widget was incorrectly mapping the `enter` key to the `onSendDanmaku` function. This change remaps both the `enter` and `select` keys to the play/pause action, restoring the expected behavior for the "OK" button on Android TV remotes.
This commit implements playlist navigation using the up and down arrow keys on the remote control. This provides a more intuitive experience for users on Android TV.

The up and down arrow keys are now used to switch to the previous and next videos in a playlist, respectively. The previous volume control functionality has been removed from these keys to accommodate this change.
Modified the video player controller to prevent the controls from auto-hiding when playback is paused. This improves usability on remote-controlled devices like Android TV where persistent on-screen controls are necessary for navigation.

Key changes:
- When the player state changes to paused, the controls are now explicitly made visible.
- The auto-hide timer logic in `hideTaskControls` now checks if the player is paused before hiding the controls.
- The `controls` setter only starts the auto-hide timer if the video is actively playing.
- Tapping the screen no longer hides the controls when the video is paused.
Wrapped the VideoPopupMenu in VideoCardH and VideoCardV with the ExcludeFocus widget.

This prevents the menu button from gaining focus during D-pad navigation on Android TV, ensuring that focus only cycles between the video cards themselves.
- Duplicated the "Submissions" tab to isolate style changes.
- Applied a new grid-based layout to the new tab.
- Hid the original "Submissions" tab to avoid duplication.
- Fixed a critical bug where the "remove" functionality was broken.
- Refactored the code to improve readability and reduce duplication.
- Duplicated the "Submissions" tab to isolate style changes.
- Applied a new grid-based layout to the new tab.
- Hid the original "Submissions" tab to avoid duplication.
- Fixed a critical bug where the "remove" functionality was broken.
- Refactored the code to improve readability and reduce duplication.
- Recreated the missing `DynamicToRecVideoAdapter` to fix the build.
…ause

fix: Keep player controls visible when paused
- Duplicated the "Submissions" tab to isolate style changes.
- Applied a new grid-based layout to the new tab.
- Hid the original "Submissions" tab to avoid duplication.
- Fixed a critical bug where the "remove" functionality was broken.
- Refactored the code to improve readability and reduce duplication.
- Recreated the missing `DynamicToRecVideoAdapter` to fix the build.
- Resolved `Stat` class conflict using a prefixed import.
- Fixed null-safety errors and undefined method calls.
- Duplicated the "Submissions" tab to isolate style changes.
- Applied a new grid-based layout to the new tab.
- Hid the original "Submissions" tab to avoid duplication.
- Fixed a critical bug where the "remove" functionality was broken.
- Refactored the code to improve readability and reduce duplication.
- Recreated the missing `DynamicToRecVideoAdapter` to fix the build.
- Resolved `Stat` class conflict using a prefixed import.
- Fixed null-safety errors and undefined method calls.
- Added missing properties to data models.
- Duplicated the "Submissions" tab to isolate style changes.
- Applied a new grid-based layout to the new tab.
- Hid the original "Submissions" tab to avoid duplication.
- Fixed a critical bug where the "remove" functionality was broken.
- Refactored the code to improve readability and reduce duplication.
- Recreated the missing `DynamicToRecVideoAdapter` to fix the build.
- Resolved `Stat` class conflict using a prefixed import.
- Fixed null-safety errors and undefined method calls.
- Added missing properties to data models.
- Filtered non-video content from the "Submissions" tab.
…-tab-1

Isolate Dynamics Submission Tab Style
Updates a wide range of default settings based on user feedback to improve the out-of-the-box experience for new users.

Modified settings include:
- Player: Disabled fullscreen lock/screenshot buttons, enabled auto-fullscreen, and hid action items.
- Dynamics: Set UP panel to top, hid unread badges.
- Startup: Changed default page to Dynamics.
- Extensions: Enabled SponsorBlock, AI summary, and other QoL features.
- Filtering: Set minimum recommended video duration to 90s.
Refactored the Dynamics page to dynamically calculate tab indices instead of relying on hardcoded values. This fixes a bug where clicking a user avatar would navigate to the wrong tab after the "All" and "Following" tabs were reordered.
Updates a wide range of default settings based on user feedback to improve the out-of-the-box experience for new users.

Modified settings include:
- Player: Disabled fullscreen lock/screenshot buttons, enabled auto-fullscreen, and hid action items.
- Dynamics: Set UP panel to top, hid unread badges.
- Startup: Changed default page to Dynamics.
- Extensions: Enabled SponsorBlock, AI summary, and other QoL features.
- Filtering: Set minimum recommended video duration to 90s.

This commit also updates the `defaultVal` in the `SettingsModel` definitions to ensure the UI reflects the new default values.
Refactored the Dynamics page to dynamically calculate tab indices instead of relying on hardcoded values. This fixes a bug where clicking a user avatar would navigate to the wrong tab after the "All" and "Following" tabs were reordered.
Changed the default value for the "auto play when entering the detail page" setting to be enabled.
The previous commit only changed the default value in the settings UI, but not in the application logic. This commit updates the default value in `storage_pref.dart` to ensure the auto-play setting works as expected.
UcnacDx2 and others added 7 commits December 1, 2025 02:12
This reverts commit 96de002, reversing
changes made to 0da40c8.
When the video streaming mid (video account) is different from the
account used for history recording (heartbeat account), the playback
progress from the videoUrl API would be for the wrong user.

This fix:
1. Adds lastPlayTime field to PlayInfoData model
2. Adds Api.playInfo to heartbeat account API set
3. When video and heartbeat accounts differ for UGC videos,
   fetches progress from playInfo API (heartbeat account) instead
   of relying on videoUrl API (video account)

Co-authored-by: UcnacDx2 <127503808+UcnacDx2@users.noreply.github.com>
Co-authored-by: UcnacDx2 <127503808+UcnacDx2@users.noreply.github.com>
…gress

Fix video progress tracking when video and heartbeat accounts differ
This change refactors the video player's control layout to be navigable and operable using a D-pad or keyboard arrow keys, drawing inspiration from Android TV's Leanback library.

The `BottomControl` widget is refactored into a three-tier vertical layout (primary controls, progress bar, secondary controls) to support vertical D-pad navigation. A new `FocusableBtn` widget is introduced, which provides clear visual feedback when focused. All existing buttons have been replaced with this new focusable version.

The `PlayerFocus` widget is enhanced to intercept arrow key events and use Flutter's built-in focus traversal system to navigate between the focusable widgets. The progress bar is also made focusable and handles left/right arrow keys to seek backward and forward.
@google-labs-jules
Copy link
Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!


For security, I will only act on instructions from the user who triggered this task.

New to Jules? Learn more at jules.google/docs.

@google-labs-jules google-labs-jules bot mentioned this pull request Dec 12, 2025
@UcnacDx2
Copy link
Owner

The job is failing due to several Dart/Flutter compile-time errors in your code:

  1. Unmatched Parenthesis

    • In lib/plugin/pl_player/widgets/bottom_control.dart, line 129, there's a syntax error: “Can't find ')' to match '('.” Review the Focus widget definition and its parameters to ensure parentheses/bracket pairs are correct.
  2. Undeclared Types and Getters

    • Errors like “‘RawKeyDownEvent’ isn't a type” and “The getter 'LogicalKeyboardKey' isn't defined for the type 'BottomControl'” at lines 132, 133, and 139 indicate missing import for keyboard events and keys.
    • Add the following import at the top of your file:
      import 'package:flutter/services.dart';
    • Make sure you're using the correct event type (RawKeyEvent or RawKeyDownEvent) matching Flutter's API.
  3. Incorrect Getter Usage

    • The error on line 663: “The getter 'controller' isn't defined for the type 'VideoDetailController'.” Check your use of controller inside VideoDetailController. You might need to use a correctly scoped controller instance, or fix the model to match the intended property/method.
  4. Uninitialized Final Field

    • In lib/pages/video/widgets/player_focus.dart, line 35, your final FocusNode? focusNode; declares a field without initialization, but it is required for a final field.
    • Provide the field via the constructor:
      final FocusNode? focusNode;
      const PlayerFocus({
        Key? key,
        // ... other parameters,
        this.focusNode,
        // ...
      }) : super(key: key);

Sample fix for missing imports and event handling:
At the top of lib/plugin/pl_player/widgets/bottom_control.dart, add:

import 'package:flutter/services.dart';

Then, for the onKey handler in the Focus widget (line 131+), ensure it looks like:

onKey: (node, event) {
  if (event is RawKeyDownEvent) {
    if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
      // ...
    }
  }
}

Summary of changes to make:

  • Correct parenthesis/bracket mistakes in your widget builds (around line 129).
  • Add the missing import for LogicalKeyboardKey and key event classes.
  • Ensure all referenced properties and methods exist on their objects (check VideoDetailController).
  • Make sure final fields are initialized or nullable and optional in your widget constructors.

Review and fix these issues and your job should pass. For direct references, see the files at commit c8b5619:

This change refactors the video player's control layout to be navigable and operable using a D-pad or keyboard arrow keys, drawing inspiration from Android TV's Leanback library.

The `BottomControl` widget is refactored into a three-tier vertical layout (primary controls, progress bar, secondary controls) to support vertical D-pad navigation. A new `FocusableBtn` widget is introduced, which provides clear visual feedback when focused. All existing buttons have been replaced with this new focusable version.

The `PlayerFocus` widget is enhanced to intercept arrow key events and use Flutter's built-in focus traversal system to navigate between the focusable widgets. The progress bar is also made focusable and handles left/right arrow keys to seek backward and forward.

This commit also fixes several compilation errors that were introduced in the previous commit.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the video player's control layout to support D-pad and keyboard navigation, inspired by Android TV's Leanback library. The changes introduce a new focusable button widget and restructure the bottom controls into a three-tier vertical layout.

  • Introduces FocusableBtn widget with visual focus indicators
  • Refactors BottomControl into primary/progress/secondary tiers for vertical navigation
  • Enhances PlayerFocus to intercept arrow keys and delegate to Flutter's focus traversal
  • Makes the progress bar focusable with left/right arrow seeking

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
lib/plugin/pl_player/widgets/focusable_btn.dart New widget providing visual focus feedback for buttons
lib/plugin/pl_player/widgets/play_pause_btn.dart Updated to use the new FocusableBtn widget
lib/plugin/pl_player/widgets/bottom_control.dart Refactored from function-based to widget-based layout with focusable controls and progress bar
lib/plugin/pl_player/view.dart Removes the now-refactored buildBottomControl method
lib/pages/video/widgets/player_focus.dart Enhanced with arrow key handling for focus traversal

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -87,27 +120,57 @@ class BottomControl extends StatelessWidget {
}

Widget progressBar() {
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FocusNode created on line 122 is never disposed, which will cause a memory leak. Since BottomControl is a StatelessWidget, the progressBar() method gets called on every rebuild, creating a new FocusNode each time without disposing the previous ones. Consider converting BottomControl to a StatefulWidget and properly managing the lifecycle of the FocusNode, or use a stateful widget for the progress bar portion.

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +154
},
child: DecoratedBox(
decoration: BoxDecoration(
border: focusNode.hasFocus
? Border.all(color: Colors.white, width: 2)
: null,
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DecoratedBox uses focusNode.hasFocus directly to determine the border decoration, but hasFocus is not a reactive property. This means the border won't update when the focus state changes. The widget needs to rebuild when focus changes. Consider wrapping this in a StatefulWidget and adding a listener to the FocusNode to trigger rebuilds, similar to how it's done in FocusableBtn.

Copilot uses AI. Check for mistakes.
onSeek: (e) => onSeek(e, max),
return Focus(
focusNode: focusNode,
onKey: (node, event) {
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the deprecated RawKeyDownEvent class. Flutter deprecated the RawKeyboard API in favor of the new hardware keyboard API. Use KeyDownEvent instead, which is already used in player_focus.dart (line 51). This ensures consistency and future compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +81
final child = SizedBox(
width: widget.width,
height: widget.height,
child: InkWell(
onTap: widget.onTap,
onLongPress: widget.onLongPress,
onSecondaryTap: widget.onSecondaryTap,
child: DecoratedBox(
decoration: BoxDecoration(
border: _isFocused
? Border.all(color: Colors.white, width: 2)
: null,
borderRadius: BorderRadius.circular(4),
),
child: widget.icon,
),
),
);

if (widget.tooltip != null) {
return Tooltip(message: widget.tooltip!, child: child);
}
return child;
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FocusableBtn widget doesn't wrap its content in a Focus widget, so it cannot actually receive keyboard focus or participate in focus traversal. The FocusNode is created and listeners are attached, but the widget tree doesn't connect it to Flutter's focus system. Wrap the InkWell or the entire child in a Focus widget with focusNode: _focusNode to make it actually focusable via keyboard navigation.

Suggested change
final child = SizedBox(
width: widget.width,
height: widget.height,
child: InkWell(
onTap: widget.onTap,
onLongPress: widget.onLongPress,
onSecondaryTap: widget.onSecondaryTap,
child: DecoratedBox(
decoration: BoxDecoration(
border: _isFocused
? Border.all(color: Colors.white, width: 2)
: null,
borderRadius: BorderRadius.circular(4),
),
child: widget.icon,
),
),
);
if (widget.tooltip != null) {
return Tooltip(message: widget.tooltip!, child: child);
}
return child;
final focusableChild = Focus(
focusNode: _focusNode,
child: SizedBox(
width: widget.width,
height: widget.height,
child: InkWell(
onTap: widget.onTap,
onLongPress: widget.onLongPress,
onSecondaryTap: widget.onSecondaryTap,
child: DecoratedBox(
decoration: BoxDecoration(
border: _isFocused
? Border.all(color: Colors.white, width: 2)
: null,
borderRadius: BorderRadius.circular(4),
),
child: widget.icon,
),
),
),
);
if (widget.tooltip != null) {
return Tooltip(message: widget.tooltip!, child: focusableChild);
}
return focusableChild;

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +75
child: InkWell(
onTap: widget.onTap,
onLongPress: widget.onLongPress,
onSecondaryTap: widget.onSecondaryTap,
child: DecoratedBox(
decoration: BoxDecoration(
border: _isFocused
? Border.all(color: Colors.white, width: 2)
: null,
borderRadius: BorderRadius.circular(4),
),
child: widget.icon,
),
),
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InkWell widget doesn't receive keyboard activation events (Enter/Space key presses) because it's not wrapped in a Focus widget. Users won't be able to activate the button via keyboard when it's focused. To support keyboard activation, either wrap the widget in a Focus widget with onKeyEvent handler, or use InkWell's autofocus and focusNode properties in conjunction with a proper Focus wrapper.

Copilot uses AI. Check for mistakes.
return Focus(
focusNode: focusNode,
onKey: (node, event) {
if (event is KeyDownEvent) {
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses RawKeyDownEvent and LogicalKeyboardKey but doesn't import them. These classes are from package:flutter/services.dart. Add the missing import statement at the top of the file.

Suggested change
if (event is KeyDownEvent) {
if (event is RawKeyDownEvent) {

Copilot uses AI. Check for mistakes.
@UcnacDx2
Copy link
Owner

把发弹幕,开启弹幕和画中画和更多菜单的那排按钮也挪下来

This change refactors the video player's control layout to be navigable and operable using a D-pad or keyboard arrow keys, drawing inspiration from Android TV's Leanback library.

The `BottomControl` widget is refactored into a three-tier vertical layout (primary controls, progress bar, secondary controls) to support vertical D-pad navigation. A new `FocusableBtn` widget is introduced, which provides clear visual feedback when focused. All existing buttons have been replaced with this new focusable version.

The `PlayerFocus` widget is enhanced to intercept arrow key events and use Flutter's built-in focus traversal system to navigate between the focusable widgets. The progress bar is also made focusable and handles left/right arrow keys to seek backward and forward.

This commit also fixes several compilation errors that were introduced in the previous commit, and moves the "send danmaku," "toggle danmaku," "picture-in-picture," and "more menu" buttons to the bottom control bar.
@UcnacDx2
Copy link
Owner

The job failed due to two errors in lib/plugin/pl_player/widgets/bottom_control.dart:

  1. The getter 'Platform' isn't defined for the type 'BottomControlState' at line 857.
  2. The getter 'showSettingSheet' isn't defined for the type 'BottomControlState' at line 882.

Solutions:

  1. 'Platform' getter error (line 857):

    • In Dart/Flutter, you should use the Platform class from dart:io, but only if your target supports it (not on web).
    • If this code is for a non-web platform and you want to check the OS, import dart:io at the top:
      import 'dart:io';
    • Then use Platform.isAndroid, Platform.isIOS, etc.
    • If you target web as well, use conditional imports or the universal_platform package.
  2. 'showSettingSheet' getter error (line 882):

    • showSettingSheet appears to be an undefined method or variable.
    • Check if you meant to use a method in your BottomControlState or BottomControl widget. If there is a function for showing settings, define it:
      void showSettingSheet() {
        // Your implementation to open the settings sheet
      }
    • Or, if you intended to use a callback via widget props, ensure you pass and reference it correctly.

Example Fix for line 882:
Locate:

onTap: showSettingSheet,

If showSettingSheet is a method, define it inside BottomControlState. If not, replace it with the intended callback or method (e.g., widget.showSettingSheet if it's in props).

Review the lines in the file:

Correcting these should resolve the build failure.

This commit refactors the video player controls to be more friendly for D-Pad (remote/keyboard) navigation. The implementation is inspired by the Android Leanback layout philosophy.

Key changes:
- Restructured the control layout in `bottom_control.dart` into a three-row vertical layout (primary controls, progress bar, secondary controls).
- Created a reusable `FocusableBtn` widget to make all control buttons focusable.
- Implemented D-Pad navigation logic in `player_focus.dart` to handle arrow key traversal between focusable elements.
- Made the progress bar focusable and implemented seeking with left/right arrow keys.
- Moved the danmaku, PiP, and "more" buttons from `header_control.dart` to `bottom_control.dart` to consolidate all controls in one place.
- Cleaned up `header_control.dart` by removing the now-unused methods and imports.
@UcnacDx2
Copy link
Owner

The job failed because of missing or incorrectly referenced classes, methods, and parameters in the Dart/Flutter code. Specifically, the following issues were reported:

Key Errors and Solutions

  1. Undefined method or class DpadRegionScope

    • Example:
      lib/pages/main/view.dart:253:34: Error: The method 'DpadRegionScope' isn't defined for the type '_MainAppState'.
      
    • Solution:
      • Make sure DpadRegionScope is correctly imported if it exists elsewhere in your codebase.
      • If it's a new widget or utility, ensure it is implemented. Otherwise, update the code to use the correct class/method name.
  2. Undefined name TraversalDirection

    • Example:
      lib/utils/tv/region_manager.dart:9:22: Error: Undefined name 'TraversalDirection'.
      
    • Solution:
      • Import or define the TraversalDirection enum or class. It may belong to a package or should be implemented by you.
  3. Incorrect/missing parameters in constructors or methods

    • Example:
      lib/pages/video/view.dart:708:64: Error: Required named parameter 'onPressed' must be provided.
      lib/common/widgets/tv/tv_button.dart:21:7: Error: No named parameter with the name 'effects'.
      
    • Solution:
      • Ensure all required parameters like onPressed are provided to widgets like IconButton.
      • Remove or correct parameters such as 'effects' or 'color' if the target widget does not support them.

What to do

  • Check for typos or incorrect imports for DpadRegionScope and TraversalDirection.
  • Ensure all custom widgets or enums you’re referencing are present and up-to-date.
  • Review widget constructors for any changes in required parameters, especially after package updates.

Example Fix

If you previously had:

DpadRegionScope(
  child: IconButton(
    icon: Icon(Icons.play_arrow),
  ),
)

But DpadRegionScope does not exist, either:

  • Implement it in your codebase
  • Or, replace it with the correct widget.

If an IconButton now requires an onPressed parameter:

IconButton(
  icon: Icon(Icons.play_arrow),
  onPressed: () {
    // your callback
  },
)

If TraversalDirection is undefined, import it:

import 'package:flutter/widgets.dart'; // if it's from Flutter SDK

Or define it if it’s a custom enum.

Summary

  • Define or import missing classes/methods (DpadRegionScope, TraversalDirection).
  • Supply all required widget parameters.
  • Remove or update non-existent parameters.

Correct these issues in your Dart files where the errors are reported. This will resolve the build failure. If you need specific code for one of the errors, let me know which file and line to focus on.

This commit refactors the video player controls to be more friendly for D-Pad (remote/keyboard) navigation. The implementation is inspired by the Android Leanback layout philosophy.

Key changes:
- Restructured the control layout in `bottom_control.dart` into a three-row vertical layout (primary controls, progress bar, secondary controls).
- Created a reusable `FocusableBtn` widget to make all control buttons focusable.
- Implemented D-Pad navigation logic in `player_focus.dart` to handle arrow key traversal between focusable elements.
- Made the progress bar focusable and implemented seeking with left/right arrow keys.
- Moved the danmaku, PiP, and "more" buttons from `header_control.dart` to `bottom_control.dart` to consolidate all controls in one place.
- Cleaned up `header_control.dart` by removing the now-unused methods and imports.

Fixes build errors by:
- Adding the missing `flutter/widgets.dart` import in `player_focus.dart`.
- Adding the missing `onPressed` parameter to an `IconButton` in `video/view.dart`.
@UcnacDx2
Copy link
Owner

GitHub Copilot Chat Assistant

Cause

  • The Linux build failed during the Dart kernel snapshot step with many "Type 'X' not found", "The method 'foo' isn't defined", and "Too many positional arguments" errors. These are compiler/analysis errors (not runtime).
  • Root cause: header_control.dart (and several other files: bottom_control.dart, pages/video/view.dart) reference many types and helpers that are not available in that compilation unit because the necessary imports/exports are missing or were changed. Examples from the logs:
    • Missing types: VideoDanmaku, LiveDanmaku, DanmakuItem, DanmakuExtra, PlayUrlModel, Box (Hive).
    • Missing symbols from third-party packages: Request, Options, ResponseType, Constants (Dio).
    • Missing widgets/helpers: iconButton, ActionRowLineItem, SetSwitchItem, SettingBoxKey, showSettingSheet, showBottomSheet signatures mismatch.
  • Because these symbols are missing the Dart front-end aborts the kernel snapshot and the build stops.

What to do (step-by-step, actionable)

  1. Grep/locate the missing type and helper definitions in the repo

    • Find where each type is declared (VideoDanmaku, LiveDanmaku, DanmakuItem, DanmakuExtra, PlayUrlModel, Box, iconButton, ActionRowLineItem, SetSwitchItem, SettingBoxKey).
    • Example searches you can run locally:
      • git grep -n "class VideoDanmaku" || git grep -n "typedef VideoDanmaku"
      • git grep -n "class DanmakuItem" || git grep -n "class DanmakuExtra"
      • git grep -n "class PlayUrlModel"
      • git grep -n "iconButton(" || git grep -n "ActionRowLineItem"
      • git grep -n "SettingBoxKey"
    • If your project uses barrel files (exports), also search for exports in plugin/pl_player or other barrel files that should expose these types.
  2. Add the missing imports to the top of each file that currently fails to compile

    • For header_control.dart (example): add imports for the module that actually declares Danmaku types, PlayUrlModel, and Hive Box. Insert them after the other imports at top of lib/pages/video/widgets/header_control.dart.

    • Minimal example patch (replace the placeholder paths with the actual file paths where the types are defined):

      // lib/pages/video/widgets/header_control.dart (add these near line ~1..24)
      import 'package:hive/hive.dart'; // for Box
      import 'package:dio/dio.dart'; // for Request, Options, ResponseType, Constants (if used unprefixed)
      // Import file(s) that define the danmaku types and PlayUrlModel:
      import 'package:PiliPlus/plugin/pl_player/models/danmaku.dart';
      import 'package:PiliPlus/pages/video/models/play_url_model.dart';
      // Import the widget/helpers file(s) that declare iconButton, ActionRowLineItem, SetSwitchItem, SettingBoxKey:
      import 'package:PiliPlus/common/widgets/action_row_item.dart';
      import 'package:PiliPlus/common/widgets/setting_items.dart';
    • Notes:

      • If Request/Options/etc are used without a prefix and you import dio, be aware of symbol conflicts — you can import as a prefix if you prefer: import 'package:dio/dio.dart' as dio; and then use dio.Request.
      • If the project uses a barrel export (e.g., plugin/pl_player/controller.dart or plugin/pl_player/pl_player.dart) that should already expose the types, verify that file still exports those model files. If not, either import the model files directly or restore the export in the barrel.
  3. Restore/adjust exports if a barrel file was changed

    • If many files earlier relied on a barrel export (e.g., plugin/pl_player/controller.dart exporting models) but someone removed or renamed exports, fix the barrel file by re-exporting the model files. Example:

      // lib/plugin/pl_player/controller.dart
      export 'models/danmaku.dart';      // re-export types used by UI files
      export 'models/play_url_model.dart';
      export 'widgets/player_helpers.dart';
    • This keeps many import sites untouched and centralizes exports.

  4. Fix the "Too many positional arguments" / mismatched signatures

    • Errors like "Too many positional arguments: 0 allowed, but 1 found" come from calling methods whose signature changed (e.g., showBottomSheet). Inspect the called site and the API signature in the Flutter SDK or in your helper function.
    • In header_control.dart you call showBottomSheet(contextArg) but at the top you hid showBottomSheet via import 'package:flutter/material.dart' hide showBottomSheet;. Ensure your custom showBottomSheet wrapper has the expected signature. If you intended to call Flutter's showBottomSheet, call it with named arguments or import it normally. If you intended to call your PageUtils.showVideoBottomSheet wrapper, ensure you invoke it consistently (in header_control.dart you call showBottomSheet((context, setState) { ... }) which matches a wrapper signature — but other files may be calling showBottomSheet(context, builder) expecting Flutter's API).
    • Resolution: unify which showBottomSheet API you intend to use. If you want to use the Flutter Material API, remove the hide and call Flutter's API correctly. If you want your wrapper, ensure all callsites call it with the single builder positional arg as in header_control.dart.
  5. Typical concrete fixes for the errors seen in logs

    • Add Hive Box type import: import 'package:hive/hive.dart'; (for Box).
    • Add Dio import when using Request/Options/ResponseType/Options/Constants unprefixed: import 'package:dio/dio.dart';
    • Import the module that defines PlayUrlModel where used (header_control.dart line ~897).
    • Import the module that defines Danmaku types (VideoDanmaku, LiveDanmaku, DanmakuItem, DanmakuExtra) where used (header_control.dart and plugin/pl_player/widgets/bottom_control.dart).
    • Import the widget helper files that define iconButton, ActionRowLineItem, SetSwitchItem, SettingBoxKey, autoWrapReportDialog, etc.
  6. After edits

    • Run a local build/analyze:
      • flutter analyze
      • flutter build linux (or run the same command your CI uses)
    • Fix any remaining unresolved imports (the analyzer will tell you the exact missing symbols and show the import suggestions).
    • Commit a small PR that contains:
      • The added explicit imports (and/or restored barrel exports) — keep changes minimal and focused.
      • If you changed any wrapper signatures (showBottomSheet), update callsites accordingly.

Example minimal patch (illustrative)

  • Edit lib/pages/video/widgets/header_control.dart and add:
@@
 import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
+// >>> Added to fix missing types referenced below:
+import 'package:hive/hive.dart'; // Box
+import 'package:dio/dio.dart'; // Request/Options/ResponseType etc if used unprefixed
+// import the real locations for these types in your repo:
+import 'package:PiliPlus/plugin/pl_player/models/danmaku.dart';
+import 'package:PiliPlus/pages/video/models/play_url_model.dart';
+import 'package:PiliPlus/common/widgets/action_row_item.dart';
+import 'package:PiliPlus/common/widgets/setting_items.dart';

Why this will fix the CI failure

  • The Dart compiler errors are due to unresolved symbols. Adding the proper imports (or restoring barrel exports) resolves those symbols so the front-end can complete the kernel snapshot. Once the missing symbols are resolved, the rest of the compilation should proceed and the Linux build should continue (assuming no further unrelated errors).

If you prefer, an ordered checklist to open a PR

  1. Search for the declarations of all missing types and helpers.
  2. Add direct imports to the files that use them (header_control.dart, bottom_control.dart, pages/video/view.dart).
  3. Or restore re-exports in a barrel file if they were intentionally centralized.
  4. Fix mismatched API call signatures for showBottomSheet and any other function changes.
  5. Run flutter analyze and fix remaining issues.
  6. Push small PR and re-run CI.

References

If you want, I can (a) list the exact import paths for each missing symbol if you point me to where those types are declared in the repo, or (b) produce a ready-made PR patch if you confirm the file paths that contain VideoDanmaku, DanmakuItem, PlayUrlModel and the helper widgets.

This commit refactors the video player controls to be more friendly for D-Pad (remote/keyboard) navigation. The implementation is inspired by the Android Leanback layout philosophy.

Key changes:
- Restructured the control layout in `bottom_control.dart` into a three-row vertical layout (primary controls, progress bar, secondary controls).
- Created a reusable `FocusableBtn` widget to make all control buttons focusable.
- Implemented D-Pad navigation logic in `player_focus.dart` to handle arrow key traversal between focusable elements.
- Made the progress bar focusable and implemented seeking with left/right arrow keys.
- Moved the danmaku, PiP, and "more" buttons from `header_control.dart` to `bottom_control.dart` to consolidate all controls in one place.
- Cleaned up `header_control.dart` by removing the now-unused methods and imports.

Fixes build errors by:
- Adding the missing `flutter/widgets.dart` import in `player_focus.dart`.
- Adding the missing `onPressed` parameter to an `IconButton` in `video/view.dart`.
- Restoring missing imports to `bottom_control.dart`.
- Fixing syntax and type errors in `bottom_control.dart`.
@UcnacDx2 UcnacDx2 closed this Dec 13, 2025
@UcnacDx2 UcnacDx2 deleted the feat/dpad-friendly-player-controls-277104891523565276 branch December 13, 2025 04:56
@UcnacDx2 UcnacDx2 restored the feat/dpad-friendly-player-controls-277104891523565276 branch December 14, 2025 02:37
@UcnacDx2
Copy link
Owner

基于我们刚才确定的“高内聚低耦合”设计方案,以下是详细的实施计划。这个计划将分为四个阶段,从基础架构搭建到组件封装,最后是逻辑集成。

阶段一:基础架构搭建 (Infrastructure)

目标:建立焦点状态的管理中心,不破坏现有的业务逻辑。

创建 TVFocusManager 类

文件路径:lib/plugin/pl_player/logic/tv_focus_manager.dart (新建)

内容:

定义三个核心 FocusNode:seekBarNode, playButtonNode, qualityButtonNode。

实现 dispose() 方法。

可选优化:添加 debugLabel 以便调试。

集成到 PlPlayerController

文件路径:lib/plugin/pl_player/controller.dart

动作:

引入 TVFocusManager。

实例化 final TVFocusManager tvFocusManager = TVFocusManager();。

在 onClose 或 dispose 中调用 tvFocusManager.dispose()。

注意:确保 Controller 的生命周期管理正确,防止内存泄漏。

阶段二:核心组件封装 (Component Encapsulation)

目标:封装 UI 交互细节,让外部调用者无需关心“按下左键是快退还是切焦点”。

创建 InteractiveSeekBar (智能进度条)

文件路径:lib/plugin/pl_player/widgets/interactive_seek_bar.dart (新建)

逻辑实现:

接收 child (原本的 Slider) 和 controller。

使用 Focus 组件包裹。

onKeyEvent 处理:

Left/Right: 调用 controller.seekRelative (快进快退),返回 handled。

OK/Enter: 调用 controller.onDoubleTapCenter (播放暂停),返回 handled。

Up/Down: 返回 ignored (让系统处理焦点导航)。

视觉反馈:监听 hasFocus,在 child 外层包裹带边框的 Container 或 DecoratedBox。

创建/升级 FocusableBtn (通用焦点按钮)

文件路径:lib/plugin/pl_player/widgets/focusable_btn.dart

逻辑实现:

接收 FocusNode 参数。

监听焦点状态变化。

当 hasFocus 为 true 时,显示白色边框(推荐使用 AnimatedContainer 做平滑过渡)。

关键点:确保按钮被点击 (onTap) 或按下确认键 (Enter) 时触发相同的回调。

阶段三:全局策略拦截 (Global Interception)

目标:实现“黑屏唤醒”和“特定入口跳转”的业务规则。

创建 TVEventInterceptor

文件路径:lib/plugin/pl_player/widgets/tv_event_interceptor.dart (新建)

逻辑实现:

包裹播放器整体视图。

在 onKeyEvent 中检查 controller.showControls.value (控制栏是否可见)。

如果不可见 (Hidden):

OK: controller.controls = true -> tvFocusManager.seekBarNode.requestFocus() -> handled。

UP: controller.controls = true -> tvFocusManager.playButtonNode.requestFocus() -> handled。

DOWN: controller.controls = true -> tvFocusManager.qualityButtonNode.requestFocus() -> handled。

如果可见 (Visible):

返回 ignored (放行,让组件层处理)。

技巧:使用 addPostFrameCallback 确保 UI 渲染完成后再请求焦点。

阶段四:组装与集成 (Assembly)

目标:将上述模块组装到视图层,建立物理布局关系。

修改播放器主视图

文件路径:lib/pages/video/view.dart 或 lib/plugin/pl_player/view.dart

动作:

找到播放器的根 Widget(通常是 Stack)。

用 TVEventInterceptor 包裹整个 Player View。

重构底部控制栏布局

文件路径:lib/plugin/pl_player/widgets/bottom_control.dart

动作:

确保整体结构为纵向布局 (Column):

Top Row (主要控制): 将播放/暂停按钮替换为使用 tvFocusManager.playButtonNode 的 FocusableBtn。

Middle: 将 Slider 用 InteractiveSeekBar 包裹,绑定 tvFocusManager.seekBarNode。

Bottom Row (次要控制): 将画质/选集按钮替换为使用 tvFocusManager.qualityButtonNode 的 FocusableBtn。

布局验证:确保这三部分在 Column 中是物理相邻的,这样 Flutter 的默认导航算法(FocusTraversal)会自动处理 Up/Down 切换。

验证与验收清单 (Checklist)

执行完上述步骤后,请按以下顺序测试:

唤醒测试:

播放中等待控制栏消失。

按 OK:控制栏应出现,进度条高亮,视频继续播放(未暂停)。

等待消失,按 上:控制栏出现,播放按钮高亮。

等待消失,按 下:控制栏出现,画质按钮高亮。

导航测试:

焦点在进度条时,按 上:焦点跳到播放按钮。

焦点在播放按钮时,按 下:焦点跳回进度条。

焦点在进度条时,按 下:焦点跳到画质按钮。

焦点在播放按钮行,按 左/右:焦点在同行的按钮间切换(如从播放跳到下一集)。

功能测试:

焦点在进度条时,按 左/右:视频进度改变,焦点不跑偏。

焦点在进度条时,按 OK:视频暂停/继续。

焦点在画质按钮时,按 OK:弹出画质菜单。

补充技巧

调试焦点:在 MaterialApp 中设置 debugShowCheckedModeBanner: false 之外,可以使用 FocusManager.instance.debugLabel 查看当前焦点的日志。

防止自动隐藏:在 controller.dart 的 hideTaskControls 逻辑中,务必保留之前提到的判断:if (hasFocus || isPaused) return;,防止用户操作到一半控制栏突然消失。

@UcnacDx2 UcnacDx2 reopened this Dec 14, 2025
Refactors the player controls to be D-pad friendly, following the
Leanback layout principles.

- Creates a TVFocusManager to manage focus nodes.
- Creates FocusableBtn and InteractiveSeekBar for focusable widgets.
- Creates TVEventInterceptor to handle waking up the controls.
- Refactors the bottom_control.dart to a three-row layout.
- Updates player_focus.dart to handle D-pad navigation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat

3 participants