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:
Ivan Fontosh
2026-08-04 10:15:28 +08:00
parent 4e6f7321b8
commit cbb6edc378
9 changed files with 148 additions and 59 deletions
+50 -15
View File
@@ -8,6 +8,7 @@ import { installStdoutEpipeGuards } from './safeConsole';
installStdoutEpipeGuards();
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 {
PROJECT_ZIP_OPEN_DIALOG_FILTER,
@@ -402,8 +403,10 @@ async function main() {
const licenseService = new LicenseService(app.getPath('userData'));
tokensStore = new TokensStore(app.getPath('userData'));
await tokensStore.ensureLoaded();
playersStore = new PlayersStore(app.getPath('userData'));
await playersStore.ensureLoaded();
if (USERS_BRANCH_FEATURES_ENABLED) {
playersStore = new PlayersStore(app.getPath('userData'));
await playersStore.ensureLoaded();
}
setLicenseAssert(() => {
licenseService.assertForIpc();
});
@@ -429,9 +432,11 @@ async function main() {
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
if (playerIds.length > 0) {
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
if (USERS_BRANCH_FEATURES_ENABLED) {
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
if (playerIds.length > 0) {
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
}
}
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
openMultiWindow();
@@ -1357,13 +1362,19 @@ async function main() {
});
registerHandler(ipcChannels.players.list, async () => {
await playersStore!.ensureLoaded();
return { players: playersStore!.listPlayers(), teams: playersStore!.listTeams() };
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { players: [], teams: [] };
}
await playersStore.ensureLoaded();
return { players: playersStore.listPlayers(), teams: playersStore.listTeams() };
});
registerHandler(
ipcChannels.players.upsert,
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,
...(id !== undefined ? { id } : {}),
@@ -1380,17 +1391,26 @@ async function main() {
},
);
registerHandler(ipcChannels.players.delete, async ({ id }) => {
await playersStore!.delete(id);
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { ok: true };
}
await playersStore.delete(id);
emitPlayersState();
return { ok: true };
});
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();
return { players };
});
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,
...(id !== undefined ? { id } : {}),
...(color !== undefined ? { color } : {}),
@@ -1399,21 +1419,33 @@ async function main() {
return { team };
});
registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => {
await playersStore!.deleteTeam(id);
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { ok: true };
}
await playersStore.deleteTeam(id);
emitPlayersState();
return { ok: true };
});
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();
return { teams };
});
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();
return { player };
});
registerHandler(ipcChannels.players.pickImage, async () => {
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { canceled: true as const };
}
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
@@ -1432,7 +1464,10 @@ async function main() {
return { canceled: false as const, filePath, previewDataUrl };
});
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, () => {
return { state: sceneNpcTokensSessionStore.getState() };
+2 -2
View File
@@ -81,7 +81,7 @@ async function serveFile(info: ReadInfo, request: Request): Promise<Response> {
export function registerDndAssetProtocol(
projectStore: ZipProjectStore,
tokensStore: TokensStore,
playersStore: PlayersStore,
playersStore: PlayersStore | null,
): void {
session.defaultSession.protocol.handle('dnd', async (request) => {
const url = new URL(request.url);
@@ -95,7 +95,7 @@ export function registerDndAssetProtocol(
} else if (url.hostname === 'token') {
info = tokensStore.getImageReadInfo(asTokenId(id));
} else if (url.hostname === 'player') {
info = playersStore.getImageReadInfo(asPlayerId(id));
info = playersStore?.getImageReadInfo(asPlayerId(id)) ?? null;
}
if (!info) {
return new Response(null, { status: 404 });