Skip to content

Commit ecc2ab0

Browse files
committed
feat: Curation templates
fix: System ext path being wrong in browser mode refactor: Remove outdated extension curationTemplate system fix: Shift selection in Curate
1 parent 615e5e2 commit ecc2ab0

16 files changed

Lines changed: 2285 additions & 2289 deletions

File tree

lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@
465465
"newCuration": "New Curation",
466466
"newCurationDesc": "Creates a new curation with a unique folder",
467467
"duplicateCuration": "Duplicate",
468+
"createTemplateFromCuration": "Create Template from Curation",
468469
"newCurationFromTemplate": "New Curation (Template)",
469470
"loadMeta": "Load Meta",
470471
"loadMetaDesc": "Load one or more Curation meta files",

src/back/extensions/ApiImplementation.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { PreferencesFile } from '@shared/preferences/PreferencesFile';
3131
import { overwritePreferenceData } from '@shared/preferences/util';
3232
import { formatString } from '@shared/utils/StringFormatter';
3333
import * as flashpoint from 'flashpoint-launcher';
34-
import { CurationTemplate, Game, IExtensionManifest, Task } from 'flashpoint-launcher';
34+
import { Game, IExtensionManifest, Task } from 'flashpoint-launcher';
3535
import * as fsExtra from 'fs-extra';
3636
import * as fs from 'node:fs';
3737
import * as path from 'node:path';
@@ -443,7 +443,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
443443
status: `Loading ${filePath}`
444444
});
445445
}
446-
const curState = await loadCurationArchive(filePath, null)
446+
const curState = await loadCurationArchive(filePath, false, undefined)
447447
.catch((error) => {
448448
log.error('Curate', `Failed to load curation archive! ${error.toString()}`);
449449
state.socketServer.broadcast(BackOut.OPEN_ALERT, formatString(state.languageContainer['dialog'].failedToLoadCuration, error.toString()) as string);
@@ -467,9 +467,8 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest,
467467
getCurations: () => {
468468
return [...state.loadedCurations];
469469
},
470-
async getCurationTemplates(): Promise<CurationTemplate[]> {
471-
const contribs = await state.extensionsService.getEnabledContributions('curationTemplates', state.preferences.disabledExtensions);
472-
return contribs.reduce<CurationTemplate[]>((prev, cur) => prev.concat(cur.value), []);
470+
async getCurationTemplates(): Promise<string[]> {
471+
return state.curationTemplates;
473472
},
474473
getCuration: (folder: string) => {
475474
const curation = state.loadedCurations.find(c => c.folder === folder);

src/back/extensions/ExtensionService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class ExtensionService {
2727
protected readonly _configData: AppConfigData,
2828
protected readonly _extensionPath: string,
2929
protected readonly _isDev: boolean,
30+
protected readonly _isElectron: boolean,
3031
) {
3132
this._extensions = [];
3233
this._extensionData = {};
@@ -39,7 +40,7 @@ export class ExtensionService {
3940
}
4041

4142
private async _scanExtensions(): Promise<void> {
42-
const sysExts = await scanSystemExtensions(this._isDev);
43+
const sysExts = await scanSystemExtensions(this._isDev, this._isElectron);
4344
sysExts.forEach(e => this._extensions.push(e));
4445
const exts = await scanExtensions(this._configData, this._extensionPath);
4546
exts.forEach(e => this._extensions.push(e));

src/back/extensions/ExtensionsScanner.ts

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { EditCurationMeta } from '@shared/curate/OLD_types';
21
import { ExtensionType, IExtension } from '@shared/extensions/interfaces';
32
import { readJsonFile } from '@shared/Util';
43
import * as Coerce from '@shared/utils/Coerce';
54
import { IObjectParserProp, ObjectParser } from '@shared/utils/ObjectParser';
6-
import { AppConfigData, Application, ButtonContext, ContextButton, Contributions, CurationTemplate, ExtConfiguration, ExtConfigurationProp, ExtTheme, IExtensionManifest, ILogoSet, ModuleContribution } from 'flashpoint-launcher';
5+
import { AppConfigData, Application, ButtonContext, ContextButton, Contributions, ExtConfiguration, ExtConfigurationProp, ExtTheme, IExtensionManifest, ILogoSet, ModuleContribution } from 'flashpoint-launcher';
76
import * as fs from 'node:fs';
87
import * as path from 'node:path';
98

109
const { str, num } = Coerce;
1110
const fsPromises = fs.promises;
1211

13-
export async function scanSystemExtensions(isDev: boolean): Promise<IExtension[]> {
14-
const extensionPath = isDev ? './extensions' : './resources/extensions';
12+
export async function scanSystemExtensions(isDev: boolean, isElectron: boolean): Promise<IExtension[]> {
13+
const extensionPath = (!isDev && isElectron) ? './resources/extensions': './extensions';
1514

1615
const result = new Map<string, IExtension>();
1716

@@ -182,7 +181,6 @@ function parseContributions(parser: IObjectParserProp<Contributions>): Contribut
182181
contextButtons: [],
183182
applications: [],
184183
configuration: [],
185-
curationTemplates: [],
186184
moduleFederation: [],
187185
themeFiles: [],
188186
};
@@ -191,7 +189,6 @@ function parseContributions(parser: IObjectParserProp<Contributions>): Contribut
191189
parser.prop('contextButtons', true).array(item => contributes.contextButtons.push(parseContextButton(item)));
192190
parser.prop('applications', true).array(item => contributes.applications.push(parseApplication(item)));
193191
parser.prop('configuration', true).array(item => contributes.configuration.push(parseConfiguration(item)));
194-
parser.prop('curationTemplates', true).array(item => contributes.curationTemplates.push(parseCurationTemplate(item)));
195192
parser.prop('moduleFederation', true).array(item => contributes.moduleFederation.push(parseModuleContribution(item)));
196193
parser.prop('themeFiles', true).arrayRaw(item => contributes.themeFiles.push(str(item)));
197194
return contributes;
@@ -268,22 +265,6 @@ function parseConfiguration(parser: IObjectParserProp<ExtConfiguration>): ExtCon
268265
return configuration;
269266
}
270267

271-
function parseCurationTemplate(parser: IObjectParserProp<CurationTemplate>): CurationTemplate {
272-
const curationTemplate: CurationTemplate = {
273-
name: '',
274-
logo: '',
275-
meta: {}
276-
};
277-
278-
parser.prop('name', v => curationTemplate.name = str(v));
279-
parser.prop('logo', v => curationTemplate.logo = str(v));
280-
curationTemplate.meta = parseCurationMeta(parser.prop('meta'));
281-
282-
// @TODO reuse code
283-
284-
return curationTemplate;
285-
}
286-
287268
function parseModuleContribution(parser: IObjectParserProp<ModuleContribution>): ModuleContribution {
288269
const mc: ModuleContribution = {
289270
scope: '',
@@ -296,31 +277,6 @@ function parseModuleContribution(parser: IObjectParserProp<ModuleContribution>):
296277
return mc;
297278
}
298279

299-
function parseCurationMeta(parser: IObjectParserProp<EditCurationMeta>): EditCurationMeta {
300-
const parsed: EditCurationMeta = {};
301-
302-
parser.prop('notes', v => parsed.notes = str(v));
303-
parser.prop('applicationPath', v => parsed.applicationPath = str(v));
304-
parser.prop('curationNotes', v => parsed.curationNotes = str(v));
305-
parser.prop('developer', v => parsed.developer = arrayStr(v));
306-
parser.prop('extreme', v => parsed.extreme = str(v).toLowerCase() === 'yes');
307-
parser.prop('language', v => parsed.language = arrayStr(v));
308-
parser.prop('launchCommand', v => parsed.launchCommand = str(v));
309-
parser.prop('originalDescription', v => parsed.originalDescription = str(v));
310-
parser.prop('playMode', v => parsed.playMode = arrayStr(v));
311-
parser.prop('publisher', v => parsed.publisher = arrayStr(v));
312-
parser.prop('releaseDate', v => parsed.releaseDate = str(v));
313-
parser.prop('series', v => parsed.series = str(v));
314-
parser.prop('source', v => parsed.source = str(v));
315-
parser.prop('status', v => parsed.status = str(v));
316-
parser.prop('title', v => parsed.title = str(v));
317-
parser.prop('alternateTitles', v => parsed.alternateTitles = arrayStr(v));
318-
parser.prop('version', v => parsed.version = str(v));
319-
parser.prop('library', v => parsed.library = str(v).toLowerCase()); // must be lower case
320-
321-
return parsed;
322-
}
323-
324280
function parseConfigurationProperty(parser: IObjectParserProp<ExtConfigurationProp>): ExtConfigurationProp {
325281
const prop: ExtConfigurationProp = {
326282
title: '',
@@ -347,12 +303,3 @@ function toPropType(v: any): ExtConfigurationProp['type'] {
347303
throw new Error('Configuration prop type is not valid. (string, object, number or boolean)');
348304
}
349305
}
350-
351-
// Coerce an object into a sensible string
352-
function arrayStr(rawStr: any): string {
353-
if (Array.isArray(rawStr)) {
354-
// Convert lists to ; separated strings
355-
return rawStr.join('; ');
356-
}
357-
return str(rawStr);
358-
}

src/back/index.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import {
3030
CURATIONS_FOLDER_EXPORTED,
3131
CURATIONS_FOLDER_EXTRACTING,
3232
CURATIONS_FOLDER_TEMP,
33-
CURATIONS_FOLDER_WORKING, CURATION_META_FILENAMES
33+
CURATIONS_FOLDER_TEMPLATES,
34+
CURATIONS_FOLDER_WORKING,
35+
CURATION_META_FILENAMES
3436
} from '@shared/constants';
3537
import { formatString } from '@shared/utils/StringFormatter';
3638
import { ComponentStatus, IBackProcessInfo, ILogoSet, LangFileContent, RecursivePartial } from 'flashpoint-launcher';
@@ -209,6 +211,7 @@ export const state: BackState = {
209211
extensionsService: createErrorProxy('extensionsService'),
210212
sevenZipPath: '',
211213
loadedCurations: [],
214+
curationTemplates: [],
212215
platformAppPaths: {},
213216
writeLocks: 0,
214217
prefsQueue: new EventQueue(),
@@ -663,7 +666,12 @@ async function prepForInit(initConfig: BackInitArgs): Promise<void> {
663666
// Load Extensions
664667

665668
await fs.ensureDir(path.join(state.config.flashpointPath, state.preferences.extensionsPath));
666-
state.extensionsService = new ExtensionService(state.config, path.join(state.config.flashpointPath, state.preferences.extensionsPath), state.isDev);
669+
state.extensionsService = new ExtensionService(
670+
state.config,
671+
path.join(state.config.flashpointPath, state.preferences.extensionsPath),
672+
state.isDev,
673+
state.isElectron,
674+
);
667675
await state.extensionsService.installedExtensionsReady.wait();
668676

669677
console.log('Back - Parsed Extensions');
@@ -1084,6 +1092,13 @@ async function initialize() {
10841092

10851093
console.log('Back - Initialized Database');
10861094

1095+
// Read curation template filenames
1096+
const templatesPath = path.join(state.config.flashpointPath, CURATIONS_FOLDER_TEMPLATES);
1097+
await fs.ensureDir(templatesPath);
1098+
state.curationTemplates = (await fs.promises.readdir(templatesPath, { withFileTypes: true }))
1099+
.filter(f => f.isFile())
1100+
.map(f => f.name);
1101+
10871102
// Load curations asynchronously
10881103

10891104
const rootPath = path.resolve(state.config.flashpointPath, CURATIONS_FOLDER_WORKING);
@@ -1646,7 +1661,7 @@ async function removeFileServerDownloadItem(item: ImageDownloadItem): Promise<vo
16461661
if (index >= 0) { state.fileServerDownloads.current.splice(index, 1); }
16471662
}
16481663

1649-
export async function loadCurationArchive(filePath: string, fpfssInfo: flashpoint.CurationFpfssInfo | null, onProgress?: (progress: Progress) => void): Promise<flashpoint.CurationState> {
1664+
export async function loadCurationArchive(filePath: string, clearUuid?: boolean, fpfssInfo?: flashpoint.CurationFpfssInfo, onProgress?: (progress: Progress) => void): Promise<flashpoint.CurationState> {
16501665
const key = uuid();
16511666
const extractPath = path.resolve(state.config.flashpointPath, CURATIONS_FOLDER_EXTRACTING, key);
16521667
// Extract to temp folder
@@ -1678,14 +1693,17 @@ export async function loadCurationArchive(filePath: string, fpfssInfo: flashpoin
16781693
// Load curation
16791694
const parsedMeta = await readCurationMeta(curationPath, state.platformAppPaths);
16801695
if (!parsedMeta) { throw new Error('Fail'); }
1696+
if (clearUuid) {
1697+
parsedMeta.uuid = uuid();
1698+
}
16811699

16821700
const loadedCuration: flashpoint.LoadedCuration = {
16831701
folder: key,
16841702
uuid: parsedMeta.uuid || uuid(),
16851703
group: parsedMeta.group,
16861704
game: parsedMeta.game,
16871705
addApps: parsedMeta.addApps,
1688-
fpfssInfo,
1706+
fpfssInfo: fpfssInfo || null,
16891707
thumbnail: await loadCurationIndexImage(path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, key, 'logo.png')),
16901708
screenshot: await loadCurationIndexImage(path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, key, 'ss.png')),
16911709
};

src/back/responses.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { overwriteConfigData } from '@shared/config/util';
2020
import {
2121
CURATIONS_FOLDER_EXPORTED,
22+
CURATIONS_FOLDER_TEMPLATES,
2223
CURATIONS_FOLDER_WORKING,
2324
LOGOS,
2425
SCREENSHOTS,
@@ -189,7 +190,6 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
189190
state.socketServer.register(BackIn.GET_RENDERER_EXTENSION_INFO, async () => {
190191
return {
191192
contextButtons: await state.extensionsService.getEnabledContributions('contextButtons', state.preferences.disabledExtensions),
192-
curationTemplates: await state.extensionsService.getEnabledContributions('curationTemplates', state.preferences.disabledExtensions),
193193
extConfigs: await state.extensionsService.getEnabledContributions('configuration', state.preferences.disabledExtensions),
194194
extConfig: state.extConfig,
195195
extensions: (await state.extensionsService.getExtensions()).map(e => {
@@ -2200,8 +2200,11 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
22002200
return result;
22012201
});
22022202

2203-
state.socketServer.register(BackIn.CURATE_LOAD_ARCHIVES, async (event, filePaths, taskId) => {
2203+
state.socketServer.register(BackIn.CURATE_LOAD_ARCHIVES, async (event, filePaths, isTemplate, taskId) => {
22042204
let processed = 0;
2205+
if (isTemplate) {
2206+
filePaths = filePaths.map(f => path.join(state.config.flashpointPath, CURATIONS_FOLDER_TEMPLATES, f));
2207+
}
22052208
const taskProgress = new TaskProgress(filePaths.length);
22062209
if (taskId) {
22072210
taskProgress.on('progress', (text, done) => {
@@ -2223,7 +2226,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
22232226
for (const filePath of filePaths) {
22242227
processed = processed + 1;
22252228
taskProgress.setStage(processed, `Loading ${filePath}`);
2226-
await loadCurationArchive(filePath, null, throttle((progress: Progress) => {
2229+
await loadCurationArchive(filePath, !!isTemplate, undefined, throttle((progress: Progress) => {
22272230
taskProgress.setStageProgress((progress.percent / 100), `Extracting Files - ${progress.fileCount}`);
22282231
}, 200))
22292232
.catch((error) => {
@@ -2239,6 +2242,10 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
22392242
return genCurationWarnings(curation, state.config.flashpointPath, state.suggestions, state.languageContainer.curate, state.apiEmitters.curations.onWillGenCurationWarnings);
22402243
});
22412244

2245+
state.socketServer.register(BackIn.CURATE_GET_TEMPLATES, () => {
2246+
return state.curationTemplates;
2247+
});
2248+
22422249
state.socketServer.register(BackIn.CURATE_GET_LIST, async () => {
22432250
if (state.curationsReady) {
22442251
return state.loadedCurations;
@@ -2415,7 +2422,6 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
24152422
await fs.ensureDir(curationsPath);
24162423
const curations = await fs.promises.readdir(curationsPath, { withFileTypes: true });
24172424
for (const curation of curations) {
2418-
console.log(curation.name);
24192425
if (curation.isDirectory()) {
24202426
const exists = state.loadedCurations.find(c => c.folder === curation.name);
24212427
if (!exists) {
@@ -2475,6 +2481,44 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
24752481
}
24762482
});
24772483

2484+
state.socketServer.register(BackIn.CURATE_CREATE_TEMPLATE_FROM_CURATION, async (event, folder, name) => {
2485+
const curation = state.loadedCurations.find(c => c.folder === folder);
2486+
if (curation) {
2487+
let filename = `${sanitizeFilename(name)}.7z`;
2488+
const templateFolder = path.join(state.config.flashpointPath, CURATIONS_FOLDER_TEMPLATES);
2489+
let fullPath = path.join(templateFolder, filename);
2490+
2491+
await fs.ensureDir(templateFolder);
2492+
// Don't overwrite existing templates
2493+
if (fs.existsSync(fullPath)) {
2494+
filename = `${sanitizeFilename(name)}-${Date.now()}.7z`;
2495+
fullPath = path.join(templateFolder, filename);
2496+
}
2497+
2498+
// Make sure curation is up to date on disk
2499+
const curPath = path.resolve(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, curation.folder);
2500+
await saveCuration(curPath, curation);
2501+
state.socketServer.broadcast(BackOut.CURATE_SELECT_LOCK, curation.folder, true);
2502+
await new Promise<void>((resolve) => {
2503+
// Cast required until types fixed
2504+
return (add as any)(fullPath, curPath, { recursive: true, exclude: [`!${FPFSS_INFO_FILENAME}`], $bin: pathTo7zBack(state.isDev, state.isElectron, state.exePath) })
2505+
.on('end', () => { resolve(); })
2506+
.on('error', (error: any) => {
2507+
log.error('Curate', error.message);
2508+
resolve();
2509+
});
2510+
})
2511+
.finally(() => {
2512+
state.socketServer.broadcast(BackOut.CURATE_SELECT_LOCK, curation.folder, false);
2513+
});
2514+
2515+
state.curationTemplates.push(filename);
2516+
} else {
2517+
throw new Error(`No curation found with folder '${folder}'`);
2518+
}
2519+
state.socketServer.broadcast(BackOut.CURATE_TEMPLATES_CHANGE, state.curationTemplates);
2520+
});
2521+
24782522
state.socketServer.register(BackIn.FPFSS_OPEN_CURATION, async (event, fpfssInfo, url, accessToken, taskId) => {
24792523
// Setup task info
24802524
const taskProgress = new TaskProgress(2);
@@ -2509,7 +2553,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise<v
25092553

25102554

25112555
taskProgress.setStage(2, `Loading ${tempFile}`);
2512-
await loadCurationArchive(tempFile, fpfssInfo, throttle((progress: Progress) => {
2556+
await loadCurationArchive(tempFile, false, fpfssInfo, throttle((progress: Progress) => {
25132557
taskProgress.setStageProgress((progress.percent / 100), `Extracting Files - ${progress.fileCount}`);
25142558
}, 200))
25152559
.catch((error) => {

src/back/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export type BackState = {
7474
sevenZipPath: string;
7575
/** All currently loaded curations. */
7676
loadedCurations: flashpoint.CurationState[];
77+
/** List of curation template files */
78+
curationTemplates: string[];
7779
/** Most recent app paths that were fetched from the database (cached in the back so it's available for the curation stuff /obelisk). */
7880
platformAppPaths: PlatformAppPathSuggestions;
7981
writeLocks: number;

0 commit comments

Comments
 (0)