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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user