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:
@@ -10,6 +10,7 @@ type State = {
|
||||
projects: ProjectSummary[];
|
||||
project: Project | null;
|
||||
selectedSceneId: SceneId | null;
|
||||
zipProgress: { kind: 'import' | 'export'; percent: number; stage: string; detail?: string } | null;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
@@ -27,6 +28,7 @@ type Actions = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
previewAssetId?: AssetId | null;
|
||||
previewThumbAssetId?: AssetId | null;
|
||||
previewAssetType?: 'image' | 'video' | null;
|
||||
previewVideoAutostart?: boolean;
|
||||
previewRotationDeg?: 0 | 90 | 180 | 270;
|
||||
@@ -58,7 +60,12 @@ function randomId(prefix: string): string {
|
||||
|
||||
export function useProjectState(licenseActive: boolean): readonly [State, Actions] {
|
||||
const api = getDndApi();
|
||||
const [state, setState] = useState<State>({ projects: [], project: null, selectedSceneId: null });
|
||||
const [state, setState] = useState<State>({
|
||||
projects: [],
|
||||
project: null,
|
||||
selectedSceneId: null,
|
||||
zipProgress: null,
|
||||
});
|
||||
const projectRef = useRef<Project | null>(null);
|
||||
/** Bumps on mutations / refresh; initial license load only applies if still current (avoids racing late list/get over newer state). */
|
||||
const projectDataEpochRef = useRef(0);
|
||||
@@ -66,6 +73,43 @@ export function useProjectState(licenseActive: boolean): readonly [State, Action
|
||||
projectRef.current = state.project;
|
||||
}, [state.project]);
|
||||
|
||||
useEffect(() => {
|
||||
const offImport = api.on(ipcChannels.project.importZipProgress, (evt) => {
|
||||
const e = evt as unknown as { percent: number; stage: string; detail?: string };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
zipProgress: {
|
||||
kind: 'import',
|
||||
percent: e.percent,
|
||||
stage: e.stage,
|
||||
...(e.detail ? { detail: e.detail } : null),
|
||||
},
|
||||
}));
|
||||
if (e.stage === 'done' || e.percent >= 100) {
|
||||
setTimeout(() => setState((s) => ({ ...s, zipProgress: null })), 450);
|
||||
}
|
||||
});
|
||||
const offExport = api.on(ipcChannels.project.exportZipProgress, (evt) => {
|
||||
const e = evt as unknown as { percent: number; stage: string; detail?: string };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
zipProgress: {
|
||||
kind: 'export',
|
||||
percent: e.percent,
|
||||
stage: e.stage,
|
||||
...(e.detail ? { detail: e.detail } : null),
|
||||
},
|
||||
}));
|
||||
if (e.stage === 'done' || e.percent >= 100) {
|
||||
setTimeout(() => setState((s) => ({ ...s, zipProgress: null })), 450);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
offImport();
|
||||
offExport();
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
const actions = useMemo<Actions>(() => {
|
||||
const refreshProjects = async () => {
|
||||
projectDataEpochRef.current += 1;
|
||||
@@ -99,6 +143,7 @@ export function useProjectState(licenseActive: boolean): readonly [State, Action
|
||||
title: `Новая сцена`,
|
||||
description: '',
|
||||
previewAssetId: null,
|
||||
previewThumbAssetId: null,
|
||||
previewAssetType: null,
|
||||
previewVideoAutostart: false,
|
||||
previewRotationDeg: 0,
|
||||
@@ -153,6 +198,7 @@ export function useProjectState(licenseActive: boolean): readonly [State, Action
|
||||
title?: string;
|
||||
description?: string;
|
||||
previewAssetId?: AssetId | null;
|
||||
previewThumbAssetId?: AssetId | null;
|
||||
previewAssetType?: 'image' | 'video' | null;
|
||||
previewVideoAutostart?: boolean;
|
||||
previewRotationDeg?: 0 | 90 | 180 | 270;
|
||||
@@ -171,6 +217,9 @@ export function useProjectState(licenseActive: boolean): readonly [State, Action
|
||||
...(patch.title !== undefined ? { title: patch.title } : null),
|
||||
...(patch.description !== undefined ? { description: patch.description } : null),
|
||||
...(patch.previewAssetId !== undefined ? { previewAssetId: patch.previewAssetId } : null),
|
||||
...(patch.previewThumbAssetId !== undefined
|
||||
? { previewThumbAssetId: patch.previewThumbAssetId }
|
||||
: null),
|
||||
...(patch.previewAssetType !== undefined ? { previewAssetType: patch.previewAssetType } : null),
|
||||
...(patch.previewVideoAutostart !== undefined
|
||||
? { previewVideoAutostart: patch.previewVideoAutostart }
|
||||
@@ -342,7 +391,7 @@ export function useProjectState(licenseActive: boolean): readonly [State, Action
|
||||
if (!licenseActive) {
|
||||
queueMicrotask(() => {
|
||||
projectDataEpochRef.current += 1;
|
||||
setState({ projects: [], project: null, selectedSceneId: null });
|
||||
setState({ projects: [], project: null, selectedSceneId: null, zipProgress: null });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user