Autosave: 20260319-094613

This commit is contained in:
Flatlogic Bot 2026-03-19 09:46:13 +00:00
parent 990839e9ca
commit 5d79b82de2

View File

@ -392,7 +392,18 @@ const resolveDurationWithFallback = async (
} }
try { try {
const response = await axios.get(playbackUrl, { responseType: 'blob' }); const requestUrl =
playbackUrl.startsWith('http://') || playbackUrl.startsWith('https://')
? playbackUrl
: playbackUrl.replace(/^\/api(?=\/)/, '');
const token =
typeof window !== 'undefined' ? localStorage.getItem('token') || '' : '';
const response = await axios.get(requestUrl, {
responseType: 'blob',
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
const blobUrl = URL.createObjectURL(response.data); const blobUrl = URL.createObjectURL(response.data);
try { try {
const blobDuration = await readMediaDuration(blobUrl, mediaType); const blobDuration = await readMediaDuration(blobUrl, mediaType);
@ -538,7 +549,6 @@ const createDefaultElement = (
navType: getNavigationButtonKind(type), navType: getNavigationButtonKind(type),
iconUrl: '', iconUrl: '',
transitionReverseMode: 'auto_reverse', transitionReverseMode: 'auto_reverse',
transitionDurationSec: 0.7,
}; };
} }
@ -739,6 +749,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
useState<EditorMenuItem>('none'); useState<EditorMenuItem>('none');
const [transitionPreview, setTransitionPreview] = const [transitionPreview, setTransitionPreview] =
useState<TransitionPreviewState | null>(null); useState<TransitionPreviewState | null>(null);
const [pendingNavigationPageId, setPendingNavigationPageId] = useState('');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -748,7 +759,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const [newTransitionVideoUrl, setNewTransitionVideoUrl] = useState(''); const [newTransitionVideoUrl, setNewTransitionVideoUrl] = useState('');
const [newTransitionSupportsReverse, setNewTransitionSupportsReverse] = const [newTransitionSupportsReverse, setNewTransitionSupportsReverse] =
useState(true); useState(true);
const [newTransitionDurationSec, setNewTransitionDurationSec] = useState(0.7);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
const [resolvedDurationBySource, setResolvedDurationBySource] = useState< const [resolvedDurationBySource, setResolvedDurationBySource] = useState<
@ -984,8 +994,28 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
}); });
} }
if (newTransitionVideoUrl) {
targets.push({ source: newTransitionVideoUrl, mediaType: 'video' });
}
elements.forEach((element) => {
if (!isNavigationElementType(element.type)) return;
if (element.transitionVideoUrl) {
targets.push({ source: element.transitionVideoUrl, mediaType: 'video' });
}
if (element.reverseVideoUrl) {
targets.push({ source: element.reverseVideoUrl, mediaType: 'video' });
}
});
return targets; return targets;
}, [backgroundAudioUrl, backgroundVideoUrl, selectedElement]); }, [
backgroundAudioUrl,
backgroundVideoUrl,
elements,
newTransitionVideoUrl,
selectedElement,
]);
useEffect(() => { useEffect(() => {
let isCancelled = false; let isCancelled = false;
@ -1050,6 +1080,19 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
getKnownDurationForSource(selectedElement.mediaUrl || ''), getKnownDurationForSource(selectedElement.mediaUrl || ''),
); );
}, [getKnownDurationForSource, selectedElement]); }, [getKnownDurationForSource, selectedElement]);
const newTransitionDurationNote = useMemo(
() => formatDurationNote(getKnownDurationForSource(newTransitionVideoUrl)),
[getKnownDurationForSource, newTransitionVideoUrl],
);
const selectedTransitionDurationNote = useMemo(() => {
if (!selectedElement || !isNavigationElementType(selectedElement.type)) {
return 'Duration: unknown';
}
return formatDurationNote(
getKnownDurationForSource(selectedElement.transitionVideoUrl || ''),
);
}, [getKnownDurationForSource, selectedElement]);
useEffect(() => { useEffect(() => {
if (newTransitionVideoUrl) return; if (newTransitionVideoUrl) return;
@ -1057,6 +1100,32 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
setNewTransitionVideoUrl(transitionVideoAssetOptions[0].value); setNewTransitionVideoUrl(transitionVideoAssetOptions[0].value);
}, [newTransitionVideoUrl, transitionVideoAssetOptions]); }, [newTransitionVideoUrl, transitionVideoAssetOptions]);
useEffect(() => {
setElements((prev) => {
let hasChanges = false;
const next = prev.map((element) => {
if (!isNavigationElementType(element.type)) return element;
const resolvedDuration = getKnownDurationForSource(
element.transitionVideoUrl || '',
);
const nextDuration =
Number.isFinite(resolvedDuration) && Number(resolvedDuration) > 0
? Number(resolvedDuration)
: undefined;
if (element.transitionDurationSec === nextDuration) return element;
hasChanges = true;
return {
...element,
transitionDurationSec: nextDuration,
};
});
return hasChanges ? next : prev;
});
}, [getKnownDurationForSource]);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!projectId || !router.isReady || !isAuthReady) return; if (!projectId || !router.isReady || !isAuthReady) return;
@ -1643,7 +1712,13 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const sanitizedName = const sanitizedName =
String(newTransitionName || '').trim() || String(newTransitionName || '').trim() ||
`Transition ${Date.now().toString().slice(-4)}`; `Transition ${Date.now().toString().slice(-4)}`;
const parsedDuration = Number(newTransitionDurationSec); const resolvedDurationSec = getKnownDurationForSource(sanitizedVideoUrl);
if (!resolvedDurationSec) {
setErrorMessage(
'Could not resolve transition video duration yet. Please wait a moment and try again.',
);
return;
}
try { try {
setIsCreatingTransition(true); setIsCreatingTransition(true);
@ -1659,10 +1734,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
video_url: sanitizedVideoUrl, video_url: sanitizedVideoUrl,
audio_url: '', audio_url: '',
supports_reverse: Boolean(newTransitionSupportsReverse), supports_reverse: Boolean(newTransitionSupportsReverse),
duration_sec: duration_sec: resolvedDurationSec,
Number.isFinite(parsedDuration) && parsedDuration > 0
? parsedDuration
: 0.7,
}; };
await axios.post('/transitions', { data: payload }); await axios.post('/transitions', { data: payload });
@ -1680,7 +1752,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
} }
}, [ }, [
activePage?.environment, activePage?.environment,
newTransitionDurationSec, getKnownDurationForSource,
newTransitionName, newTransitionName,
newTransitionSupportsReverse, newTransitionSupportsReverse,
newTransitionVideoUrl, newTransitionVideoUrl,
@ -1924,6 +1996,44 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
element.navType === 'back' || element.type === 'navigation_prev' element.navType === 'back' || element.type === 'navigation_prev'
? 'back' ? 'back'
: 'forward'; : 'forward';
const configuredTargetId = String(element.targetPageId || '').trim();
const fallbackTargetId = (() => {
const currentPageIndex = pages.findIndex(
(page) => page.id === activePageId,
);
if (currentPageIndex < 0) return '';
const nextPageIndex =
direction === 'back' ? currentPageIndex - 1 : currentPageIndex + 1;
const nextPage = pages[nextPageIndex];
return nextPage ? String(nextPage.id || '').trim() : '';
})();
const targetPageId = configuredTargetId || fallbackTargetId;
if (!targetPageId) {
setErrorMessage('No target page available for this navigation button.');
return;
}
const hasPlayableTransition =
Boolean(element.transitionVideoUrl) &&
!(
direction === 'back' &&
element.transitionReverseMode === 'separate_video' &&
!element.reverseVideoUrl
);
if (!hasPlayableTransition) {
setPendingNavigationPageId('');
setTransitionPreview(null);
setActivePageId(targetPageId);
setSelectedElementId('');
setSelectedMenuItem('none');
setErrorMessage('');
return;
}
setPendingNavigationPageId(targetPageId);
openTransitionPreviewForElement(element, direction); openTransitionPreviewForElement(element, direction);
} }
return; return;
@ -2266,6 +2376,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const finishPreview = () => { const finishPreview = () => {
cleanupReverseFrame(); cleanupReverseFrame();
setTransitionPreview(null); setTransitionPreview(null);
setPendingNavigationPageId((pendingPageId) => {
const nextPageId = String(pendingPageId || '').trim();
if (nextPageId) {
setActivePageId(nextPageId);
setSelectedElementId('');
setSelectedMenuItem('none');
setErrorMessage('');
}
return '';
});
}; };
const configuredDurationMs = const configuredDurationMs =
@ -2700,6 +2820,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
</option> </option>
))} ))}
</select> </select>
<p className='text-[11px] text-gray-500'>
Transition duration is automatic from video metadata.{' '}
{newTransitionDurationNote}
</p>
<label className='flex items-center gap-2 text-[11px] text-gray-700'> <label className='flex items-center gap-2 text-[11px] text-gray-700'>
<input <input
type='checkbox' type='checkbox'
@ -2710,18 +2834,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
/> />
Supports reverse playback Supports reverse playback
</label> </label>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
type='number'
min='0.2'
step='0.1'
value={newTransitionDurationSec}
onChange={(event) =>
setNewTransitionDurationSec(
Number(event.target.value || 0.7),
)
}
/>
<button <button
type='button' type='button'
className='menu-action-btn' className='menu-action-btn'
@ -2888,6 +3000,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
</option> </option>
))} ))}
</select> </select>
<p className='mt-1 text-[11px] text-gray-500'>
{selectedMediaDurationNote}
</p>
</div> </div>
<div> <div>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'> <label className='mb-1 block text-[11px] font-semibold text-gray-600'>
@ -2919,11 +3034,15 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
<select <select
className='w-full rounded border border-gray-300 px-2 py-1 text-xs' className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
value={selectedElement.transitionVideoUrl || ''} value={selectedElement.transitionVideoUrl || ''}
onChange={(event) => onChange={(event) => {
const nextVideoUrl = event.target.value;
const resolvedDuration =
getKnownDurationForSource(nextVideoUrl);
updateSelectedElement({ updateSelectedElement({
transitionVideoUrl: event.target.value, transitionVideoUrl: nextVideoUrl,
}) transitionDurationSec: resolvedDuration || undefined,
} });
}}
> >
<option value=''>Not selected</option> <option value=''>Not selected</option>
{addFallbackAssetOption( {addFallbackAssetOption(
@ -2937,7 +3056,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
))} ))}
</select> </select>
<p className='mt-1 text-[11px] text-gray-500'> <p className='mt-1 text-[11px] text-gray-500'>
{selectedMediaDurationNote} {selectedTransitionDurationNote}
</p> </p>
</div> </div>
<div> <div>
@ -2995,25 +3114,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
</select> </select>
</div> </div>
)} )}
<div> <p className='text-[11px] text-gray-500'>
<label className='mb-1 block text-[11px] font-semibold text-gray-600'> Transition duration is set automatically from the selected
Transition duration (sec) video. {selectedTransitionDurationNote}
</label> </p>
<input
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
type='number'
min='0.2'
step='0.1'
value={selectedElement.transitionDurationSec ?? 0.7}
onChange={(event) =>
updateSelectedElement({
transitionDurationSec: Number(
event.target.value || 0.7,
),
})
}
/>
</div>
<div className='flex gap-2 pt-1'> <div className='flex gap-2 pt-1'>
<BaseButton <BaseButton
small small
@ -3584,7 +3688,19 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
<button <button
type='button' type='button'
className='absolute top-4 right-4 rounded bg-white/20 px-3 py-1 text-xs text-white hover:bg-white/30' className='absolute top-4 right-4 rounded bg-white/20 px-3 py-1 text-xs text-white hover:bg-white/30'
onClick={() => setTransitionPreview(null)} onClick={() => {
setTransitionPreview(null);
setPendingNavigationPageId((pendingPageId) => {
const nextPageId = String(pendingPageId || '').trim();
if (nextPageId) {
setActivePageId(nextPageId);
setSelectedElementId('');
setSelectedMenuItem('none');
setErrorMessage('');
}
return '';
});
}}
> >
Close Close
</button> </button>