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
+13
View File
@@ -0,0 +1,13 @@
/** Отображаемое имя продукта (с пробелом), не путать с `productName` сборки (`TTRPGPlayer`). */
export const APP_DISPLAY_NAME_EN = 'TTRPG Player';
export const APP_DISPLAY_NAME_RU = 'НРИ Плеер';
/** Системный идентификатор приложения (exe, AppImage, appId). */
export const APP_PRODUCT_NAME = 'TTRPGPlayer';
export const APP_BUNDLE_ID = 'com.ttrpgplayer.app';
export function appDisplayNameForLocale(localeTag: string): string {
const tag = localeTag.trim().toLowerCase();
if (tag.startsWith('ru')) return APP_DISPLAY_NAME_RU;
return APP_DISPLAY_NAME_EN;
}
+11 -7
View File
@@ -1,16 +1,20 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { isDndProductKey } from './productKey';
import { isProductKey } from './productKey';
void test('isDndProductKey: пример пользователя', () => {
assert.equal(isDndProductKey('DND-CEBEC1BF-AD0B-4312-BFDD-675AFF5955FD'), true);
void test('isProductKey: legacy DND prefix', () => {
assert.equal(isProductKey('DND-CEBEC1BF-AD0B-4312-BFDD-675AFF5955FD'), true);
});
void test('isDndProductKey: токен с точкой — нет', () => {
assert.equal(isDndProductKey('eyJ.xxx'), false);
void test('isProductKey: TTRPG prefix', () => {
assert.equal(isProductKey('TTRPG-CEBEC1BF-AD0B-4312-BFDD-675AFF5955FD'), true);
});
void test('isDndProductKey: пробелы по краям', () => {
assert.equal(isDndProductKey(' DND-CEBEC1BF-AD0B-4312-BFDD-675AFF5955FD '), true);
void test('isProductKey: token with dot — no', () => {
assert.equal(isProductKey('eyJ.xxx'), false);
});
void test('isProductKey: trimmed', () => {
assert.equal(isProductKey(' TTRPG-CEBEC1BF-AD0B-4312-BFDD-675AFF5955FD '), true);
});
+9 -2
View File
@@ -1,6 +1,13 @@
/** Продуктовый ключ активации (не путать с лицензионным токеном `base64.base64`). */
const TTRPG_PRODUCT_KEY_RE = /^TTRPG-[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}$/iu;
const DND_PRODUCT_KEY_RE = /^DND-[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}$/iu;
export function isDndProductKey(s: string): boolean {
return DND_PRODUCT_KEY_RE.test(s.trim());
export function isProductKey(s: string): boolean {
const t = s.trim();
return TTRPG_PRODUCT_KEY_RE.test(t) || DND_PRODUCT_KEY_RE.test(t);
}
/** @deprecated Используйте {@link isProductKey}. */
export function isDndProductKey(s: string): boolean {
return isProductKey(s);
}
+1 -1
View File
@@ -20,7 +20,7 @@ void test('package.json: конфиг electron-builder (mac/win/linux)', () => {
};
};
assert.ok(pkg.build);
assert.equal(pkg.build.appId, 'com.dndplayer.app');
assert.equal(pkg.build.appId, 'com.ttrpgplayer.app');
assert.equal(pkg.build.asar, true, 'релизный артефакт: app.asar без «голого» дерева dist в .app/.exe');
assert.ok(Array.isArray(pkg.build.asarUnpack));
assert.ok(pkg.build.asarUnpack.some((p) => p.includes('preload')));
+52
View File
@@ -0,0 +1,52 @@
/** Текущий формат архива проекта (новые сохранения). */
export const PROJECT_ZIP_EXTENSION = '.ttrpg.zip';
/** Устаревший формат — только открытие / импорт (вариант B). */
export const PROJECT_ZIP_EXTENSION_LEGACY = '.dnd.zip';
export function isProjectZipFileName(fileName: string): boolean {
const lower = fileName.toLowerCase();
return lower.endsWith(PROJECT_ZIP_EXTENSION) || lower.endsWith(PROJECT_ZIP_EXTENSION_LEGACY);
}
export function isLegacyProjectZipFileName(fileName: string): boolean {
return fileName.toLowerCase().endsWith(PROJECT_ZIP_EXTENSION_LEGACY);
}
export function projectZipFileNameFromBase(stem: string): string {
return `${stem}${PROJECT_ZIP_EXTENSION}`;
}
/** Имя файла для сохранения/экспорта — всегда `.ttrpg.zip`. */
export function normalizeSaveProjectZipPath(filePath: string): string {
const lower = filePath.toLowerCase();
if (lower.endsWith(PROJECT_ZIP_EXTENSION)) return filePath;
if (lower.endsWith(PROJECT_ZIP_EXTENSION_LEGACY)) {
return filePath.slice(0, -PROJECT_ZIP_EXTENSION_LEGACY.length) + PROJECT_ZIP_EXTENSION;
}
if (lower.endsWith('.zip')) {
return filePath.replace(/\.zip$/iu, PROJECT_ZIP_EXTENSION);
}
return `${filePath}${PROJECT_ZIP_EXTENSION}`;
}
export function stripProjectZipExtension(fileName: string): string {
const lower = fileName.toLowerCase();
if (lower.endsWith(PROJECT_ZIP_EXTENSION)) {
return fileName.slice(0, -PROJECT_ZIP_EXTENSION.length);
}
if (lower.endsWith(PROJECT_ZIP_EXTENSION_LEGACY)) {
return fileName.slice(0, -PROJECT_ZIP_EXTENSION_LEGACY.length);
}
return fileName;
}
export const PROJECT_ZIP_OPEN_DIALOG_FILTER: Electron.FileFilter = {
name: 'Проект TTRPG (*.ttrpg.zip, *.dnd.zip)',
extensions: ['ttrpg.zip', 'dnd.zip'],
};
export const PROJECT_ZIP_SAVE_DIALOG_FILTER: Electron.FileFilter = {
name: 'Проект TTRPG (*.ttrpg.zip)',
extensions: ['ttrpg.zip'],
};
+1 -1
View File
@@ -98,7 +98,7 @@ export type Scene = {
export type ProjectMeta = {
name: string;
/** Имя файла проекта без суффикса `.dnd.zip` (то, что пользователь редактирует). */
/** Имя файла проекта без суффикса `.ttrpg.zip` (то, что пользователь редактирует). */
fileBaseName: string;
createdAt: IsoDateTimeString;
updatedAt: IsoDateTimeString;