chore(release): gate users-branch features for hotfix
Add USERS_BRANCH_FEATURES_ENABLED (off by default) to hide Players library, session tokens, and related UI while keeping the preview import fix. Restore primary Run button when the flag is off. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+45
-10
@@ -8,6 +8,7 @@ import { installStdoutEpipeGuards } from './safeConsole';
|
|||||||
installStdoutEpipeGuards();
|
installStdoutEpipeGuards();
|
||||||
|
|
||||||
import { openDialogFilterLabel } from '../shared/appBranding';
|
import { openDialogFilterLabel } from '../shared/appBranding';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../shared/features/usersBranchFeatures';
|
||||||
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
|
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
|
||||||
import {
|
import {
|
||||||
PROJECT_ZIP_OPEN_DIALOG_FILTER,
|
PROJECT_ZIP_OPEN_DIALOG_FILTER,
|
||||||
@@ -402,8 +403,10 @@ async function main() {
|
|||||||
const licenseService = new LicenseService(app.getPath('userData'));
|
const licenseService = new LicenseService(app.getPath('userData'));
|
||||||
tokensStore = new TokensStore(app.getPath('userData'));
|
tokensStore = new TokensStore(app.getPath('userData'));
|
||||||
await tokensStore.ensureLoaded();
|
await tokensStore.ensureLoaded();
|
||||||
|
if (USERS_BRANCH_FEATURES_ENABLED) {
|
||||||
playersStore = new PlayersStore(app.getPath('userData'));
|
playersStore = new PlayersStore(app.getPath('userData'));
|
||||||
await playersStore.ensureLoaded();
|
await playersStore.ensureLoaded();
|
||||||
|
}
|
||||||
setLicenseAssert(() => {
|
setLicenseAssert(() => {
|
||||||
licenseService.assertForIpc();
|
licenseService.assertForIpc();
|
||||||
});
|
});
|
||||||
@@ -429,10 +432,12 @@ async function main() {
|
|||||||
sceneTokensSessionStore.reset();
|
sceneTokensSessionStore.reset();
|
||||||
sceneNpcTokensSessionStore.reset();
|
sceneNpcTokensSessionStore.reset();
|
||||||
scenePlayerTokensSessionStore.reset();
|
scenePlayerTokensSessionStore.reset();
|
||||||
|
if (USERS_BRANCH_FEATURES_ENABLED) {
|
||||||
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
|
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
|
||||||
if (playerIds.length > 0) {
|
if (playerIds.length > 0) {
|
||||||
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
|
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
|
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
|
||||||
openMultiWindow();
|
openMultiWindow();
|
||||||
const project = projectStore.getOpenProject();
|
const project = projectStore.getOpenProject();
|
||||||
@@ -1357,13 +1362,19 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerHandler(ipcChannels.players.list, async () => {
|
registerHandler(ipcChannels.players.list, async () => {
|
||||||
await playersStore!.ensureLoaded();
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
return { players: playersStore!.listPlayers(), teams: playersStore!.listTeams() };
|
return { players: [], teams: [] };
|
||||||
|
}
|
||||||
|
await playersStore.ensureLoaded();
|
||||||
|
return { players: playersStore.listPlayers(), teams: playersStore.listTeams() };
|
||||||
});
|
});
|
||||||
registerHandler(
|
registerHandler(
|
||||||
ipcChannels.players.upsert,
|
ipcChannels.players.upsert,
|
||||||
async ({ id, name, filePath, teamId, ringColor, imageOffset, imageScale }) => {
|
async ({ id, name, filePath, teamId, ringColor, imageOffset, imageScale }) => {
|
||||||
const player = await playersStore!.upsert(
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
throw new Error('Players library is disabled in this build');
|
||||||
|
}
|
||||||
|
const player = await playersStore.upsert(
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
...(id !== undefined ? { id } : {}),
|
...(id !== undefined ? { id } : {}),
|
||||||
@@ -1380,17 +1391,26 @@ async function main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
registerHandler(ipcChannels.players.delete, async ({ id }) => {
|
registerHandler(ipcChannels.players.delete, async ({ id }) => {
|
||||||
await playersStore!.delete(id);
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
await playersStore.delete(id);
|
||||||
emitPlayersState();
|
emitPlayersState();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.setOrder, async ({ playerIds }) => {
|
registerHandler(ipcChannels.players.setOrder, async ({ playerIds }) => {
|
||||||
const players = await playersStore!.setPlayersOrder(playerIds);
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { players: [] };
|
||||||
|
}
|
||||||
|
const players = await playersStore.setPlayersOrder(playerIds);
|
||||||
emitPlayersState();
|
emitPlayersState();
|
||||||
return { players };
|
return { players };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.upsertTeam, async ({ id, name, color }) => {
|
registerHandler(ipcChannels.players.upsertTeam, async ({ id, name, color }) => {
|
||||||
const team = await playersStore!.upsertTeam({
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
throw new Error('Players library is disabled in this build');
|
||||||
|
}
|
||||||
|
const team = await playersStore.upsertTeam({
|
||||||
name,
|
name,
|
||||||
...(id !== undefined ? { id } : {}),
|
...(id !== undefined ? { id } : {}),
|
||||||
...(color !== undefined ? { color } : {}),
|
...(color !== undefined ? { color } : {}),
|
||||||
@@ -1399,21 +1419,33 @@ async function main() {
|
|||||||
return { team };
|
return { team };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => {
|
registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => {
|
||||||
await playersStore!.deleteTeam(id);
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
await playersStore.deleteTeam(id);
|
||||||
emitPlayersState();
|
emitPlayersState();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.setTeamsOrder, async ({ teamIds }) => {
|
registerHandler(ipcChannels.players.setTeamsOrder, async ({ teamIds }) => {
|
||||||
const teams = await playersStore!.setTeamsOrder(teamIds);
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { teams: [] };
|
||||||
|
}
|
||||||
|
const teams = await playersStore.setTeamsOrder(teamIds);
|
||||||
emitPlayersState();
|
emitPlayersState();
|
||||||
return { teams };
|
return { teams };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.assignTeam, async ({ playerId, teamId }) => {
|
registerHandler(ipcChannels.players.assignTeam, async ({ playerId, teamId }) => {
|
||||||
const player = await playersStore!.assignPlayerTeam(playerId, teamId);
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
throw new Error('Players library is disabled in this build');
|
||||||
|
}
|
||||||
|
const player = await playersStore.assignPlayerTeam(playerId, teamId);
|
||||||
emitPlayersState();
|
emitPlayersState();
|
||||||
return { player };
|
return { player };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.pickImage, async () => {
|
registerHandler(ipcChannels.players.pickImage, async () => {
|
||||||
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { canceled: true as const };
|
||||||
|
}
|
||||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
@@ -1432,7 +1464,10 @@ async function main() {
|
|||||||
return { canceled: false as const, filePath, previewDataUrl };
|
return { canceled: false as const, filePath, previewDataUrl };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.players.imageUrl, ({ id }) => {
|
registerHandler(ipcChannels.players.imageUrl, ({ id }) => {
|
||||||
return { url: playersStore!.getImageUrl(id) };
|
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
|
||||||
|
return { url: null };
|
||||||
|
}
|
||||||
|
return { url: playersStore.getImageUrl(id) };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.sceneNpcTokensSession.getState, () => {
|
registerHandler(ipcChannels.sceneNpcTokensSession.getState, () => {
|
||||||
return { state: sceneNpcTokensSessionStore.getState() };
|
return { state: sceneNpcTokensSessionStore.getState() };
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ async function serveFile(info: ReadInfo, request: Request): Promise<Response> {
|
|||||||
export function registerDndAssetProtocol(
|
export function registerDndAssetProtocol(
|
||||||
projectStore: ZipProjectStore,
|
projectStore: ZipProjectStore,
|
||||||
tokensStore: TokensStore,
|
tokensStore: TokensStore,
|
||||||
playersStore: PlayersStore,
|
playersStore: PlayersStore | null,
|
||||||
): void {
|
): void {
|
||||||
session.defaultSession.protocol.handle('dnd', async (request) => {
|
session.defaultSession.protocol.handle('dnd', async (request) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
@@ -95,7 +95,7 @@ export function registerDndAssetProtocol(
|
|||||||
} else if (url.hostname === 'token') {
|
} else if (url.hostname === 'token') {
|
||||||
info = tokensStore.getImageReadInfo(asTokenId(id));
|
info = tokensStore.getImageReadInfo(asTokenId(id));
|
||||||
} else if (url.hostname === 'player') {
|
} else if (url.hostname === 'player') {
|
||||||
info = playersStore.getImageReadInfo(asPlayerId(id));
|
info = playersStore?.getImageReadInfo(asPlayerId(id)) ?? null;
|
||||||
}
|
}
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
|
|||||||
import { fitAspectRect } from '../../shared/geometry/fitAspectRect';
|
import { fitAspectRect } from '../../shared/geometry/fitAspectRect';
|
||||||
import { ipcChannels } from '../../shared/ipc/contracts';
|
import { ipcChannels } from '../../shared/ipc/contracts';
|
||||||
import type { SessionState } from '../../shared/ipc/contracts';
|
import type { SessionState } from '../../shared/ipc/contracts';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
|
||||||
import {
|
import {
|
||||||
isNodeInMainStoryline,
|
isNodeInMainStoryline,
|
||||||
isNodeInSideStoryline,
|
isNodeInSideStoryline,
|
||||||
@@ -1778,6 +1779,8 @@ export function ControlApp() {
|
|||||||
<div className={styles.previewHeader}>
|
<div className={styles.previewHeader}>
|
||||||
<div className={styles.previewTitle}>{t('control.screenPreview')}</div>
|
<div className={styles.previewTitle}>{t('control.screenPreview')}</div>
|
||||||
<div className={styles.previewActions}>
|
<div className={styles.previewActions}>
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
|
<>
|
||||||
<label className={styles.npcTokenScale}>
|
<label className={styles.npcTokenScale}>
|
||||||
<span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span>
|
<span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span>
|
||||||
<input
|
<input
|
||||||
@@ -1816,6 +1819,8 @@ export function ControlApp() {
|
|||||||
? t('control.hidePlayers')
|
? t('control.hidePlayers')
|
||||||
: t('control.showPlayers')}
|
: t('control.showPlayers')}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}>
|
<Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}>
|
||||||
{t('control.stopPresentation')}
|
{t('control.stopPresentation')}
|
||||||
@@ -2026,7 +2031,7 @@ export function ControlApp() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{previewContentRect ? (
|
{USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
|
||||||
<SceneNpcTokensOverlay
|
<SceneNpcTokensOverlay
|
||||||
placements={currentScene?.npcTokens ?? []}
|
placements={currentScene?.npcTokens ?? []}
|
||||||
library={session?.project?.npcs ?? []}
|
library={session?.project?.npcs ?? []}
|
||||||
@@ -2046,7 +2051,7 @@ export function ControlApp() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{previewContentRect ? (
|
{USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
|
||||||
<ScenePlayerTokensOverlay
|
<ScenePlayerTokensOverlay
|
||||||
library={appPlayers}
|
library={appPlayers}
|
||||||
session={scenePlayerTokensSession}
|
session={scenePlayerTokensSession}
|
||||||
@@ -2509,7 +2514,7 @@ export function ControlApp() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{npcSessionCtxMenu
|
{USERS_BRANCH_FEATURES_ENABLED && npcSessionCtxMenu
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { startTransition, useCallback, useEffect, useMemo, useRef, useSta
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
|
||||||
import type {
|
import type {
|
||||||
NpcImportResolution,
|
NpcImportResolution,
|
||||||
SceneImportResolution,
|
SceneImportResolution,
|
||||||
@@ -841,6 +842,7 @@ export function EditorApp() {
|
|||||||
{t('top.file')}
|
{t('top.file')}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="players-header-btn"
|
data-testid="players-header-btn"
|
||||||
@@ -858,6 +860,7 @@ export function EditorApp() {
|
|||||||
>
|
>
|
||||||
{t('top.players')}
|
{t('top.players')}
|
||||||
</button>
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.flex1} />
|
<div className={styles.flex1} />
|
||||||
{appVersionText ? (
|
{appVersionText ? (
|
||||||
@@ -868,6 +871,7 @@ export function EditorApp() {
|
|||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
{state.project ? (
|
{state.project ? (
|
||||||
<>
|
<>
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
<div
|
<div
|
||||||
ref={runSplitRef}
|
ref={runSplitRef}
|
||||||
className={styles.splitRun}
|
className={styles.splitRun}
|
||||||
@@ -917,6 +921,19 @@ export function EditorApp() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled={runDisabled || launching}
|
||||||
|
data-testid="run-main-btn"
|
||||||
|
onClick={() => {
|
||||||
|
if (!licenseActive || !graphStartGraphNodeId || launching) return;
|
||||||
|
launchFromGraphNode(graphStartGraphNodeId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('top.run')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{runDisabled ? (
|
{runDisabled ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1620,6 +1637,8 @@ export function EditorApp() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CheckUpdatesModal open={checkUpdatesOpen} onClose={() => setCheckUpdatesOpen(false)} />
|
<CheckUpdatesModal open={checkUpdatesOpen} onClose={() => setCheckUpdatesOpen(false)} />
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
|
<>
|
||||||
<PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} />
|
<PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} />
|
||||||
<LaunchPlayersModal
|
<LaunchPlayersModal
|
||||||
open={launchPlayersOpen}
|
open={launchPlayersOpen}
|
||||||
@@ -1629,7 +1648,9 @@ export function EditorApp() {
|
|||||||
launchFromGraphNode(graphStartGraphNodeId, playerIds);
|
launchFromGraphNode(graphStartGraphNodeId, playerIds);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{runMenuOpen && runMenuPos
|
</>
|
||||||
|
) : null}
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED && runMenuOpen && runMenuPos
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<div
|
<div
|
||||||
data-runmenu-root="1"
|
data-runmenu-root="1"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/** Порядок разделов в окне «Инструкция». */
|
/** Порядок разделов в окне «Инструкция». */
|
||||||
export const HELP_SECTION_IDS = [
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../../shared/features/usersBranchFeatures';
|
||||||
|
|
||||||
|
const HELP_SECTION_IDS_ALL = [
|
||||||
'overview',
|
'overview',
|
||||||
'license',
|
'license',
|
||||||
'projects',
|
'projects',
|
||||||
@@ -25,7 +27,11 @@ export const HELP_SECTION_IDS = [
|
|||||||
'settings',
|
'settings',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type HelpSectionId = (typeof HELP_SECTION_IDS)[number];
|
export type HelpSectionId = (typeof HELP_SECTION_IDS_ALL)[number];
|
||||||
|
|
||||||
|
export const HELP_SECTION_IDS: readonly HelpSectionId[] = USERS_BRANCH_FEATURES_ENABLED
|
||||||
|
? HELP_SECTION_IDS_ALL
|
||||||
|
: HELP_SECTION_IDS_ALL.filter((id) => id !== 'players');
|
||||||
|
|
||||||
export function helpSectionTitleKey(id: HelpSectionId): string {
|
export function helpSectionTitleKey(id: HelpSectionId): string {
|
||||||
return `help.section.${id}.title`;
|
return `help.section.${id}.title`;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
import editorStyles from '../editor/EditorApp.module.css';
|
import editorStyles from '../editor/EditorApp.module.css';
|
||||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||||
import { getDndApi } from '../shared/dndApi';
|
import { getDndApi } from '../shared/dndApi';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
|
||||||
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
|
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
|
||||||
import { Button, Input, Select } from '../shared/ui/controls';
|
import { Button, Input, Select } from '../shared/ui/controls';
|
||||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||||
@@ -737,6 +738,7 @@ export function NpcsEditorApp() {
|
|||||||
<div>
|
<div>
|
||||||
<div className={styles.fieldLabel}>{t('npcs.avatar')}</div>
|
<div className={styles.fieldLabel}>{t('npcs.avatar')}</div>
|
||||||
<div className={styles.avatarPick}>
|
<div className={styles.avatarPick}>
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
<PlayerTokenView
|
<PlayerTokenView
|
||||||
name={selected.name}
|
name={selected.name}
|
||||||
imageUrl={selectedUrl}
|
imageUrl={selectedUrl}
|
||||||
@@ -758,6 +760,13 @@ export function NpcsEditorApp() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.avatarPreview}>
|
||||||
|
{selectedUrl ? (
|
||||||
|
<img className={styles.avatarPreviewImg} src={selectedUrl} alt="" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={avatarBusy}
|
disabled={avatarBusy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -782,6 +791,7 @@ export function NpcsEditorApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
<label>
|
<label>
|
||||||
<div className={styles.fieldLabel}>{t('npcs.disposition')}</div>
|
<div className={styles.fieldLabel}>{t('npcs.disposition')}</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -800,6 +810,7 @@ export function NpcsEditorApp() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.fieldLabel}>{t('npcs.name')}</div>
|
<div className={styles.fieldLabel}>{t('npcs.name')}</div>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
trapTypeLabelRu,
|
trapTypeLabelRu,
|
||||||
} from '../../shared/types/sceneTraps';
|
} from '../../shared/types/sceneTraps';
|
||||||
import { sceneViewPanBy, sceneViewZoomAt } from '../../shared/types/sceneView';
|
import { sceneViewPanBy, sceneViewZoomAt } from '../../shared/types/sceneView';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
|
||||||
import editorStyles from '../editor/EditorApp.module.css';
|
import editorStyles from '../editor/EditorApp.module.css';
|
||||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||||
import { getDndApi } from '../shared/dndApi';
|
import { getDndApi } from '../shared/dndApi';
|
||||||
@@ -484,7 +485,7 @@ export function SceneEditorApp() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const npcId = e.dataTransfer.getData(SCENE_NPC_DND_MIME);
|
const npcId = e.dataTransfer.getData(SCENE_NPC_DND_MIME);
|
||||||
if (npcId) {
|
if (USERS_BRANCH_FEATURES_ENABLED && npcId) {
|
||||||
addNpcTokenAt(npcId as NpcId, p.x, p.y);
|
addNpcTokenAt(npcId as NpcId, p.x, p.y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -600,6 +601,7 @@ export function SceneEditorApp() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{USERS_BRANCH_FEATURES_ENABLED ? (
|
||||||
<div className={styles.accordion} data-testid="scene-npc-accordion">
|
<div className={styles.accordion} data-testid="scene-npc-accordion">
|
||||||
<button type="button" className={styles.accordionHead} onClick={() => setNpcsOpen((v) => !v)}>
|
<button type="button" className={styles.accordionHead} onClick={() => setNpcsOpen((v) => !v)}>
|
||||||
НПС {npcsOpen ? '▾' : '▸'}
|
НПС {npcsOpen ? '▾' : '▸'}
|
||||||
@@ -621,6 +623,7 @@ export function SceneEditorApp() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className={styles.accordion}>
|
<div className={styles.accordion}>
|
||||||
<button type="button" className={styles.accordionHead} onClick={() => setTokensOpen((v) => !v)}>
|
<button type="button" className={styles.accordionHead} onClick={() => setTokensOpen((v) => !v)}>
|
||||||
@@ -884,7 +887,7 @@ export function SceneEditorApp() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
{contentRect
|
{USERS_BRANCH_FEATURES_ENABLED && contentRect
|
||||||
? localNpcTokens.map((tok) => {
|
? localNpcTokens.map((tok) => {
|
||||||
const npc = project?.npcs.find((item) => item.id === tok.npcId);
|
const npc = project?.npcs.find((item) => item.id === tok.npcId);
|
||||||
if (!npc) return null;
|
if (!npc) return null;
|
||||||
@@ -1050,7 +1053,7 @@ export function SceneEditorApp() {
|
|||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
{npcCtxMenu
|
{USERS_BRANCH_FEATURES_ENABLED && npcCtxMenu
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { computeTimeSec } from '../../main/video/videoPlaybackStore';
|
import { computeTimeSec } from '../../main/video/videoPlaybackStore';
|
||||||
import type { SessionState } from '../../shared/ipc/contracts';
|
import type { SessionState } from '../../shared/ipc/contracts';
|
||||||
|
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
|
||||||
|
|
||||||
import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay';
|
import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay';
|
||||||
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
|
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
|
||||||
@@ -54,7 +55,8 @@ export function PresentationView({
|
|||||||
const [materialsOverlay] = useMaterialsOverlayState();
|
const [materialsOverlay] = useMaterialsOverlayState();
|
||||||
const [npcsOverlay] = useNpcsOverlayState();
|
const [npcsOverlay] = useNpcsOverlayState();
|
||||||
const appTokens = useAppTokens();
|
const appTokens = useAppTokens();
|
||||||
const { players: appPlayers } = useAppPlayers();
|
const appPlayersResult = useAppPlayers();
|
||||||
|
const appPlayers = USERS_BRANCH_FEATURES_ENABLED ? appPlayersResult.players : [];
|
||||||
const [sceneTokensSession] = useSceneTokensSession();
|
const [sceneTokensSession] = useSceneTokensSession();
|
||||||
const [sceneNpcTokensSession] = useSceneNpcTokensSession();
|
const [sceneNpcTokensSession] = useSceneNpcTokensSession();
|
||||||
const [scenePlayerTokensSession] = useScenePlayerTokensSession();
|
const [scenePlayerTokensSession] = useScenePlayerTokensSession();
|
||||||
@@ -196,7 +198,7 @@ export function PresentationView({
|
|||||||
viewport={contentRect}
|
viewport={contentRect}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{scene?.previewAssetType === 'image' && contentRect ? (
|
{USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
|
||||||
<SceneNpcTokensOverlay
|
<SceneNpcTokensOverlay
|
||||||
placements={scene.npcTokens ?? []}
|
placements={scene.npcTokens ?? []}
|
||||||
library={project?.npcs ?? []}
|
library={project?.npcs ?? []}
|
||||||
@@ -205,7 +207,7 @@ export function PresentationView({
|
|||||||
grid={scene.grid}
|
grid={scene.grid}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{scene?.previewAssetType === 'image' && contentRect ? (
|
{USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
|
||||||
<ScenePlayerTokensOverlay
|
<ScenePlayerTokensOverlay
|
||||||
library={appPlayers}
|
library={appPlayers}
|
||||||
session={scenePlayerTokensSession}
|
session={scenePlayerTokensSession}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Hotfix release: set to `false` to hide everything merged from branch `users`
|
||||||
|
* (Players library, run-with-players, circular NPC/player session tokens, disposition UI).
|
||||||
|
* Re-enable after QA.
|
||||||
|
*/
|
||||||
|
export const USERS_BRANCH_FEATURES_ENABLED = false;
|
||||||
Reference in New Issue
Block a user