feat(phase1): rebrand to TTRPG Player and drop Git updates feed

Rename product to TTRPG Player (TTRPGPlayer / com.ttrpgplayer.app), use .ttrpg.zip for new saves while keeping .dnd.zip import, accept TTRPG- and DND- license keys on client, and remove sync-update-feed plus CI push to DndGamePlayerUpdates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-05-17 20:56:14 +08:00
parent 2c03921d23
commit 7c858ba633
27 changed files with 253 additions and 1328 deletions
+22 -22
View File
@@ -8,6 +8,11 @@ import { ZipFile } from 'yazl';
import { isSceneGraphEdgeRejected } from '../../shared/graph/sceneGraphEdgeRules';
import type { ScenePatch } from '../../shared/ipc/contracts';
import {
isProjectZipFileName,
projectZipFileNameFromBase,
stripProjectZipExtension,
} from '../../shared/project/projectZipExtension';
import type {
MediaAsset,
MediaAssetType,
@@ -24,7 +29,7 @@ import { asAssetId, asGraphNodeId, asProjectId } from '../../shared/types/ids';
import { getAppSemanticVersion } from '../versionInfo';
import { reconcileAssetFiles } from './assetPrune';
import { recoverOrphanDndZipTmpInRoot, replaceFileAtomic } from './atomicReplace';
import { recoverOrphanProjectZipTmpInRoot, replaceFileAtomic } from './atomicReplace';
import { rmWithRetries } from './fsRetry';
import { optimizeImageBufferVisuallyLossless } from './optimizeImageImport.lib.mjs';
import { getLegacyProjectsRootDirs, getProjectsCacheRootDir, getProjectsRootDir } from './paths';
@@ -76,10 +81,10 @@ export class ZipProjectStore {
await fs.mkdir(getProjectsRootDir(), { recursive: true });
await fs.mkdir(getProjectsCacheRootDir(), { recursive: true });
await this.migrateLegacyProjectZipsIfNeeded();
await recoverOrphanDndZipTmpInRoot(getProjectsRootDir());
await recoverOrphanProjectZipTmpInRoot(getProjectsRootDir());
}
/** Копирует .dnd.zip из каталогов с «чужим» app name, если в текущем каталоге такого файла ещё нет. */
/** Копирует архивы проектов из каталогов с «чужим» app name, если в текущем каталоге такого файла ещё нет. */
private async migrateLegacyProjectZipsIfNeeded(): Promise<void> {
const dest = getProjectsRootDir();
let destNames: string[];
@@ -88,7 +93,7 @@ export class ZipProjectStore {
} catch {
return;
}
const destZips = new Set(destNames.filter((n) => n.endsWith('.dnd.zip')));
const destZips = new Set(destNames.filter((n) => isProjectZipFileName(n)));
for (const legacyRoot of getLegacyProjectsRootDirs()) {
let legacyNames: string[];
try {
@@ -97,7 +102,7 @@ export class ZipProjectStore {
continue;
}
for (const name of legacyNames) {
if (!name.endsWith('.dnd.zip')) continue;
if (!isProjectZipFileName(name)) continue;
if (destZips.has(name)) continue;
const from = path.join(legacyRoot, name);
const to = path.join(dest, name);
@@ -132,7 +137,7 @@ export class ZipProjectStore {
const root = getProjectsRootDir();
const entries = await fs.readdir(root, { withFileTypes: true });
const files = entries
.filter((e) => e.isFile() && e.name.endsWith('.dnd.zip'))
.filter((e) => e.isFile() && isProjectZipFileName(e.name))
.map((e) => path.join(root, e.name));
const out: ProjectIndexEntry[] = [];
@@ -180,7 +185,7 @@ export class ZipProjectStore {
sceneGraphEdges: [],
};
const zipPath = path.join(getProjectsRootDir(), `${fileBaseName}.dnd.zip`);
const zipPath = path.join(getProjectsRootDir(), projectZipFileNameFromBase(fileBaseName));
const cacheDir = path.join(getProjectsCacheRootDir(), id);
const projectPath = path.join(cacheDir, 'project.json');
this.openProject = { id, zipPath, cacheDir, projectPath, project };
@@ -778,7 +783,7 @@ export class ZipProjectStore {
const list = await this.listProjects();
const oldBase = open.project.meta.fileBaseName;
const nextFileName = `${sanitizedBase}.dnd.zip`;
const nextFileName = projectZipFileNameFromBase(sanitizedBase);
const nameClash = list.some(
(p) => p.id !== open.id && p.name.trim().toLowerCase() === nextName.toLowerCase(),
@@ -897,8 +902,8 @@ export class ZipProjectStore {
if (!st?.isFile()) {
throw new Error('Файл проекта не найден');
}
if (!resolved.toLowerCase().endsWith('.dnd.zip')) {
throw new Error('Ожидается файл с расширением .dnd.zip');
if (!isProjectZipFileName(resolved)) {
throw new Error('Ожидается файл проекта с расширением .ttrpg.zip или .dnd.zip');
}
const root = getProjectsRootDir();
@@ -909,11 +914,11 @@ export class ZipProjectStore {
let destPath: string;
let destFileName: string;
if (dirNorm === rootNorm && baseName.toLowerCase().endsWith('.dnd.zip')) {
if (dirNorm === rootNorm && isProjectZipFileName(baseName)) {
destPath = resolved;
destFileName = baseName;
} else {
destFileName = await uniqueDndZipFileName(root, baseName);
destFileName = await uniqueProjectZipFileNameInRoot(root, baseName);
destPath = path.join(root, destFileName);
if (onProgress) onProgress({ stage: 'copy', percent: 1, detail: 'Копирование…' });
await copyFileWithProgress(resolved, destPath, (pct) => {
@@ -930,7 +935,7 @@ export class ZipProjectStore {
const othersWithSameId = entries.filter((e) => e.id === project.id && e.fileName !== destFileName);
if (othersWithSameId.length > 0) {
const newId = asProjectId(this.randomId());
const stem = destFileName.replace(/\.dnd\.zip$/iu, '');
const stem = stripProjectZipExtension(destFileName);
project = {
...project,
id: newId,
@@ -1217,20 +1222,15 @@ function sanitizeFileName(name: string): string {
return safe.length > 0 ? safe : 'Untitled';
}
async function uniqueDndZipFileName(root: string, preferredBaseFileName: string): Promise<string> {
let base = preferredBaseFileName;
if (!base.toLowerCase().endsWith('.dnd.zip')) {
const stem = sanitizeFileName(path.basename(base, path.extname(base)) || 'project');
base = `${stem}.dnd.zip`;
}
const stem = base.replace(/\.dnd\.zip$/iu, '');
let candidate = base;
async function uniqueProjectZipFileNameInRoot(root: string, preferredBaseFileName: string): Promise<string> {
const stem = sanitizeFileName(stripProjectZipExtension(path.basename(preferredBaseFileName)) || 'project');
let candidate = projectZipFileNameFromBase(stem);
let n = 0;
for (;;) {
try {
await fs.access(path.join(root, candidate));
n += 1;
candidate = `${stem}_${String(n)}.dnd.zip`;
candidate = projectZipFileNameFromBase(`${stem}_${String(n)}`);
} catch {
return candidate;
}