Skip to content

Commit 640389f

Browse files
author
Greg Trihus
committed
Highlight and play segment logic
1 parent d7f790d commit 640389f

File tree

10 files changed

+224
-74
lines changed

10 files changed

+224
-74
lines changed

src/renderer/src/components/MediaRecord.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@ function MediaRecord(props: IProps) {
458458
return mediaStateRef.current.url;
459459
return '';
460460
};
461-
const handleLoadAudio = async () => {
462-
showMessage(t.loading);
461+
const handleLoadAudio = async (silent: boolean = false) => {
462+
if (!silent) showMessage(t.loading);
463463
if (loading || !mediaId) return;
464464
setLoading(true);
465465
reset();
@@ -483,11 +483,22 @@ function MediaRecord(props: IProps) {
483483

484484
useEffect(() => {
485485
if ((preload ?? 0) > 0 && !loading) {
486-
handleLoadAudio();
486+
handleLoadAudio(true);
487487
}
488488
// eslint-disable-next-line react-hooks/exhaustive-deps
489489
}, [preload]);
490490

491+
useEffect(() => {
492+
if (!mediaId) {
493+
reset();
494+
return;
495+
}
496+
if (!loading) {
497+
handleLoadAudio(true);
498+
}
499+
// eslint-disable-next-line react-hooks/exhaustive-deps
500+
}, [mediaId]);
501+
491502
const segments = '{}';
492503

493504
const content = (

src/renderer/src/components/PassageDetail/PassageDetailItem.tsx

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
import DeleteIcon from '@mui/icons-material/Delete';
1919
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
2020
import PauseIcon from '@mui/icons-material/Pause';
21-
import { FaForwardStep, FaEarListen } from 'react-icons/fa6';
21+
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
22+
import { FaEarListen } from 'react-icons/fa6';
2223
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
2324
import {
2425
ArtifactTypeSlug,
@@ -64,11 +65,18 @@ import DiscussionPanel from '../../components/Discussions/DiscussionPanel';
6465
import PassageDetailMobileLayout from './mobile/PassageDetailMobileLayout';
6566
import MobileWorkflowSteps from './mobile/MobileWorkflowSteps';
6667
import PassageAudioHeaderMobile from './mobile/PassageAudioHeaderMobile';
68+
import HighlightButton from './mobile/HighlightButton';
6769
import SegmentStatusMobile from './mobile/SegmentStatusMobile';
6870
import PassageDetailMobileFooter from './mobile/PassageDetailMobileFooter';
6971
import { WSAudioPlayerControls } from '../WSAudioPlayer';
7072
import { parseRegions } from '../../crud/useWavesurferRegions';
7173

74+
enum Activity {
75+
Listen,
76+
Record,
77+
Next,
78+
}
79+
7280
const PlayerRow = styled(Box)<BoxProps>(() => ({
7381
width: '100%',
7482
'& audio': {
@@ -121,6 +129,7 @@ export function PassageDetailItem(props: IProps) {
121129
const [organization] = useGlobal('organization');
122130
const [plan] = useGlobal('plan'); //will be constant here
123131
const { fetchMediaUrl, mediaState } = useFetchMediaUrl(reporter);
132+
const [activity, setActivity] = useState<Activity>(Activity.Listen);
124133
const [statusText, setStatusText] = useState('');
125134
const [canSave, setCanSave] = useState(false);
126135
const [defaultFilename, setDefaultFileName] = useState('');
@@ -406,14 +415,38 @@ export function PassageDetailItem(props: IProps) {
406415
setRecording(recorderRecordingRef.current || recorderPlayingRef.current);
407416
};
408417
const handleRecorderRecording = (recording: boolean) => {
418+
const wasRecording = recorderRecordingRef.current;
409419
recorderRecordingRef.current = recording;
410420
updateRecorderBusy();
421+
if (wasRecording && !recording) {
422+
setActivity(Activity.Next);
423+
}
411424
};
412425
const handleRecorderPlayStatus = (playing: boolean) => {
413426
recorderPlayingRef.current = playing;
414427
setIsRecorderPlaying(playing);
415428
updateRecorderBusy();
416429
};
430+
const handleSegmentFinished = () => {
431+
if (segments) {
432+
setActivity(Activity.Record);
433+
}
434+
};
435+
const nextSegmentPlayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
436+
null
437+
);
438+
const handleNextSegmentClick = () => {
439+
setActivity(Activity.Next);
440+
playerControlsRef.current?.nextSegment();
441+
if (nextSegmentPlayTimerRef.current) {
442+
clearTimeout(nextSegmentPlayTimerRef.current);
443+
}
444+
nextSegmentPlayTimerRef.current = setTimeout(() => {
445+
if (!playerControlsRef.current?.isPlaying()) {
446+
playerControlsRef.current?.togglePlay();
447+
}
448+
}, 75);
449+
};
417450
const onSegmentParamChange = (
418451
params: IRegionParams,
419452
teamDefault: boolean
@@ -474,6 +507,7 @@ export function PassageDetailItem(props: IProps) {
474507
? SaveSegments.showSaveButton
475508
: undefined
476509
}
510+
forceRegionOnly={Boolean(segments)}
477511
defaultSegParams={segParams}
478512
suggestedSegments={segString}
479513
verses={verses}
@@ -492,6 +526,7 @@ export function PassageDetailItem(props: IProps) {
492526
hideSegmentControls={false}
493527
onProgress={setPlayerProgress}
494528
onDuration={setPlayerDuration}
529+
onSegmentFinished={handleSegmentFinished}
495530
onInteraction={() => {
496531
showSegmentsChangedMessage();
497532
userSegmentInteractionRef.current = true;
@@ -596,24 +631,34 @@ export function PassageDetailItem(props: IProps) {
596631
justifyContent: 'center',
597632
alignItems: 'center',
598633
width: '100%',
634+
gap: 1,
599635
py: 1,
600636
}}
601637
>
602-
<IconButton
603-
aria-label="Listen"
604-
onClick={() => playerControlsRef.current?.togglePlay()}
638+
<HighlightButton
639+
ariaLabel="Listen"
640+
onClick={() => {
641+
setActivity(Activity.Listen);
642+
playerControlsRef.current?.togglePlay();
643+
}}
644+
disabled={false}
645+
highlight={activity === Activity.Listen}
605646
>
606647
<FaEarListen />
607-
</IconButton>
648+
</HighlightButton>
608649
<RecordButton
609650
recording={recorderRecordingRef.current}
610651
oneTryOnly={true}
611-
onClick={() => recorderControlsRef.current?.toggleRecord()}
652+
onClick={() => {
653+
setActivity(Activity.Record);
654+
recorderControlsRef.current?.toggleRecord();
655+
}}
612656
disabled={playing}
613657
isSmall={false}
614658
showText={Boolean(currentSegmentMediaId)}
615659
hasRecording={Boolean(currentSegmentMediaId)}
616660
isStopLogic={true}
661+
active={activity === Activity.Record}
617662
/>
618663
{canSave && (
619664
<PriButton
@@ -629,12 +674,16 @@ export function PassageDetailItem(props: IProps) {
629674
{ts.save}
630675
</PriButton>
631676
)}
632-
<IconButton
633-
aria-label="Next Segment"
634-
onClick={() => playerControlsRef.current?.nextSegment()}
677+
<HighlightButton
678+
ariaLabel="Next Segment"
679+
onClick={() => {
680+
handleNextSegmentClick();
681+
}}
682+
disabled={false}
683+
highlight={activity === Activity.Next}
635684
>
636-
<FaForwardStep />
637-
</IconButton>
685+
<ArrowForwardIcon />
686+
</HighlightButton>
638687
</Box>
639688
</>
640689
);
@@ -651,12 +700,17 @@ export function PassageDetailItem(props: IProps) {
651700
playing={playing}
652701
progress={playerProgress}
653702
duration={playerDuration}
654-
onTogglePlay={() => playerControlsRef.current?.togglePlay()}
703+
onTogglePlay={() => {
704+
setActivity(Activity.Listen);
705+
playerControlsRef.current?.togglePlay();
706+
}}
655707
onPrevSegment={() => playerControlsRef.current?.prevSegment()}
656-
onNextSegment={() => playerControlsRef.current?.nextSegment()}
657-
highlightPrev={currentSegmentIndex > 1}
708+
onNextSegment={handleNextSegmentClick}
709+
highlightPlay={activity === Activity.Listen}
710+
highlightPrev={false}
658711
highlightNext={
659-
currentSegmentIndex > 0 && currentSegmentIndex < segmentCount
712+
activity === Activity.Next &&
713+
currentSegmentIndex < segmentCount
660714
}
661715
/>
662716
</>
@@ -740,6 +794,7 @@ export function PassageDetailItem(props: IProps) {
740794
? SaveSegments.showSaveButton
741795
: undefined
742796
}
797+
forceRegionOnly={Boolean(segments)}
743798
defaultSegParams={segParams}
744799
suggestedSegments={segString}
745800
verses={verses}
@@ -762,6 +817,7 @@ export function PassageDetailItem(props: IProps) {
762817
showSegmentsChangedMessage();
763818
userSegmentInteractionRef.current = true;
764819
}}
820+
onSegmentFinished={handleSegmentFinished}
765821
/>
766822
<Box>
767823
<Box sx={rowProp}>

src/renderer/src/components/PassageDetail/PassageDetailPlayer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { UnsavedContext } from '../../context/UnsavedContext';
55
import {
66
IRegionParams,
77
IRegions,
8+
IRegion,
89
parseRegions,
910
} from '../../crud/useWavesurferRegions';
1011
import WSAudioPlayer, { WSAudioPlayerControls } from '../WSAudioPlayer';
@@ -74,7 +75,9 @@ export interface DetailPlayerProps {
7475
onSaveProgress?: (progress: number) => void;
7576
onDuration?: (duration: number) => void;
7677
onInteraction?: () => void;
78+
onSegmentFinished?: (segment: IRegion) => void;
7779
onTranscription?: (transcription: string) => void;
80+
forceRegionOnly?: boolean;
7881
allowZoomAndSpeed?: boolean;
7982
position?: number;
8083
width: number;
@@ -105,7 +108,9 @@ export function PassageDetailPlayer(props: DetailPlayerProps) {
105108
onSaveProgress,
106109
onDuration: onDurationProp,
107110
onInteraction,
111+
onSegmentFinished,
108112
onTranscription,
113+
forceRegionOnly,
109114
allowZoomAndSpeed,
110115
position,
111116
width,
@@ -477,6 +482,8 @@ export function PassageDetailPlayer(props: DetailPlayerProps) {
477482
onProgress={onProgress}
478483
onSaveProgress={onSaveProgress}
479484
onDuration={handleDuration}
485+
onSegmentPlaybackEnd={onSegmentFinished}
486+
forceRegionOnly={forceRegionOnly}
480487
controlsRef={controlsRef}
481488
hideToolbar={hideToolbar}
482489
hideSegmentControls={hideSegmentControls || hideSegmentChange}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { IconButton } from '@mui/material';
2+
import type { ReactNode } from 'react';
3+
4+
const highlightSx = {
5+
backgroundColor: 'primary.main',
6+
color: 'primary.contrastText',
7+
'&:hover': {
8+
backgroundColor: 'primary.dark',
9+
},
10+
};
11+
12+
interface HighlightButtonProps {
13+
ariaLabel: string;
14+
onClick: () => void;
15+
disabled: boolean;
16+
size?: 'small' | 'medium' | 'large';
17+
highlight?: boolean;
18+
children: ReactNode;
19+
}
20+
21+
export default function HighlightButton({
22+
ariaLabel,
23+
onClick,
24+
disabled,
25+
size,
26+
highlight,
27+
children,
28+
}: HighlightButtonProps) {
29+
return (
30+
<IconButton
31+
aria-label={ariaLabel}
32+
onClick={onClick}
33+
disabled={disabled}
34+
size={size}
35+
sx={highlight ? highlightSx : undefined}
36+
>
37+
{children}
38+
</IconButton>
39+
);
40+
}
Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Box, IconButton, Typography } from '@mui/material';
1+
import { Box, Typography } from '@mui/material';
22
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
33
import PauseIcon from '@mui/icons-material/Pause';
44
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
55
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
66
import Duration from '../../../control/Duration';
7+
import HighlightButton from './HighlightButton';
78

89
interface Props {
910
playing: boolean;
@@ -12,25 +13,19 @@ interface Props {
1213
onTogglePlay: () => void;
1314
onPrevSegment: () => void;
1415
onNextSegment: () => void;
16+
highlightPlay?: boolean;
1517
highlightPrev?: boolean;
1618
highlightNext?: boolean;
1719
}
1820

19-
const highlightSx = {
20-
backgroundColor: 'primary.main',
21-
color: 'primary.contrastText',
22-
'&:hover': {
23-
backgroundColor: 'primary.dark',
24-
},
25-
};
26-
2721
export default function PassageAudioHeaderMobile({
2822
playing,
2923
progress,
3024
duration,
3125
onTogglePlay,
3226
onPrevSegment,
3327
onNextSegment,
28+
highlightPlay,
3429
highlightPrev,
3530
highlightNext,
3631
}: Props) {
@@ -43,35 +38,37 @@ export default function PassageAudioHeaderMobile({
4338
display: 'flex',
4439
alignItems: 'center',
4540
gap: 1,
41+
px: 1,
4642
}}
4743
>
48-
<IconButton
49-
aria-label={playing ? 'Pause' : 'Play'}
44+
<HighlightButton
45+
ariaLabel={playing ? 'Pause' : 'Play'}
5046
onClick={onTogglePlay}
5147
disabled={!canPlay}
5248
size="large"
49+
highlight={highlightPlay}
5350
>
5451
{playing ? <PauseIcon /> : <PlayArrowIcon />}
55-
</IconButton>
52+
</HighlightButton>
5653
<Typography variant="body2" sx={{ flex: 1, minWidth: 0 }}>
5754
<Duration seconds={progress} /> {' / '} <Duration seconds={duration} />
5855
</Typography>
59-
<IconButton
60-
aria-label="Previous segment"
56+
<HighlightButton
57+
ariaLabel="Previous segment"
6158
onClick={onPrevSegment}
6259
disabled={!canPlay}
63-
sx={highlightPrev ? highlightSx : undefined}
60+
highlight={highlightPrev}
6461
>
6562
<ArrowBackIcon />
66-
</IconButton>
67-
<IconButton
68-
aria-label="Next segment"
63+
</HighlightButton>
64+
<HighlightButton
65+
ariaLabel="Next segment"
6966
onClick={onNextSegment}
7067
disabled={!canPlay}
71-
sx={highlightNext ? highlightSx : undefined}
68+
highlight={highlightNext}
7269
>
7370
<ArrowForwardIcon />
74-
</IconButton>
71+
</HighlightButton>
7572
</Box>
7673
);
7774
}

0 commit comments

Comments
 (0)