feat(editor): highlight edges and show preview import loader
- Highlight all edges connected to selected scene - Show overlay spinner while uploading/optimizing preview image - macOS: keep control window independent from presentation Made-with: Cursor
This commit is contained in:
@@ -279,7 +279,9 @@ export function openMultiWindow() {
|
||||
presentation.maximize();
|
||||
}
|
||||
if (!windows.has('control')) {
|
||||
createWindow('control', { parent: presentation });
|
||||
// macOS: parent-child window binding moves child with the parent (unlike Windows behavior we want).
|
||||
// Keep control window independent on darwin.
|
||||
createWindow('control', process.platform === 'darwin' ? undefined : { parent: presentation });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,46 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previewBusyOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.previewBusyModal {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.previewBusyText {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.previewSpinner {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: rgba(255, 255, 255, 0.9);
|
||||
animation: previewSpin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes previewSpin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.videoCover {
|
||||
|
||||
@@ -64,6 +64,7 @@ export function EditorApp() {
|
||||
const [settingsMenuOpen, setSettingsMenuOpen] = useState(false);
|
||||
const [renameOpen, setRenameOpen] = useState(false);
|
||||
const [exportModalOpen, setExportModalOpen] = useState(false);
|
||||
const [previewBusy, setPreviewBusy] = useState(false);
|
||||
const [licenseSnap, setLicenseSnap] = useState<LicenseSnapshot | null>(null);
|
||||
const [licenseKeyModalOpen, setLicenseKeyModalOpen] = useState(false);
|
||||
const [eulaModalOpen, setEulaModalOpen] = useState(false);
|
||||
@@ -480,6 +481,7 @@ export function EditorApp() {
|
||||
previewAssetType={sc?.previewAssetType ?? null}
|
||||
previewVideoAutostart={sc?.previewVideoAutostart ?? false}
|
||||
previewRotationDeg={sc?.previewRotationDeg ?? 0}
|
||||
previewBusy={previewBusy}
|
||||
mediaAssets={sceneMediaAssets}
|
||||
audioRefs={sceneAudioRefs}
|
||||
onAudioRefsChange={(next) =>
|
||||
@@ -492,7 +494,18 @@ export function EditorApp() {
|
||||
onDescriptionChange={(description) =>
|
||||
void actions.updateScene(sid, { description })
|
||||
}
|
||||
onImportPreview={() => void actions.importScenePreview(sid)}
|
||||
onImportPreview={() => {
|
||||
setPreviewBusy(true);
|
||||
void (async () => {
|
||||
try {
|
||||
await actions.importScenePreview(sid);
|
||||
} catch (e) {
|
||||
window.alert(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setPreviewBusy(false);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
onClearPreview={() => void actions.clearScenePreview(sid)}
|
||||
onRotatePreview={(previewRotationDeg) =>
|
||||
void actions.updateScene(sid, { previewRotationDeg })
|
||||
@@ -1040,6 +1053,7 @@ type SceneInspectorProps = {
|
||||
previewAssetType: 'image' | 'video' | null;
|
||||
previewVideoAutostart: boolean;
|
||||
previewRotationDeg: 0 | 90 | 180 | 270;
|
||||
previewBusy: boolean;
|
||||
mediaAssets: MediaAsset[];
|
||||
audioRefs: SceneAudioRef[];
|
||||
onAudioRefsChange: (next: SceneAudioRef[]) => void;
|
||||
@@ -1145,6 +1159,7 @@ function SceneInspector({
|
||||
previewAssetType,
|
||||
previewVideoAutostart,
|
||||
previewRotationDeg,
|
||||
previewBusy,
|
||||
mediaAssets,
|
||||
audioRefs,
|
||||
onAudioRefsChange,
|
||||
@@ -1188,6 +1203,14 @@ function SceneInspector({
|
||||
) : (
|
||||
<div className={styles.previewEmpty}>Превью не задано</div>
|
||||
)}
|
||||
{previewBusy ? (
|
||||
<div className={styles.previewBusyOverlay} aria-live="polite">
|
||||
<div className={styles.previewBusyModal}>
|
||||
<div className={styles.previewSpinner} aria-hidden />
|
||||
<div className={styles.previewBusyText}>Загрузка и оптимизация изображения…</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.actionsRow}>
|
||||
<Button variant="primary" onClick={onImportPreview}>
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
}
|
||||
|
||||
.cardActive {
|
||||
border-color: var(--graph-node-active-border);
|
||||
box-shadow: 0 25px 50px -12px rgba(139, 92, 246, 0.1);
|
||||
border-color: rgba(167, 139, 250, 0.95);
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(167, 139, 250, 0.35),
|
||||
0 25px 50px -12px rgba(167, 139, 250, 0.12);
|
||||
}
|
||||
|
||||
.previewShell {
|
||||
|
||||
@@ -365,16 +365,36 @@ function SceneGraphCanvas({
|
||||
}, [currentSceneId, sceneCardById, sceneGraphNodes]);
|
||||
|
||||
const desiredEdges = useMemo<Edge[]>(() => {
|
||||
const selectedGraphNodeIds = new Set<GraphNodeId>();
|
||||
if (currentSceneId) {
|
||||
for (const gn of sceneGraphNodes) {
|
||||
if (gn.sceneId === currentSceneId) selectedGraphNodeIds.add(gn.id);
|
||||
}
|
||||
}
|
||||
const hasSelection = selectedGraphNodeIds.size > 0;
|
||||
return sceneGraphEdges.map((e) => ({
|
||||
...(hasSelection
|
||||
? {
|
||||
style:
|
||||
selectedGraphNodeIds.has(e.sourceGraphNodeId) || selectedGraphNodeIds.has(e.targetGraphNodeId)
|
||||
? { stroke: 'rgba(167,139,250,0.95)', strokeWidth: 3 }
|
||||
: { stroke: 'rgba(255,255,255,0.10)', strokeWidth: 2 },
|
||||
markerEnd:
|
||||
selectedGraphNodeIds.has(e.sourceGraphNodeId) || selectedGraphNodeIds.has(e.targetGraphNodeId)
|
||||
? { type: MarkerType.ArrowClosed, color: 'rgba(167,139,250,0.95)', strokeWidth: 2 }
|
||||
: { type: MarkerType.ArrowClosed, color: 'rgba(255,255,255,0.18)', strokeWidth: 2 },
|
||||
}
|
||||
: {
|
||||
style: { stroke: 'rgba(167,139,250,0.55)', strokeWidth: 2 },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: 'rgba(167,139,250,0.85)', strokeWidth: 2 },
|
||||
}),
|
||||
id: e.id,
|
||||
source: e.sourceGraphNodeId,
|
||||
target: e.targetGraphNodeId,
|
||||
type: 'smoothstep',
|
||||
animated: false,
|
||||
style: { stroke: 'rgba(167,139,250,0.55)', strokeWidth: 2 },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: 'rgba(167,139,250,0.85)', strokeWidth: 2 },
|
||||
}));
|
||||
}, [sceneGraphEdges]);
|
||||
}, [currentSceneId, sceneGraphEdges, sceneGraphNodes]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node<SceneCardData>>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||
|
||||
Reference in New Issue
Block a user