feat(project): optimize image imports and converter

- Optimize imported scene preview images (smart WebP/JPEG/PNG, preserve alpha, keep pixel size)

- Update converter to re-encode existing image assets with same algorithm

- Improve import/export progress overlay and reduce presentation slide stutter

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-23 17:59:57 +08:00
parent 1d051f8bf9
commit 8f8eef53c9
33 changed files with 3684 additions and 68 deletions
+41 -6
View File
@@ -23,6 +23,7 @@ type SceneCard = {
title: string;
active: boolean;
previewAssetId: AssetId | null;
previewThumbAssetId: AssetId | null;
previewAssetType: 'image' | 'video' | null;
previewVideoAutostart: boolean;
previewRotationDeg: 0 | 90 | 180 | 270;
@@ -92,6 +93,7 @@ export function EditorApp() {
title: s.title,
active: s.id === state.selectedSceneId,
previewAssetId: s.previewAssetId,
previewThumbAssetId: s.previewThumbAssetId,
previewAssetType: s.previewAssetType,
previewVideoAutostart: s.previewVideoAutostart,
previewRotationDeg: s.previewRotationDeg,
@@ -263,6 +265,28 @@ export function EditorApp() {
return (
<>
{state.zipProgress
? createPortal(
<div className={styles.progressOverlay} role="dialog" aria-label="Прогресс операции">
<div className={styles.progressModal}>
<div className={styles.progressTitle}>
{state.zipProgress.kind === 'import' ? 'Импорт проекта' : 'Экспорт проекта'}
</div>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${String(Math.max(0, Math.min(100, state.zipProgress.percent)))}%` }}
/>
</div>
<div className={styles.progressMeta}>
<div>{state.zipProgress.detail ?? state.zipProgress.stage}</div>
<div>{state.zipProgress.percent}%</div>
</div>
</div>
</div>,
document.body,
)
: null}
<LayoutShell
bodyOverlay={bodyOverlay}
topBar={
@@ -1276,7 +1300,8 @@ type SceneListCardProps = {
};
function SceneListCard({ scene, onSelect, onDeleteScene }: SceneListCardProps) {
const url = useAssetUrl(scene.previewAssetId);
const thumbUrl = useAssetUrl(scene.previewThumbAssetId);
const previewUrl = useAssetUrl(scene.previewAssetId);
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
useEffect(() => {
@@ -1315,21 +1340,31 @@ function SceneListCard({ scene, onSelect, onDeleteScene }: SceneListCardProps) {
if (e.key === 'Enter' || e.key === ' ') onSelect();
}}
>
<div className={url ? styles.sceneThumb : styles.sceneThumbEmpty}>
{url && scene.previewAssetType === 'image' ? (
<div className={thumbUrl || previewUrl ? styles.sceneThumb : styles.sceneThumbEmpty}>
{thumbUrl ? (
<div className={styles.sceneThumbInner}>
<RotatedImage
url={url}
url={thumbUrl}
rotationDeg={scene.previewRotationDeg}
mode="cover"
loading="lazy"
decoding="async"
/>
</div>
) : url && scene.previewAssetType === 'video' ? (
) : previewUrl && scene.previewAssetType === 'image' ? (
<div className={styles.sceneThumbInner}>
<RotatedImage
url={previewUrl}
rotationDeg={scene.previewRotationDeg}
mode="cover"
loading="lazy"
decoding="async"
/>
</div>
) : previewUrl && scene.previewAssetType === 'video' ? (
<div className={styles.sceneThumbInner}>
<video
src={url}
src={previewUrl}
muted
playsInline
preload="metadata"