Skip to content

Commit a71d1d7

Browse files
committed
feat(audio): disable layered audio and volume sliders on iOS, clarify UI for iOS users
1 parent 7fbbfb5 commit a71d1d7

File tree

5 files changed

+90
-34
lines changed

5 files changed

+90
-34
lines changed

src/shared-components/molecules/PillControlBar.tsx

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useMantineTheme, Flex, Group, ActionIcon, Slider, Box } from '@mantine/core';
3+
import { useMantineTheme, Flex, Group, ActionIcon, Slider, Box, Tooltip } from '@mantine/core';
44
import { LuHeadphones, LuMusic, LuListMusic, LuChevronDown } from 'react-icons/lu';
55

66
interface PillControlBarProps {
@@ -17,6 +17,7 @@ interface PillControlBarProps {
1717
colorScheme?: 'light' | 'dark';
1818
sliderWidth?: number;
1919
gap?: number;
20+
isIOS?: boolean;
2021
}
2122

2223
export const PillControlBar = ({
@@ -33,6 +34,7 @@ export const PillControlBar = ({
3334
colorScheme = 'light',
3435
sliderWidth = 80,
3536
gap = 6,
37+
isIOS = false,
3638
}: PillControlBarProps) => {
3739
const theme = useMantineTheme();
3840

@@ -84,15 +86,30 @@ export const PillControlBar = ({
8486
>
8587
<LuHeadphones size={16} />
8688
</ActionIcon>
87-
<Slider
88-
value={voiceVolume}
89-
onChange={onVoiceVolumeChange}
90-
min={0}
91-
max={1}
92-
step={0.01}
93-
size="xs"
94-
style={{ width: sliderWidth }}
95-
/>
89+
{isIOS ? (
90+
<Tooltip label="Volume control is not available on iOS. Use your device volume buttons." position="top" withArrow>
91+
<Slider
92+
value={voiceVolume}
93+
onChange={onVoiceVolumeChange}
94+
min={0}
95+
max={1}
96+
step={0.01}
97+
size="xs"
98+
style={{ width: sliderWidth }}
99+
disabled
100+
/>
101+
</Tooltip>
102+
) : (
103+
<Slider
104+
value={voiceVolume}
105+
onChange={onVoiceVolumeChange}
106+
min={0}
107+
max={1}
108+
step={0.01}
109+
size="xs"
110+
style={{ width: sliderWidth }}
111+
/>
112+
)}
96113
</Group>
97114

98115
{/* Music */}
@@ -112,15 +129,30 @@ export const PillControlBar = ({
112129
>
113130
<LuMusic size={16} />
114131
</ActionIcon>
115-
<Slider
116-
value={musicVolume}
117-
onChange={onMusicVolumeChange}
118-
min={0}
119-
max={1}
120-
step={0.01}
121-
size="xs"
122-
style={{ width: sliderWidth }}
123-
/>
132+
{isIOS ? (
133+
<Tooltip label="Volume control is not available on iOS. Use your device volume buttons." position="top" withArrow>
134+
<Slider
135+
value={musicVolume}
136+
onChange={onMusicVolumeChange}
137+
min={0}
138+
max={1}
139+
step={0.01}
140+
size="xs"
141+
style={{ width: sliderWidth }}
142+
disabled
143+
/>
144+
</Tooltip>
145+
) : (
146+
<Slider
147+
value={musicVolume}
148+
onChange={onMusicVolumeChange}
149+
min={0}
150+
max={1}
151+
step={0.01}
152+
size="xs"
153+
style={{ width: sliderWidth }}
154+
/>
155+
)}
124156
</Group>
125157

126158
{/* Playlist */}

src/shared-components/organisms/Footer/components/StandardPlayer/StandardPlayer.mobile.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
getButtonStyles,
2121
getBottomRowStyle,
2222
} from './StandardPlayer.mobile.styles';
23+
import { isIOSMobile } from '@utils/platform';
2324

2425
export const StandardPlayerMobile = (props: StandardPlayerProps) => {
2526
const {
@@ -69,6 +70,7 @@ export const StandardPlayerMobile = (props: StandardPlayerProps) => {
6970
const displayTitle = getDisplayTitle(controlMode, isMusicEnabled, isNarrationEnabled, activeMusicTrack, activeVoiceTrack);
7071
const displayArtist = getDisplayArtist(isMusicEnabled, isNarrationEnabled, activeMusicTrack, activeVoiceTrack);
7172
const artworkSize = 56;
73+
const isIOS = isIOSMobile();
7274

7375
return (
7476
<Flex
@@ -203,6 +205,7 @@ export const StandardPlayerMobile = (props: StandardPlayerProps) => {
203205
colorScheme={colorScheme === 'dark' ? 'dark' : 'light'}
204206
sliderWidth={56}
205207
gap={4}
208+
isIOS={isIOS}
206209
/>
207210
</Center>
208211
</Flex>

src/shared-components/organisms/Footer/components/StandardPlayer/StandardPlayer.web.hook.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
22
import { useMantineTheme } from '@mantine/core';
33
import { useElementSize } from '@mantine/hooks';
44
import { StandardPlayerProps } from './StandardPlayer.types';
5+
import { isIOSMobile } from '@utils/platform';
56

67
// Custom hook for StandardPlayerWeb stateful logic
78
export function useStandardPlayerWeb(props: StandardPlayerProps) {
@@ -11,6 +12,7 @@ export function useStandardPlayerWeb(props: StandardPlayerProps) {
1112
const [controlMode, setControlMode] = useState<'music' | 'narration'>(props.isMusicEnabled ? 'music' : 'narration');
1213
const [isMusicHovered, setIsMusicHovered] = useState(false);
1314
const [isNarrationHovered, setIsNarrationHovered] = useState(false);
15+
const [layeredAudioMessage, setLayeredAudioMessage] = useState<string | null>(null);
1416

1517
useEffect(() => {
1618
if (props.isMusicEnabled && props.isNarrationEnabled) return;
@@ -19,6 +21,10 @@ export function useStandardPlayerWeb(props: StandardPlayerProps) {
1921
}, [props.isMusicEnabled, props.isNarrationEnabled]);
2022

2123
const toggleControlMode = () => {
24+
if (isIOSMobile() && props.isMusicEnabled && props.isNarrationEnabled) {
25+
setLayeredAudioMessage('Layered audio is not supported on iOS.');
26+
return;
27+
}
2228
setControlMode((prev) => (prev === 'narration' ? 'music' : 'narration'));
2329
};
2430

@@ -107,6 +113,7 @@ export function useStandardPlayerWeb(props: StandardPlayerProps) {
107113
displayTitle,
108114
displayArtist,
109115
iconProps,
116+
layeredAudioMessage,
110117
};
111118
}
112119

src/shared-components/organisms/Footer/components/StandardPlayer/StandardPlayer.web.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { Box, Text, ActionIcon, Group, Slider, Tooltip, Button, Flex } from '@mantine/core';
3+
import { Box, Text, ActionIcon, Group, Slider, Tooltip, Button, Flex, Alert } from '@mantine/core';
44
import {
55
LuChevronDown, LuPlay, LuPause, LuSkipBack, LuSkipForward, LuListMusic, LuMic, LuMusic,
66
LuHeadphones, LuVolume1, LuVolume2
@@ -35,6 +35,7 @@ import {
3535
import { useStandardPlayerWeb } from './StandardPlayer.web.hook';
3636
import { formatTime } from '../../Footer.logic';
3737
import { useRef } from 'react';
38+
import { isIOSMobile } from '@utils/platform';
3839

3940
// NOTE: This component assumes it's only rendered on desktop.
4041

@@ -64,6 +65,7 @@ export const StandardPlayerWeb = (props: StandardPlayerProps) => {
6465
displayTitle,
6566
displayArtist,
6667
iconProps,
68+
layeredAudioMessage,
6769
} = useStandardPlayerWeb(props);
6870

6971
const {
@@ -96,6 +98,8 @@ export const StandardPlayerWeb = (props: StandardPlayerProps) => {
9698
pauseVoice,
9799
} = props;
98100

101+
const isIOS = isIOSMobile();
102+
99103
return (
100104
<Flex
101105
align="center"
@@ -226,17 +230,16 @@ export const StandardPlayerWeb = (props: StandardPlayerProps) => {
226230
}}
227231
onMouseEnter={() => setIsNarrationHovered(true)}
228232
onMouseLeave={() => setIsNarrationHovered(false)}
233+
disabled={isIOS}
229234
>
230235
Narration
231236
</Button>
232-
{isNarrationEnabled && (
233-
<Tooltip label="Narration Volume" position="top" withArrow>
234-
<Group gap={2} wrap="nowrap" align="center" style={{ cursor: 'pointer' }}>
235-
<ActionIcon size="xs" variant="transparent" color={iconProps.color} style={{ pointerEvents: 'none', opacity: 0.7 }}><LuVolume1 size={12} color={colorScheme === 'dark' ? theme.white : 'currentColor'} /></ActionIcon>
236-
<Slider value={voiceVolume} onChange={onVoiceVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Narration volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} />
237-
<ActionIcon size="xs" variant="transparent" color={iconProps.color} style={{ pointerEvents: 'none', opacity: 0.7 }}><LuVolume2 size={14} color={colorScheme === 'dark' ? theme.white : 'currentColor'} /></ActionIcon>
238-
</Group>
237+
{isIOS ? (
238+
<Tooltip label="Volume control is not available on iOS. Use your device volume buttons." position="top" withArrow>
239+
<Slider value={voiceVolume} onChange={onVoiceVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Narration volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} disabled />
239240
</Tooltip>
241+
) : (
242+
<Slider value={voiceVolume} onChange={onVoiceVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Narration volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} />
240243
)}
241244
</Group>
242245
<Group gap="xs" align="center" wrap="nowrap" style={groupMarginRight}>
@@ -276,17 +279,16 @@ export const StandardPlayerWeb = (props: StandardPlayerProps) => {
276279
}}
277280
onMouseEnter={() => setIsMusicHovered(true)}
278281
onMouseLeave={() => setIsMusicHovered(false)}
282+
disabled={isIOS}
279283
>
280284
Music
281285
</Button>
282-
{isMusicEnabled && (
283-
<Tooltip label="Music Volume" position="top" withArrow>
284-
<Group gap={2} wrap="nowrap" align="center" style={{ cursor: 'pointer' }}>
285-
<ActionIcon size="xs" variant="transparent" color={iconProps.color} style={{ pointerEvents: 'none', opacity: 0.7 }}><LuVolume1 size={12} color={colorScheme === 'dark' ? theme.white : 'currentColor'} /></ActionIcon>
286-
<Slider value={musicVolume} onChange={onMusicVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Music volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} />
287-
<ActionIcon size="xs" variant="transparent" color={iconProps.color} style={{ pointerEvents: 'none', opacity: 0.7 }}><LuVolume2 size={14} color={colorScheme === 'dark' ? theme.white : 'currentColor'} /></ActionIcon>
288-
</Group>
286+
{isIOS ? (
287+
<Tooltip label="Volume control is not available on iOS. Use your device volume buttons." position="top" withArrow>
288+
<Slider value={musicVolume} onChange={onMusicVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Music volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} disabled />
289289
</Tooltip>
290+
) : (
291+
<Slider value={musicVolume} onChange={onMusicVolumeChange} min={0} max={1} step={0.01} size={2} thumbSize={10} style={sliderStyle} color={iconProps.color} aria-label="Music volume" styles={{ thumb: { transition: 'transform 0.1s ease', ':hover': { transform: 'scale(1.2)' } }, track: { transition: 'all 0.1s ease' } }} />
290292
)}
291293
</Group>
292294
</Box>
@@ -318,6 +320,11 @@ export const StandardPlayerWeb = (props: StandardPlayerProps) => {
318320
</Box>
319321
</Box>
320322
</Flex>
323+
{layeredAudioMessage && (
324+
<Alert color="yellow" style={{ position: 'absolute', bottom: 16, left: '50%', transform: 'translateX(-50%)', zIndex: 1000 }}>
325+
{layeredAudioMessage}
326+
</Alert>
327+
)}
321328
</Flex>
322329
);
323330
};

src/utils/platform.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Platform detection utilities for client-side feature gating
2+
3+
export function isIOSMobile(): boolean {
4+
if (typeof window === 'undefined' || typeof navigator === 'undefined') return false;
5+
// iPhone/iPod, not iPad or Mac, and must be touch device
6+
return /iPhone|iPod/.test(navigator.userAgent) && 'ontouchstart' in window;
7+
}

0 commit comments

Comments
 (0)