@@ -217,27 +217,69 @@ function EditField<
217217 try {
218218 const selectedItems = store . get ( itemAtom ) ;
219219 if ( fieldName === "endTime" && showDurationInput ) {
220- const durationValue = Number ( rawValue . trim ( ) ) ;
221- if ( ! Number . isFinite ( durationValue ) || durationValue <= 0 ) return ;
220+ const trimmedValue = rawValue . trim ( ) ;
221+ const isDelta = trimmedValue . startsWith ( "+" ) || trimmedValue . startsWith ( "-" ) ;
222+ const parsedValue = Number ( trimmedValue ) ;
223+ if ( ! Number . isFinite ( parsedValue ) ) return ;
224+ if ( ! isDelta && parsedValue <= 0 ) return ;
222225 editLyricLines ( ( state ) => {
223226 for ( const line of state . lyricLines ) {
224227 if ( isWordField ) {
228+ const updates = new Map < string , { startTime ?: number , endTime ?: number } > ( ) ;
229+
230+ // First pass: Calculate all new end times for selected words
225231 for ( let wordIndex = 0 ; wordIndex < line . words . length ; wordIndex ++ ) {
226232 const word = line . words [ wordIndex ] ;
227233 if ( ! selectedItems . has ( word . id ) ) continue ;
234+
228235 const nextWord = line . words [ wordIndex + 1 ] ;
229236 const nextStartTime = nextWord ?. startTime ;
230- const newEndTime = word . startTime + durationValue ;
231- if (
232- typeof nextStartTime === "number" &&
233- newEndTime < nextStartTime
234- ) {
235- continue ;
237+ const originalEndTime = word . endTime ;
238+
239+ // Calculate new end time
240+ const newEndTimeRaw = isDelta ? word . endTime + parsedValue : word . startTime + parsedValue ;
241+ const newEndTime = Math . max ( word . startTime , newEndTimeRaw ) ;
242+
243+ // Store the update for the current word
244+ const wordUpdate = updates . get ( word . id ) || { } ;
245+ wordUpdate . endTime = newEndTime ;
246+ updates . set ( word . id , wordUpdate ) ;
247+
248+ // If it was synchronized, store the start time update for the next word
249+ if ( isDelta && nextWord && originalEndTime === nextStartTime ) {
250+ // We only move nextWord's startTime if the new end time doesn't exceed its original end time
251+ // to avoid inverting its duration (unless it's also selected, handled below)
252+ const nextWordOriginalEndTime = nextWord . endTime ;
253+ if ( newEndTime <= nextWordOriginalEndTime || selectedItems . has ( nextWord . id ) ) {
254+ const nextUpdate = updates . get ( nextWord . id ) || { } ;
255+ nextUpdate . startTime = newEndTime ;
256+ // Don't auto-fix nextWord.endTime here, let the second pass or its own delta fix it
257+ updates . set ( nextWord . id , nextUpdate ) ;
258+ }
259+ }
260+ }
261+
262+ // Second pass: Apply updates and ensure durations are valid
263+ for ( let wordIndex = 0 ; wordIndex < line . words . length ; wordIndex ++ ) {
264+ const word = line . words [ wordIndex ] ;
265+ const update = updates . get ( word . id ) ;
266+
267+ if ( update ) {
268+ if ( update . startTime !== undefined ) {
269+ word . startTime = update . startTime ;
270+ }
271+ if ( update . endTime !== undefined ) {
272+ word . endTime = update . endTime ;
273+ }
274+ // Ensure valid duration after applying updates
275+ if ( word . endTime < word . startTime ) {
276+ word . endTime = word . startTime ;
277+ }
236278 }
237- word . endTime = newEndTime ;
238279 }
239280 } else if ( selectedItems . has ( line . id ) ) {
240- line . endTime = line . startTime + durationValue ;
281+ const newEndTimeRaw = isDelta ? line . endTime + parsedValue : line . startTime + parsedValue ;
282+ line . endTime = Math . max ( line . startTime , newEndTimeRaw ) ;
241283 }
242284 }
243285 return state ;
0 commit comments