Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/renderer/src/business/player/usePlayerLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,26 @@ export const usePlayerLogic = (props: PlayerLogicProps) => {
const onCurrentSegment = (segment: IRegion | undefined) => {
let index = 0;
if (segment && segmentsRef.current) {
const segs = parseRegions(segmentsRef.current);
const sorted = parseRegions(segmentsRef.current).regions.sort(
(a: IRegion, b: IRegion) => a.start - b.start
);
const matchTol = 0.6;
index =
segs.regions
.sort((a: IRegion, b: IRegion) => a.start - b.start)
.findIndex(
(r: IRegion) => r.start <= segment?.start && r.end >= segment?.end
sorted.findIndex(
(r: IRegion) =>
Math.abs(r.start - segment.start) <= matchTol &&
Math.abs(r.end - segment.end) <= matchTol
) + 1;
if (index <= 0) {
index =
sorted.findIndex(
(r: IRegion, i: number) =>
segment.start >= r.start - 0.01 &&
(i === sorted.length - 1
? segment.start <= r.end + 0.01
: segment.start < r.end - 0.01)
) + 1;
}
} else {
if (setSegmentToWhole()) return;
}
Expand Down
263 changes: 250 additions & 13 deletions src/renderer/src/components/PassageDetail/PassageDetailMarkVerses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ import { PassageTypeEnum } from '../../model/passageType';
import { usePlanType } from '../../crud/usePlanType';
import { passageTypeFromRef } from '../../control/passageTypeFromRef';
import { useStepPermissions } from '../../utils/useStepPermission';
import { type WSAudioPlayerControls } from '../WSAudioPlayer';
import {
isMarkVersesTableRowCompleted,
isMarkVersesTableTailIncomplete,
} from '../../utils/markVersesSegmentColors';

const NotTable = 490;
const verseToolId = 'VerseTool';
/** Nudge past a join when seeking so the playhead lands in the right-hand segment. */
const SEGMENT_BOUNDARY_TOLERANCE_SEC = 0.1;
/** Table limits use one decimal; waveform uses float seconds — allow rounding drift. */
const SEGMENT_ROW_MATCH_TOLERANCE_SEC = 0.6;

const paperProps = { p: 2, m: 'auto', width: `calc(100% - 32px)` } as SxProps;

Expand Down Expand Up @@ -89,9 +98,19 @@ const StyledTable = styled('div')(({ theme }) => ({
verticalAlign: 'inherit !important',
'& .value-viewer': { textAlign: 'center' },
},
'& .lim.cur': {
'& .lim.done, & .ref.done': {
verticalAlign: 'inherit !important',
'& .value-viewer': { textAlign: 'center', backgroundColor: 'yellow' },
'& .value-viewer': {
textAlign: 'center',
backgroundColor: 'rgba(76, 175, 80, 0.35)',
},
},
'& .lim.cur, & .ref.cur': {
verticalAlign: 'inherit !important',
'& .value-viewer': {
textAlign: 'center',
backgroundColor: 'rgba(255, 235, 59, 0.5)',
},
},
'& .data-grid .Err': { backgroundColor: 'orange' },
}));
Expand Down Expand Up @@ -121,6 +140,8 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
passage,
currentstep,
currentSegment,
currentSegmentIndex,
setCurrentSegment,
setStepComplete,
gotoNextStep,
rowData,
Expand All @@ -142,6 +163,8 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
const segmentsRef = useRef('{}');
const passageRefs = useRef<string[]>([]);
const resettingSegmentsRef = useRef(false);
const playerControlsRef = useRef<WSAudioPlayerControls | null>(null);
const markVersesTailOpenRef = useRef(false);
const { canDoSectionStep } = useStepPermissions();
const hasPermission = canDoSectionStep(currentstep, section);
const { localizedArtifactType } = useArtifactType();
Expand Down Expand Up @@ -178,7 +201,6 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
[passage, isFlat]
);

const readOnlys = [true, false];
const widths = [200, 150];
const cClass = ['lim', 'ref'];

Expand Down Expand Up @@ -209,13 +231,24 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);

useEffect(() => {
markVersesTailOpenRef.current = isMarkVersesTableTailIncomplete(
data,
ColName.Limits
);
playerControlsRef.current?.applyMarkVersesRegionColors?.();
}, [data, pastedSegments]);

const rowCells = (row: string[], first = false) =>
row.map(
(v, i) =>
({
value: v,
width: widths[i],
readOnly: first || readOnlys[i],
readOnly:
first ||
i === ColName.Limits ||
(i === ColName.Ref && !`${row[ColName.Limits] ?? ''}`.trim()),
className: first
? 'cTitle'
: cClass[i] +
Expand Down Expand Up @@ -362,6 +395,132 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {

const formLim = ({ start, end }: IRegion) => `${d1(start)}-${d1(end)}`;

const getSegmentFromRow = (row?: ICell[]) => {
if (!row) return undefined;
const limits = `${row[ColName.Limits]?.value ?? ''}`.split('-');
if (limits.length !== 2) return undefined;
const start = parseFloat(limits[0]);
const end = parseFloat(limits[1]);
if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
return { start, end } as IRegion;
};

const parseCurrentSegmentRegion = (value: string) => {
const match = value.trim().match(/^([\d.]+)-([\d.]+)$/);
if (!match) return undefined;
const start = parseFloat(match[1]);
const end = parseFloat(match[2]);
if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
return { start, end } as IRegion;
};

const findCurrentTableRowIndex = (tableData: ICell[][]) => {
const existingHighlight = tableData.findIndex(
(row, index) =>
index > 0 &&
((row[ColName.Limits] as ICell).className ?? '').includes('cur')
);

const target = parseCurrentSegmentRegion(currentSegment);

if (target) {
for (let i = 1; i < tableData.length; i++) {
const seg = getSegmentFromRow(tableData[i]);
if (!seg) continue;
if (
Math.abs(seg.start - target.start) <=
SEGMENT_ROW_MATCH_TOLERANCE_SEC &&
Math.abs(seg.end - target.end) <= SEGMENT_ROW_MATCH_TOLERANCE_SEC
) {
return i;
}
}
let startOnlyMatch = -1;
for (let i = 1; i < tableData.length; i++) {
const seg = getSegmentFromRow(tableData[i]);
if (!seg) continue;
if (
Math.abs(seg.start - target.start) <= SEGMENT_ROW_MATCH_TOLERANCE_SEC
) {
startOnlyMatch = i;
}
}
if (startOnlyMatch > 0) return startOnlyMatch;
} else if (existingHighlight > 0) {
return existingHighlight;
}

if (
currentSegmentIndex > 0 &&
currentSegmentIndex < tableData.length &&
getSegmentFromRow(tableData[currentSegmentIndex])
) {
if (!target) return currentSegmentIndex;
const rowSeg = getSegmentFromRow(
tableData[currentSegmentIndex] as ICell[]
);
if (
rowSeg &&
Math.abs(rowSeg.start - target.start) <=
SEGMENT_ROW_MATCH_TOLERANCE_SEC &&
Math.abs(rowSeg.end - target.end) <= SEGMENT_ROW_MATCH_TOLERANCE_SEC
) {
return currentSegmentIndex;
}
}

return existingHighlight > 0 ? existingHighlight : -1;
};

const applyRowHighlight = (tableData: ICell[][], activeRow: number) => {
tableData.forEach((row, index) => {
if (index === 0) return;
const limits = row[ColName.Limits] as ICell;
const ref = row[ColName.Ref] as ICell;
const baseLim = 'lim';
const baseRef = (ref.className ?? 'ref')
.replace(/\s*(cur|done)\b/g, '')
.trim();
const rowDone = isMarkVersesTableRowCompleted(tableData, index, ColName.Limits);
const isCurrent = index === activeRow;
limits.className = isCurrent
? `${baseLim} cur`
: rowDone
? `${baseLim} done`
: baseLim;
ref.className = isCurrent
? `${baseRef} cur`
: rowDone
? `${baseRef} done`
: baseRef;
});
};

const applyCurrentRowHighlight = (tableData: ICell[][]) => {
const currentRow = findCurrentTableRowIndex(tableData);
tableData.forEach((row, index) => {
if (index === 0) return;
const limits = row[ColName.Limits] as ICell;
const ref = row[ColName.Ref] as ICell;
const baseLim = 'lim';
const baseRef = (ref.className ?? 'ref')
.replace(/\s*(cur|done)\b/g, '')
.trim();
const rowDone = isMarkVersesTableRowCompleted(tableData, index, ColName.Limits);
const isCurrent = index === currentRow;
limits.className = isCurrent
? `${baseLim} cur`
: rowDone
? `${baseLim} done`
: baseLim;
ref.className = isCurrent
? `${baseRef} cur`
: rowDone
? `${baseRef} done`
: baseRef;
});
};

const resetSegments = (regions: IRegion[]) => {
const segments = JSON.stringify({ regions });
// Add slight delay before setting pasted segments
Expand Down Expand Up @@ -403,10 +562,8 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
} else {
const row = dataRef.current[i + 1] as ICell[];
if ((row[ColName.Limits] as ICell).value !== formLim(r)) {
const value = formLim(r);
const limits = row[ColName.Limits] as ICell;
limits.value = value;
if (value === currentSegment.trim()) limits.className += ' cur';
limits.value = formLim(r);
change = true;
}
const ref = row[ColName.Ref] as ICell;
Expand Down Expand Up @@ -438,6 +595,7 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
});

if (change) {
applyCurrentRowHighlight(newData);
setData(newData);
if (reset) {
resetSegments(regions);
Expand All @@ -446,7 +604,87 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
}
};

const handleValueRenderer = (cell: ICell) => cell.value;
useEffect(() => {
if (dataRef.current.length === 0) return;
const newData = dataRef.current.map((row) =>
row.map((cell) => ({ ...cell }))
);
applyCurrentRowHighlight(newData);
setData(newData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSegment, currentSegmentIndex, numSegments]);

const sheetData = useMemo(
() =>
hasPermission
? data.map((row, rowIndex) =>
row.map((cell, colIndex) => ({
...cell,
readOnly:
rowIndex === 0 ||
colIndex === ColName.Limits ||
(colIndex === ColName.Ref &&
!`${(row[ColName.Limits] as ICell).value ?? ''}`.trim()),
}))
)
: data.map((r) => r.map((c) => ({ ...c, readOnly: true }))),
[data, hasPermission]
);

const handleRowClick = async (rowIndex: number) => {
const row = dataRef.current[rowIndex] as ICell[] | undefined;
const segment = getSegmentFromRow(row);
if (!row || !segment) return;

const limits = row[ColName.Limits] as ICell;
if ((limits.className ?? '').includes('cur')) return;

const newData = dataRef.current.map((r) => r.map((c) => ({ ...c })));
applyRowHighlight(newData, rowIndex);
setData(newData);
setCurrentSegment(segment, rowIndex);

const ctrl = playerControlsRef.current;
if (ctrl?.isReady()) {
const seekTime =
segment.start > 0
? segment.start + SEGMENT_BOUNDARY_TOLERANCE_SEC
: segment.start;
await ctrl.gotoTime(seekTime, segment);
setCurrentSegment(segment, rowIndex);
}
};

const handleSheetSelect = (selection: DataSheet.Selection) => {
const row = selection.start.i;
const col = selection.start.j;
if (row <= 0) return;
if (col === ColName.Ref) {
void handleRowClick(row);
}
};

const handleValueRenderer = (cell: ICell, row: number, col: number) => {
if (row > 0 && col === ColName.Limits && cell.value) {
return (
<span
role="button"
tabIndex={0}
style={{ cursor: 'pointer', display: 'block', width: '100%' }}
onClick={() => void handleRowClick(row)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
void handleRowClick(row);
}
}}
>
{cell.value}
</span>
);
}
return cell.value;
};
const setSegments = () => {
//make an iRegions array from the dataRef.current
const regions: IRegion[] = [];
Expand Down Expand Up @@ -624,17 +862,16 @@ export function PassageDetailMarkVerses({ width }: MarkVersesProps) {
onSegment={handleSegment}
suggestedSegments={pastedSegments}
allowZoomAndSpeed={true}
controlsRef={playerControlsRef}
markVersesTailOpenRef={markVersesTailOpenRef}
/>
<StyledPaper style={heightStyle}>
<StyledTable id="verse-sheet" data-testid="verse-sheet">
<DataSheet
data={
hasPermission
? data
: data.map((r) => r.map((c) => ({ ...c, readOnly: true })))
}
data={sheetData}
valueRenderer={handleValueRenderer}
onCellsChanged={handleCellsChanged}
onSelect={handleSheetSelect}
parsePaste={handleParsePaste}
/>
</StyledTable>
Expand Down
Loading
Loading