feat(editor): DnD сцен в список, порядок списка и правки импорта

Можно перетащить изображения/видео в колонку сцен: создаются новые сцены
с превью и названием из файла, с полноэкранным прогрессом и отчётом о
пропущенных файлах.

Добавлен ручной порядок сцен в списке (sceneListOrder) с сохранением в
проект; перетаскивание карточек отделено от импорта файлов. Во время
сортировки снова работает скролл колесом.

Исправлен залипающий disabled у «+ Новая сцена» после создания сцены.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-12 10:32:50 +08:00
parent b7e6ff6915
commit 35a6e979eb
18 changed files with 789 additions and 73 deletions
+39
View File
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
isPreviewMediaPath,
partitionSceneMediaDrops,
sceneTitleFromMediaPath,
} from './fileDrop';
void test('sceneTitleFromMediaPath strips extension and path', () => {
assert.equal(sceneTitleFromMediaPath('C:\\media\\Forest Gate.png'), 'Forest Gate');
assert.equal(sceneTitleFromMediaPath('/tmp/battle.mp4'), 'battle');
assert.equal(sceneTitleFromMediaPath('onlyname'), 'onlyname');
});
void test('isPreviewMediaPath accepts images and videos', () => {
assert.equal(isPreviewMediaPath('a.png'), true);
assert.equal(isPreviewMediaPath('a.JPG'), true);
assert.equal(isPreviewMediaPath('a.webm'), true);
assert.equal(isPreviewMediaPath('a.mp3'), false);
assert.equal(isPreviewMediaPath('a.pdf'), false);
});
void test('partitionSceneMediaDrops keeps valid and rejects others', () => {
const { accepted, rejected } = partitionSceneMediaDrops([
{ path: 'D:\\a\\one.jpg', name: 'one.jpg' },
{ path: 'D:\\a\\two.mp3', name: 'two.mp3' },
{ path: '', name: 'ghost.png' },
{ path: 'D:\\a\\three.mov', name: 'three.mov' },
]);
assert.deepEqual(
accepted.map((x) => x.name),
['one.jpg', 'three.mov'],
);
assert.deepEqual(rejected, [
{ name: 'two.mp3', reason: 'unsupported' },
{ name: 'ghost.png', reason: 'no_path' },
]);
});