[FEAT] Play all the playlist in background when phone is stop.

the explanation is the android does not support to update the GUI interface
when the application is down, but it support some update of the playing file
in the audio tag.
This commit is contained in:
Edouard DUPIN 2025-03-23 21:01:54 +01:00
parent 3beafab7e1
commit ba7b6e4755
3 changed files with 189 additions and 158 deletions

View File

@ -30,6 +30,7 @@ import { useSpecificGender } from '@/service/Gender';
import { useSpecificTrack } from '@/service/Track'; import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access'; import { DataUrlAccess } from '@/utils/data-url-access';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { usePageVisibility } from '@/utils/visibleook';
import { Slider } from './ui/slider'; import { Slider } from './ui/slider';
@ -89,6 +90,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
: '' : ''
); );
}, [dataTrack, setMediaSource]); }, [dataTrack, setMediaSource]);
const { isVisible } = usePageVisibility();
const backColor = useColorModeValue('back.100', 'back.800'); const backColor = useColorModeValue('back.100', 'back.800');
const configButton = { const configButton = {
borderRadius: 'full', borderRadius: 'full',
@ -201,7 +203,6 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
if (!audioRef || !audioRef.current) { if (!audioRef || !audioRef.current) {
return; return;
} }
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime); setTimeProgress(audioRef.current.currentTime);
}; };
const onDurationChange = (event) => {}; const onDurationChange = (event) => {};
@ -221,150 +222,154 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
}; };
return ( return (
<> <>
{!isNullOrUndefined(trackOffset) && ( {isVisible && (
<Flex <>
position="absolute" {!isNullOrUndefined(trackOffset) && (
height="150px" <Flex
minHeight="150px" position="absolute"
paddingY="5px" height="150px"
paddingX="10px" minHeight="150px"
marginX="15px" paddingY="5px"
bottom={0} paddingX="10px"
//top="calc(100% - 150px)" marginX="15px"
left={0} bottom={0}
right={0} //top="calc(100% - 150px)"
zIndex={1000} left={0}
borderWidth="1px" right={0}
borderColor="brand.900" zIndex={1000}
bgColor={backColor} borderWidth="1px"
borderTopRadius="10px" borderColor="brand.900"
direction="column" bgColor={backColor}
> borderTopRadius="10px"
<Text direction="column"
alignContent="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
// noOfLines={1}
>
{dataTrack?.name ?? '???'}
</Text>
<Text
alignContent="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
// noOfLines={1}
>
{dataArtists.map((data) => data.name).join(', ')} /{' '}
{dataAlbum && dataAlbum?.name}
{dataGender && ` / ${dataGender.name}`}
</Text>
<Box width="full" paddingX="15px">
<Slider
defaultValue={[0]}
value={[timeProgress]}
min={0}
max={duration}
step={0.1}
onValueChange={(e) => onSeek(e.value)}
variant="outline"
colorPalette="brand"
marks={marks()}
//focusCapture={false}
> >
<SliderTrack <Text
bg="brand.200" alignContent="left"
height="10px" fontSize="20px"
borderRadius="full" fontWeight="bold"
></SliderTrack> userSelect="none"
</Slider> marginRight="auto"
</Box> overflow="hidden"
<Flex> // noOfLines={1}
<Text >
alignContent="left" {dataTrack?.name ?? '???'}
fontSize="16px" </Text>
userSelect="none" <Text
marginRight="auto" alignContent="left"
overflow="hidden" fontSize="16px"
// noOfLines={1} userSelect="none"
> marginRight="auto"
{formatTime(timeProgress)} overflow="hidden"
</Text> // noOfLines={1}
<Text alignContent="left" fontSize="16px" userSelect="none"> >
{formatTime(duration)} {dataArtists.map((data) => data.name).join(', ')} /{' '}
</Text> {dataAlbum && dataAlbum?.name}
</Flex> {dataGender && ` / ${dataGender.name}`}
<Flex gap="5px"> </Text>
<IconButton <Box width="full" paddingX="15px">
{...configButton} <Slider
aria-label={'Play'} defaultValue={[0]}
onClick={onPlay} value={[timeProgress]}
variant="ghost" min={0}
> max={duration}
{isPlaying ? ( step={0.1}
<MdPause style={{ width: '100%', height: '100%' }} /> onValueChange={(e) => onSeek(e.value)}
) : ( variant="outline"
<MdPlayArrow style={{ width: '100%', height: '100%' }} /> colorPalette="brand"
)} marks={marks()}
</IconButton> //focusCapture={false}
<IconButton >
{...configButton} <SliderTrack
aria-label={'Stop'} bg="brand.200"
onClick={onStop} height="10px"
variant="ghost" borderRadius="full"
> ></SliderTrack>
<MdStop style={{ width: '100%', height: '100%' }} /> </Slider>
</IconButton> </Box>
<IconButton <Flex>
{...configButton} <Text
aria-label={'Previous track'} alignContent="left"
onClick={onNavigatePrevious} fontSize="16px"
marginLeft="auto" userSelect="none"
variant="ghost" marginRight="auto"
> overflow="hidden"
<MdNavigateBefore // noOfLines={1}
style={{ width: '100%', height: '100%' }} >
/>{' '} {formatTime(timeProgress)}
</IconButton> </Text>
<IconButton <Text alignContent="left" fontSize="16px" userSelect="none">
{...configButton} {formatTime(duration)}
aria-label={'jump 15sec in past'} </Text>
onClick={onFastRewind} </Flex>
variant="ghost" <Flex gap="5px">
> <IconButton
<MdFastRewind style={{ width: '100%', height: '100%' }} /> {...configButton}
</IconButton> aria-label={'Play'}
<IconButton onClick={onPlay}
{...configButton} variant="ghost"
aria-label={'jump 15sec in future'} >
onClick={onFastForward} {isPlaying ? (
variant="ghost" <MdPause style={{ width: '100%', height: '100%' }} />
> ) : (
<MdFastForward style={{ width: '100%', height: '100%' }} /> <MdPlayArrow style={{ width: '100%', height: '100%' }} />
</IconButton> )}
<IconButton </IconButton>
{...configButton} <IconButton
aria-label={'Next track'} {...configButton}
marginRight="auto" aria-label={'Stop'}
onClick={onNavigateNext} onClick={onStop}
variant="ghost" variant="ghost"
> >
<MdNavigateNext style={{ width: '100%', height: '100%' }} /> <MdStop style={{ width: '100%', height: '100%' }} />
</IconButton> </IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'continue to the end'} aria-label={'Previous track'}
onClick={onTypePlay} onClick={onNavigatePrevious}
variant="ghost" marginLeft="auto"
> variant="ghost"
{playModeIcon[playingMode]} >
</IconButton> <MdNavigateBefore
</Flex> style={{ width: '100%', height: '100%' }}
</Flex> />{' '}
</IconButton>
<IconButton
{...configButton}
aria-label={'jump 15sec in past'}
onClick={onFastRewind}
variant="ghost"
>
<MdFastRewind style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'jump 15sec in future'}
onClick={onFastForward}
variant="ghost"
>
<MdFastForward style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'Next track'}
marginRight="auto"
onClick={onNavigateNext}
variant="ghost"
>
<MdNavigateNext style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'continue to the end'}
onClick={onTypePlay}
variant="ghost"
>
{playModeIcon[playingMode]}
</IconButton>
</Flex>
</Flex>
)}
</>
)} )}
<chakra.audio <chakra.audio

