mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add audio tracks menu (#1714)
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
QualityIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpeechIcon,
|
||||
SpeedIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { AirPlayButton } from '@/ui/airplay-button';
|
||||
import { useAudioTrackOptions } from '@/ui/audio-track';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { useCaptionsOptions } from '@/ui/captions-radio-group';
|
||||
import { CastButton } from '@/ui/cast-button';
|
||||
@@ -168,12 +170,14 @@ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
|
||||
function SettingsMenu(): ReactNode {
|
||||
const playbackRate = usePlaybackRateOptions();
|
||||
const quality = useQualityOptions();
|
||||
const audioTrack = useAudioTrackOptions();
|
||||
const captions = useCaptionsOptions();
|
||||
const hasPlaybackRate = playbackRate?.state.availability === 'available';
|
||||
const hasQuality = quality?.state.availability === 'available';
|
||||
const hasAudioTrack = audioTrack?.state.availability === 'available';
|
||||
const hasCaptions = captions?.state.availability === 'available';
|
||||
|
||||
if (!hasPlaybackRate && !hasQuality && !hasCaptions) return null;
|
||||
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
|
||||
|
||||
return (
|
||||
<Menu.Root side="top" align="center">
|
||||
@@ -187,7 +191,7 @@ function SettingsMenu(): ReactNode {
|
||||
<Menu.Content className={menu.settings}>
|
||||
<Menu.View className={menu.rootView}>
|
||||
<div className={menu.group}>
|
||||
{hasQuality && quality ? (
|
||||
{hasQuality ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="quality"
|
||||
@@ -240,7 +244,58 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasPlaybackRate && playbackRate ? (
|
||||
|
||||
{hasAudioTrack ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="audio-track"
|
||||
className={cn(menu.item, 'media-menu__item--submenu')}
|
||||
render={(props) => (
|
||||
<div {...props}>
|
||||
<SpeechIcon className={icon} />
|
||||
<span>Audio</span>
|
||||
<span className={menu.hint}>
|
||||
<Menu.ItemValue className={menu.hintLabel} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Menu.Content className={menu.submenuPanel}>
|
||||
<Menu.Back className={menu.back}>
|
||||
<MenuChevron flipped />
|
||||
Audio
|
||||
</Menu.Back>
|
||||
<Menu.Separator className={menu.separator} />
|
||||
<Menu.RadioGroup
|
||||
className={menu.group}
|
||||
value={audioTrack.value}
|
||||
onValueChange={audioTrack.setValue}
|
||||
aria-label="Audio tracks"
|
||||
>
|
||||
{audioTrack.options.map((option) => (
|
||||
<Menu.RadioItem
|
||||
key={option.value}
|
||||
className={menu.item}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<Menu.ItemIndicator
|
||||
checked={option.value === audioTrack.value}
|
||||
forceMount
|
||||
className={menu.indicator}
|
||||
>
|
||||
<CheckIcon className={icon} />
|
||||
</Menu.ItemIndicator>
|
||||
</Menu.RadioItem>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
|
||||
{hasPlaybackRate ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="playback-rate"
|
||||
@@ -289,7 +344,8 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasCaptions && captions ? (
|
||||
|
||||
{hasCaptions ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="captions"
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
QualityIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpeechIcon,
|
||||
SpeedIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { AirPlayButton } from '@/ui/airplay-button';
|
||||
import { useAudioTrackOptions } from '@/ui/audio-track';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { useCaptionsOptions } from '@/ui/captions-radio-group';
|
||||
import { CastButton } from '@/ui/cast-button';
|
||||
@@ -109,12 +111,14 @@ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
|
||||
function SettingsMenu(): ReactNode {
|
||||
const playbackRate = usePlaybackRateOptions();
|
||||
const quality = useQualityOptions();
|
||||
const audioTrack = useAudioTrackOptions();
|
||||
const captions = useCaptionsOptions();
|
||||
const hasPlaybackRate = playbackRate?.state.availability === 'available';
|
||||
const hasQuality = quality?.state.availability === 'available';
|
||||
const hasAudioTrack = audioTrack?.state.availability === 'available';
|
||||
const hasCaptions = captions?.state.availability === 'available';
|
||||
|
||||
if (!hasPlaybackRate && !hasQuality && !hasCaptions) return null;
|
||||
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
|
||||
|
||||
return (
|
||||
<Menu.Root side="top" align="center">
|
||||
@@ -124,7 +128,7 @@ function SettingsMenu(): ReactNode {
|
||||
<Menu.Content className="media-popover media-menu media-menu--settings">
|
||||
<Menu.View className="media-menu__panel">
|
||||
<div className="media-menu__group">
|
||||
{hasQuality && quality ? (
|
||||
{hasQuality ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="quality"
|
||||
@@ -177,7 +181,58 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasPlaybackRate && playbackRate ? (
|
||||
|
||||
{hasAudioTrack ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="audio-track"
|
||||
className="media-menu__item media-menu__item--submenu"
|
||||
render={(props) => (
|
||||
<div {...props}>
|
||||
<SpeechIcon className="media-icon" />
|
||||
<span>Audio</span>
|
||||
<span className="media-menu__hint">
|
||||
<Menu.ItemValue className="media-menu__hint-label" />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Menu.Content className="media-menu__panel">
|
||||
<Menu.Back className="media-menu__back">
|
||||
<MenuChevron flipped />
|
||||
Audio
|
||||
</Menu.Back>
|
||||
<Menu.Separator className="media-menu__separator" />
|
||||
<Menu.RadioGroup
|
||||
className="media-menu__group"
|
||||
value={audioTrack.value}
|
||||
onValueChange={audioTrack.setValue}
|
||||
aria-label="Audio tracks"
|
||||
>
|
||||
{audioTrack.options.map((option) => (
|
||||
<Menu.RadioItem
|
||||
key={option.value}
|
||||
className="media-menu__item"
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<Menu.ItemIndicator
|
||||
checked={option.value === audioTrack.value}
|
||||
forceMount
|
||||
className="media-menu__indicator"
|
||||
>
|
||||
<CheckIcon className="media-icon" />
|
||||
</Menu.ItemIndicator>
|
||||
</Menu.RadioItem>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
|
||||
{hasPlaybackRate ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="playback-rate"
|
||||
@@ -226,7 +281,8 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasCaptions && captions ? (
|
||||
|
||||
{hasCaptions ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="captions"
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
QualityIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpeechIcon,
|
||||
SpeedIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { AirPlayButton } from '@/ui/airplay-button';
|
||||
import { useAudioTrackOptions } from '@/ui/audio-track';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { useCaptionsOptions } from '@/ui/captions-radio-group';
|
||||
import { CastButton } from '@/ui/cast-button';
|
||||
@@ -168,12 +170,14 @@ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
|
||||
function SettingsMenu(): ReactNode {
|
||||
const playbackRate = usePlaybackRateOptions();
|
||||
const quality = useQualityOptions();
|
||||
const audioTrack = useAudioTrackOptions();
|
||||
const captions = useCaptionsOptions();
|
||||
const hasPlaybackRate = playbackRate?.state.availability === 'available';
|
||||
const hasQuality = quality?.state.availability === 'available';
|
||||
const hasAudioTrack = audioTrack?.state.availability === 'available';
|
||||
const hasCaptions = captions?.state.availability === 'available';
|
||||
|
||||
if (!hasPlaybackRate && !hasQuality && !hasCaptions) return null;
|
||||
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
|
||||
|
||||
return (
|
||||
<Menu.Root side="top" align="center">
|
||||
@@ -187,7 +191,7 @@ function SettingsMenu(): ReactNode {
|
||||
<Menu.Content className={menu.settings}>
|
||||
<Menu.View className={menu.rootView}>
|
||||
<div className={menu.group}>
|
||||
{hasQuality && quality ? (
|
||||
{hasQuality ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="quality"
|
||||
@@ -240,7 +244,58 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasPlaybackRate && playbackRate ? (
|
||||
|
||||
{hasAudioTrack ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="audio-track"
|
||||
className={cn(menu.item, 'media-menu__item--submenu')}
|
||||
render={(props) => (
|
||||
<div {...props}>
|
||||
<SpeechIcon className={icon} />
|
||||
<span>Audio</span>
|
||||
<span className={menu.hint}>
|
||||
<Menu.ItemValue className={menu.hintLabel} />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Menu.Content className={menu.submenuPanel}>
|
||||
<Menu.Back className={menu.back}>
|
||||
<MenuChevron flipped />
|
||||
Audio
|
||||
</Menu.Back>
|
||||
<Menu.Separator className={menu.separator} />
|
||||
<Menu.RadioGroup
|
||||
className={menu.group}
|
||||
value={audioTrack.value}
|
||||
onValueChange={audioTrack.setValue}
|
||||
aria-label="Audio tracks"
|
||||
>
|
||||
{audioTrack.options.map((option) => (
|
||||
<Menu.RadioItem
|
||||
key={option.value}
|
||||
className={menu.item}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<Menu.ItemIndicator
|
||||
checked={option.value === audioTrack.value}
|
||||
forceMount
|
||||
className={menu.indicator}
|
||||
>
|
||||
<CheckIcon className={icon} />
|
||||
</Menu.ItemIndicator>
|
||||
</Menu.RadioItem>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
|
||||
{hasPlaybackRate ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="playback-rate"
|
||||
@@ -289,7 +344,8 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasCaptions && captions ? (
|
||||
|
||||
{hasCaptions ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="captions"
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
QualityIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpeechIcon,
|
||||
SpeedIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { AirPlayButton } from '@/ui/airplay-button';
|
||||
import { useAudioTrackOptions } from '@/ui/audio-track';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { useCaptionsOptions } from '@/ui/captions-radio-group';
|
||||
import { CastButton } from '@/ui/cast-button';
|
||||
@@ -109,12 +111,14 @@ function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
|
||||
function SettingsMenu(): ReactNode {
|
||||
const playbackRate = usePlaybackRateOptions();
|
||||
const quality = useQualityOptions();
|
||||
const audioTrack = useAudioTrackOptions();
|
||||
const captions = useCaptionsOptions();
|
||||
const hasPlaybackRate = playbackRate?.state.availability === 'available';
|
||||
const hasQuality = quality?.state.availability === 'available';
|
||||
const hasAudioTrack = audioTrack?.state.availability === 'available';
|
||||
const hasCaptions = captions?.state.availability === 'available';
|
||||
|
||||
if (!hasPlaybackRate && !hasQuality && !hasCaptions) return null;
|
||||
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
|
||||
|
||||
return (
|
||||
<Menu.Root side="top" align="center">
|
||||
@@ -124,7 +128,7 @@ function SettingsMenu(): ReactNode {
|
||||
<Menu.Content className="media-surface media-popover media-menu media-menu--settings">
|
||||
<Menu.View className="media-menu__panel">
|
||||
<div className="media-menu__group">
|
||||
{hasQuality && quality ? (
|
||||
{hasQuality ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="quality"
|
||||
@@ -177,7 +181,58 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasPlaybackRate && playbackRate ? (
|
||||
|
||||
{hasAudioTrack ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="audio-track"
|
||||
className="media-menu__item media-menu__item--submenu"
|
||||
render={(props) => (
|
||||
<div {...props}>
|
||||
<SpeechIcon className="media-icon" />
|
||||
<span>Audio</span>
|
||||
<span className="media-menu__hint">
|
||||
<Menu.ItemValue className="media-menu__hint-label" />
|
||||
<MenuChevron />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Menu.Content className="media-menu__panel">
|
||||
<Menu.Back className="media-menu__back">
|
||||
<MenuChevron flipped />
|
||||
Audio
|
||||
</Menu.Back>
|
||||
<Menu.Separator className="media-menu__separator" />
|
||||
<Menu.RadioGroup
|
||||
className="media-menu__group"
|
||||
value={audioTrack.value}
|
||||
onValueChange={audioTrack.setValue}
|
||||
aria-label="Audio tracks"
|
||||
>
|
||||
{audioTrack.options.map((option) => (
|
||||
<Menu.RadioItem
|
||||
key={option.value}
|
||||
className="media-menu__item"
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<Menu.ItemIndicator
|
||||
checked={option.value === audioTrack.value}
|
||||
forceMount
|
||||
className="media-menu__indicator"
|
||||
>
|
||||
<CheckIcon className="media-icon" />
|
||||
</Menu.ItemIndicator>
|
||||
</Menu.RadioItem>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
|
||||
{hasPlaybackRate ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="playback-rate"
|
||||
@@ -226,7 +281,8 @@ function SettingsMenu(): ReactNode {
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
) : null}
|
||||
{hasCaptions && captions ? (
|
||||
|
||||
{hasCaptions ? (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
type="captions"
|
||||
|
||||
Reference in New Issue
Block a user