11import * as React from 'react' ;
22
33import { useSignal } from 'maverick.js/react' ;
4- import { isKeyboardClick , uppercaseFirstChar } from 'maverick.js/std' ;
4+ import { isArray , isKeyboardClick , uppercaseFirstChar } from 'maverick.js/std' ;
55import { isTrackCaptionKind , type DefaultLayoutWord , type TooltipPlacement } from 'vidstack' ;
66
77import { useAudioOptions } from '../../../hooks/options/use-audio-options' ;
88import { useCaptionOptions } from '../../../hooks/options/use-caption-options' ;
99import { useChapterOptions } from '../../../hooks/options/use-chapter-options' ;
10- import { usePlaybackRateOptions } from '../../../hooks/options/use-playback-rate-options' ;
1110import { useVideoQualityOptions } from '../../../hooks/options/use-video-quality-options' ;
1211import { useResizeObserver } from '../../../hooks/use-dom' ;
1312import { useMediaContext } from '../../../hooks/use-media-context' ;
1413import { useMediaState } from '../../../hooks/use-media-state' ;
15- import { createComputed } from '../../../hooks/use-signals' ;
1614import { isRemotionSource } from '../../../providers/remotion/type-check' ;
1715import { MediaAnnouncer } from '../../announcer' ;
1816import type { TimeSliderInstance } from '../../primitives/instances' ;
@@ -29,6 +27,7 @@ import { Captions } from '../../ui/captions';
2927import { ChapterTitle } from '../../ui/chapter-title' ;
3028import * as Menu from '../../ui/menu' ;
3129import * as AudioGainSlider from '../../ui/sliders/audio-gain-slider' ;
30+ import * as SpeedSlider from '../../ui/sliders/speed-slider' ;
3231import * as TimeSlider from '../../ui/sliders/time-slider' ;
3332import * as VolumeSlider from '../../ui/sliders/volume-slider' ;
3433import * as Thumbnail from '../../ui/thumbnail' ;
@@ -630,40 +629,16 @@ export { DefaultChaptersMenu };
630629 * -----------------------------------------------------------------------------------------------*/
631630
632631function DefaultSettingsMenu ( { tooltip, placement, portalClass, slots } : DefaultMediaMenuProps ) {
633- const { $state } = useMediaContext ( ) ,
634- {
632+ const {
635633 showMenuDelay,
636634 icons : Icons ,
637635 isSmallLayout,
638636 menuGroup,
639637 noModal,
640- playbackRates,
641- noAudioGainSlider,
642638 } = useDefaultLayoutContext ( ) ,
643639 settingsText = useDefaultLayoutWord ( 'Settings' ) ,
644640 $viewType = useMediaState ( 'viewType' ) ,
645- $offset = ! isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0 ,
646- // Create as a computed signal to avoid unnecessary re-rendering.
647- $$hasMenuItems = createComputed ( ( ) => {
648- const {
649- canSetPlaybackRate,
650- canSetQuality,
651- canSetAudioGain,
652- qualities,
653- audioTracks,
654- hasCaptions,
655- } = $state ;
656- return (
657- ! ! ( canSetPlaybackRate ( ) && playbackRates ?. length ) ||
658- ! ! ( canSetQuality ( ) && qualities ( ) . length ) ||
659- ! ! audioTracks ( ) . length ||
660- ( ! noAudioGainSlider && canSetAudioGain ( ) ) ||
661- hasCaptions ( )
662- ) ;
663- } , [ playbackRates , noAudioGainSlider ] ) ,
664- $hasMenuItems = useSignal ( $$hasMenuItems ) ;
665-
666- if ( ! $hasMenuItems ) return null ;
641+ $offset = ! isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0 ;
667642
668643 const Content = (
669644 < Menu . Content
@@ -672,12 +647,10 @@ function DefaultSettingsMenu({ tooltip, placement, portalClass, slots }: Default
672647 offset = { $offset }
673648 >
674649 { slot ( slots , 'settingsMenuStartItems' , null ) }
675- < DefaultMenuLoopCheckbox />
650+ < DefaultPlaybackSubmenu />
676651 < DefaultAccessibilitySubmenu />
677652 < DefaultAudioSubmenu />
678653 < DefaultCaptionSubmenu />
679- < DefaultSpeedSubmenu />
680- < DefaultQualitySubmenu />
681654 { slot ( slots , 'settingsMenuEndItems' , null ) }
682655 </ Menu . Content >
683656 ) ;
@@ -707,14 +680,34 @@ function DefaultSettingsMenu({ tooltip, placement, portalClass, slots }: Default
707680DefaultSettingsMenu . displayName = 'DefaultSettingsMenu' ;
708681export { DefaultSettingsMenu } ;
709682
683+ /* -------------------------------------------------------------------------------------------------
684+ * DefaultPlaybackSubmenu
685+ * -----------------------------------------------------------------------------------------------*/
686+
687+ function DefaultPlaybackSubmenu ( ) {
688+ const label = useDefaultLayoutWord ( 'Playback' ) ,
689+ { icons : Icons } = useDefaultLayoutContext ( ) ;
690+
691+ return (
692+ < Menu . Root className = "vds-accessibility-menu vds-menu" >
693+ < DefaultSubmenuButton label = { label } Icon = { Icons . Menu . Playback } />
694+ < Menu . Content className = "vds-menu-items" >
695+ < DefaultMenuLoopCheckbox />
696+ < DefaultMenuSpeedSlider />
697+ </ Menu . Content >
698+ </ Menu . Root >
699+ ) ;
700+ }
701+
702+ DefaultPlaybackSubmenu . displayName = 'DefaultPlaybackSubmenu' ;
703+
710704/* -------------------------------------------------------------------------------------------------
711705 * DefaultMenuLoopCheckbox
712706 * -----------------------------------------------------------------------------------------------*/
713707
714708function DefaultMenuLoopCheckbox ( ) {
715709 const label = 'Loop' ,
716710 { remote } = useMediaContext ( ) ,
717- { icons : Icons } = useDefaultLayoutContext ( ) ,
718711 translatedLabel = useDefaultLayoutWord ( label ) ;
719712
720713 function onChange ( checked : boolean , trigger ?: Event ) {
@@ -723,7 +716,6 @@ function DefaultMenuLoopCheckbox() {
723716
724717 return (
725718 < div className = "vds-menu-item vds-menu-item-checkbox" >
726- < Icons . Menu . Loop className = "vds-menu-checkbox-icon" />
727719 < div className = "vds-menu-checkbox-label" > { translatedLabel } </ div >
728720 < DefaultMenuCheckbox label = { label } storageKey = "vds-player::user-loop" onChange = { onChange } />
729721 </ div >
@@ -732,6 +724,61 @@ function DefaultMenuLoopCheckbox() {
732724
733725DefaultMenuLoopCheckbox . displayName = 'DefaultMenuLoopCheckbox' ;
734726
727+ /* -------------------------------------------------------------------------------------------------
728+ * DefaultMenuSpeedSlider
729+ * -----------------------------------------------------------------------------------------------*/
730+
731+ function DefaultMenuSpeedSlider ( ) {
732+ const { icons : Icons } = useDefaultLayoutContext ( ) ,
733+ $playbackRate = useMediaState ( 'playbackRate' ) ,
734+ label = useDefaultLayoutWord ( 'Speed' ) ,
735+ normalText = useDefaultLayoutWord ( 'Normal' ) ,
736+ value = $playbackRate === 1 ? normalText : $playbackRate + 'x' ;
737+
738+ return (
739+ < div className = "vds-menu-item vds-menu-item-slider" >
740+ < div className = "vds-menu-slider-title" >
741+ < span className = "vds-menu-slider-label" > { label } </ span >
742+ < span className = "vds-menu-slider-value" > { value } </ span >
743+ </ div >
744+ < div className = "vds-menu-slider-group" >
745+ < Icons . Menu . SpeedDown className = "vds-icon" />
746+ < DefaultSpeedSlider />
747+ < Icons . Menu . SpeedUp className = "vds-icon" />
748+ </ div >
749+ </ div >
750+ ) ;
751+ }
752+
753+ DefaultMenuSpeedSlider . displayName = 'DefaultMenuSpeedSlider' ;
754+
755+ /* -------------------------------------------------------------------------------------------------
756+ * DefaultSpeedSlider
757+ * -----------------------------------------------------------------------------------------------*/
758+
759+ function DefaultSpeedSlider ( ) {
760+ const label = useDefaultLayoutWord ( 'Speed' ) ,
761+ { playbackRates : rates } = useDefaultLayoutContext ( ) ,
762+ min = ( isArray ( rates ) ? rates [ 0 ] : rates ?. min ) || 0 ,
763+ max = ( isArray ( rates ) ? rates [ rates . length - 1 ] : rates ?. max ) || 2 ,
764+ step = ( isArray ( rates ) ? rates [ 1 ] - rates [ 0 ] : rates ?. step ) || 0.25 ;
765+ return (
766+ < SpeedSlider . Root
767+ className = "vds-speed-slider vds-slider"
768+ aria-label = { label }
769+ min = { min }
770+ max = { max }
771+ step = { step }
772+ >
773+ < SpeedSlider . Track className = "vds-slider-track" />
774+ < SpeedSlider . TrackFill className = "vds-slider-track-fill vds-slider-track" />
775+ < SpeedSlider . Thumb className = "vds-slider-thumb" />
776+ </ SpeedSlider . Root >
777+ ) ;
778+ }
779+
780+ DefaultSpeedSlider . displayName = 'DefaultSpeedSlider' ;
781+
735782/* -------------------------------------------------------------------------------------------------
736783 * DefaultAccessibilitySubmenu
737784 * -----------------------------------------------------------------------------------------------*/
@@ -924,9 +971,9 @@ function DefaultMenuAudioGainSlider() {
924971 < span className = "vds-menu-slider-value" > { value } </ span >
925972 </ div >
926973 < div className = "vds-menu-slider-group" >
927- < Icons . MuteButton . VolumeLow className = "vds-icon" />
974+ < Icons . Menu . AudioBoostDown className = "vds-icon" />
928975 < DefaultAudioGainSlider />
929- < Icons . MuteButton . VolumeHigh className = "vds-icon" />
976+ < Icons . Menu . AudioBoostUp className = "vds-icon" />
930977 </ div >
931978 </ div >
932979 ) ;
@@ -1001,60 +1048,12 @@ function DefaultAudioTracksSubmenu() {
10011048
10021049DefaultAudioTracksSubmenu . displayName = 'DefaultAudioTracksSubmenu' ;
10031050
1004- /* -------------------------------------------------------------------------------------------------
1005- * DefaultSpeedSubmenu
1006- * -----------------------------------------------------------------------------------------------*/
1007-
1008- function DefaultSpeedSubmenu ( ) {
1009- const { icons : Icons , playbackRates } = useDefaultLayoutContext ( ) ,
1010- label = useDefaultLayoutWord ( 'Speed' ) ,
1011- normalText = useDefaultLayoutWord ( 'Normal' ) ,
1012- options = usePlaybackRateOptions ( {
1013- normalLabel : normalText ,
1014- rates : playbackRates ,
1015- } ) ,
1016- hint = options . selectedValue === '1' ? normalText : options . selectedValue + 'x' ;
1017-
1018- if ( options . disabled ) return null ;
1019-
1020- return (
1021- < Menu . Root className = "vds-speed-menu vds-menu" >
1022- < DefaultSubmenuButton
1023- label = { label }
1024- hint = { hint }
1025- disabled = { options . disabled }
1026- Icon = { Icons . Menu . Speed }
1027- />
1028- < Menu . Content className = "vds-menu-items" >
1029- < Menu . RadioGroup
1030- className = "vds-speed-radio-group vds-radio-group"
1031- value = { options . selectedValue }
1032- >
1033- { options . map ( ( { label, value, select } ) => (
1034- < Menu . Radio
1035- className = "vds-speed-radio vds-radio"
1036- value = { value }
1037- onSelect = { select }
1038- key = { value }
1039- >
1040- < div className = "vds-radio-check" />
1041- < span className = "vds-radio-label" > { label } </ span >
1042- </ Menu . Radio >
1043- ) ) }
1044- </ Menu . RadioGroup >
1045- </ Menu . Content >
1046- </ Menu . Root >
1047- ) ;
1048- }
1049-
1050- DefaultSpeedSubmenu . displayName = 'DefaultSpeedSubmenu' ;
1051-
10521051/* -------------------------------------------------------------------------------------------------
10531052 * DefaultQualitySubmenu
10541053 * -----------------------------------------------------------------------------------------------*/
10551054
10561055function DefaultQualitySubmenu ( ) {
1057- const { hideQualityBitrate, icons : Icons } = useDefaultLayoutContext ( ) ,
1056+ const { hideQualityBitrate } = useDefaultLayoutContext ( ) ,
10581057 label = useDefaultLayoutWord ( 'Quality' ) ,
10591058 autoText = useDefaultLayoutWord ( 'Auto' ) ,
10601059 options = useVideoQualityOptions ( { auto : autoText , sort : 'descending' } ) ,
@@ -1068,12 +1067,7 @@ function DefaultQualitySubmenu() {
10681067
10691068 return (
10701069 < Menu . Root className = "vds-quality-menu vds-menu" >
1071- < DefaultSubmenuButton
1072- label = { label }
1073- hint = { hint }
1074- disabled = { options . disabled }
1075- Icon = { Icons . Menu . Quality }
1076- />
1070+ < DefaultSubmenuButton label = { label } hint = { hint } disabled = { options . disabled } />
10771071 < Menu . Content className = "vds-menu-items" >
10781072 < Menu . RadioGroup
10791073 className = "vds-quality-radio-group vds-radio-group"
0 commit comments