View File

@ -15,6 +15,7 @@ import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes'; import { SSORoutes } from '@/scene/sso/SSORoutes';
import { TrackRoutes } from '@/scene/track/TrackRoutes'; import { TrackRoutes } from '@/scene/track/TrackRoutes';
import { useHasRight } from '@/service/session'; import { useHasRight } from '@/service/session';
import { usePageVisibility } from '@/utils/visibleook';
import { AddPage } from './home/AddPage'; import { AddPage } from './home/AddPage';
import { SettingsPage } from './home/SettingsPage'; import { SettingsPage } from './home/SettingsPage';
@ -22,6 +23,7 @@ import { OnAirPage } from './onAir/OnAirPage';
export const AppRoutes = () => { export const AppRoutes = () => {
const { isReadable } = useHasRight('USER'); const { isReadable } = useHasRight('USER');
const { isVisible } = usePageVisibility();
return ( return (
<HistoryRouter <HistoryRouter
// @ts-expect-error // @ts-expect-error
@ -31,22 +33,27 @@ export const AppRoutes = () => {
<Routes> <Routes>
{/* Need to keep it in all case, it is the only way to log-in */} {/* Need to keep it in all case, it is the only way to log-in */}
<Route path="sso/*" element={<SSORoutes />} /> <Route path="sso/*" element={<SSORoutes />} />
{isReadable ? ( {/* Disable full display to prevent update of GUI when the application is hided */}
{isVisible && (
<> <>
<Route path="/" element={<Navigate to="home" replace />} /> {isReadable ? (
<Route path="home/*" element={<HomePage />} /> <>
<Route path="help/*" element={<HelpPage />} /> <Route path="/" element={<Navigate to="home" replace />} />
<Route path="settings/*" element={<SettingsPage />} /> <Route path="home/*" element={<HomePage />} />
<Route path="add/*" element={<AddPage />} /> <Route path="help/*" element={<HelpPage />} />
<Route path="on-air/*" element={<OnAirPage />} /> <Route path="settings/*" element={<SettingsPage />} />
<Route path="artist/*" element={<ArtistRoutes />} /> <Route path="add/*" element={<AddPage />} />
<Route path="album/*" element={<AlbumRoutes />} /> <Route path="on-air/*" element={<OnAirPage />} />
<Route path="gender/*" element={<GenderRoutes />} /> <Route path="artist/*" element={<ArtistRoutes />} />
<Route path="track/*" element={<TrackRoutes />} /> <Route path="album/*" element={<AlbumRoutes />} />
<Route path="*" element={<Error404 />} /> <Route path="gender/*" element={<GenderRoutes />} />
<Route path="track/*" element={<TrackRoutes />} />
<Route path="*" element={<Error404 />} />
</>
) : (
<Route path="*" element={<Error401 />} />
)}
</> </>
) : (
<Route path="*" element={<Error401 />} />
)} )}
</Routes> </Routes>
</HistoryRouter> </HistoryRouter>

View File

@ -0,0 +1,19 @@
import { useEffect, useState } from 'react';
export const usePageVisibility = () => {
const [isVisible, setIsVisible] = useState(!document.hidden);
useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return { isVisible };
};