From f292444825c9aedd2d4a8ee71c7a8e8efeb950d4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 16 Mar 2022 10:25:19 -0400 Subject: [PATCH 01/83] chore: add nano swapfiles to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3f7d0ba99..ac21c4c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ Data/flashpoint.sqlite /.coveralls.yml /coverage /tests/result + +# nano's swap/lock files. +*.swp From ec8bc92c2b6087f173f3f917484f480db06afa25 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 20 Mar 2022 10:18:57 -0400 Subject: [PATCH 02/83] First try, broken. --- src/back/GameLauncher.ts | 99 ++------- src/back/extensions/ApiImplementation.ts | 7 +- src/back/game/GameManager.ts | 67 ++++-- src/back/importGame.ts | 70 +++--- src/back/index.ts | 9 +- src/back/responses.ts | 204 ++++++++++++------ src/back/util/misc.ts | 58 ++++- src/database/entity/AdditionalApp.ts | 32 --- src/database/entity/Game.ts | 25 ++- src/renderer/components/CurateBox.tsx | 121 +++-------- src/renderer/components/CurateBoxAddApp.tsx | 150 ------------- .../components/RightBrowseSidebar.tsx | 76 ++++--- .../components/RightBrowseSidebarAddApp.tsx | 54 ++--- .../components/RightBrowseSidebarExtra.tsx | 125 +++++++++++ src/renderer/components/pages/BrowsePage.tsx | 3 +- src/renderer/components/pages/CuratePage.tsx | 10 +- src/renderer/context/CurationContext.ts | 118 +--------- src/shared/back/types.ts | 18 +- src/shared/curate/metaToMeta.ts | 136 ++---------- src/shared/curate/parse.ts | 52 +---- src/shared/curate/types.ts | 21 +- src/shared/game/interfaces.ts | 7 - src/shared/game/util.ts | 15 +- src/shared/lang.ts | 6 + typings/flashpoint-launcher.d.ts | 11 +- 25 files changed, 578 insertions(+), 916 deletions(-) delete mode 100644 src/database/entity/AdditionalApp.ts delete mode 100644 src/renderer/components/CurateBoxAddApp.tsx create mode 100644 src/renderer/components/RightBrowseSidebarExtra.tsx diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index 2da6ce1e6..74a2ae64d 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { AppProvider } from '@shared/extensions/interfaces'; import { ExecMapping, Omit } from '@shared/interfaces'; @@ -16,9 +15,8 @@ import * as GameDataManager from '@back/game/GameDataManager'; const { str } = Coerce; -export type LaunchAddAppOpts = LaunchBaseOpts & { - addApp: AdditionalApp; - native: boolean; +export type LaunchExtrasOpts = LaunchBaseOpts & { + extrasPath: string; } export type LaunchGameOpts = LaunchBaseOpts & { @@ -59,60 +57,20 @@ type LaunchBaseOpts = { export namespace GameLauncher { const logSource = 'Game Launcher'; - export function launchAdditionalApplication(opts: LaunchAddAppOpts): Promise { - // @FIXTHIS It is not possible to open dialog windows from the back process (all electron APIs are undefined). - switch (opts.addApp.applicationPath) { - case ':message:': - return new Promise((resolve, reject) => { - opts.openDialog({ - type: 'info', - title: 'About This Game', - message: opts.addApp.launchCommand, - buttons: ['Ok'], - }).finally(() => resolve()); - }); - case ':extras:': { - const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.addApp.launchCommand))); - return opts.openExternal(folderPath, { activate: true }) - .catch(error => { - if (error) { - opts.openDialog({ - type: 'error', - title: 'Failed to Open Extras', - message: `${error.toString()}\n`+ - `Path: ${folderPath}`, - buttons: ['Ok'], - }); - } - }); - } - default: { - let appPath: string = fixSlashes(path.join(opts.fpPath, getApplicationPath(opts.addApp.applicationPath, opts.execMappings, opts.native))); - const appPathOverride = opts.appPathOverrides.filter(a => a.enabled).find(a => a.path === appPath); - if (appPathOverride) { appPath = appPathOverride.override; } - const appArgs: string = opts.addApp.launchCommand; - const useWine: boolean = process.platform != 'win32' && appPath.endsWith('.exe'); - const launchInfo: LaunchInfo = { - gamePath: appPath, - gameArgs: appArgs, - useWine, - env: getEnvironment(opts.fpPath, opts.proxy), - }; - const proc = exec( - createCommand(launchInfo), - { env: launchInfo.env } - ); - logProcessOutput(proc); - log.info(logSource, `Launch Add-App "${opts.addApp.name}" (PID: ${proc.pid}) [ path: "${opts.addApp.applicationPath}", arg: "${opts.addApp.launchCommand}" ]`); - return new Promise((resolve, reject) => { - if (proc.killed) { resolve(); } - else { - proc.once('exit', () => { resolve(); }); - proc.once('error', error => { reject(error); }); - } + export async function launchExtras(opts: LaunchExtrasOpts): Promise { + const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.extrasPath))); + return opts.openExternal(folderPath, { activate: true }) + .catch(error => { + if (error) { + opts.openDialog({ + type: 'error', + title: 'Failed to Open Extras', + message: `${error.toString()}\n`+ + `Path: ${folderPath}`, + buttons: ['Ok'], }); } - } + }); } /** @@ -122,29 +80,12 @@ export namespace GameLauncher { export async function launchGame(opts: LaunchGameOpts, onWillEvent: ApiEmitter): Promise { // Abort if placeholder (placeholders are not "actual" games) if (opts.game.placeholder) { return; } - // Run all provided additional applications with "AutoRunBefore" enabled - if (opts.game.addApps) { - const addAppOpts: Omit = { - fpPath: opts.fpPath, - htdocsPath: opts.htdocsPath, - native: opts.native, - execMappings: opts.execMappings, - lang: opts.lang, - isDev: opts.isDev, - exePath: opts.exePath, - appPathOverrides: opts.appPathOverrides, - providers: opts.providers, - proxy: opts.proxy, - openDialog: opts.openDialog, - openExternal: opts.openExternal, - runGame: opts.runGame - }; - for (const addApp of opts.game.addApps) { - if (addApp.autoRunBefore) { - const promise = launchAdditionalApplication({ ...addAppOpts, addApp }); - if (addApp.waitForExit) { await promise; } - } - } + if (opts.game.message) { + await opts.openDialog({type: 'info', + title: 'About This Game', + message: opts.game.message, + buttons: ['Ok'], + }); } // Launch game let appPath: string = getApplicationPath(opts.game.applicationPath, opts.execMappings, opts.native); diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index 431f89e2b..aefd5af3c 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -142,7 +142,8 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, findGamesWithTag: GameManager.findGamesWithTag, updateGame: GameManager.save, updateGames: GameManager.updateGames, - removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndAddApps(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), + // Ardil TODO + removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); return game.tagsStr.split(';').findIndex(t => extremeTags.includes(t.trim())) !== -1; @@ -156,24 +157,28 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, get onWillLaunchGame() { return apiEmitters.games.onWillLaunchGame.event; }, + // Ardil TODO remove get onWillLaunchAddApp() { return apiEmitters.games.onWillLaunchAddApp.event; }, get onWillLaunchCurationGame() { return apiEmitters.games.onWillLaunchCurationGame.event; }, + // Ardil TODO remove get onWillLaunchCurationAddApp() { return apiEmitters.games.onWillLaunchCurationAddApp.event; }, get onDidLaunchGame() { return apiEmitters.games.onDidLaunchGame.event; }, + // Ardil TODO remove get onDidLaunchAddApp() { return apiEmitters.games.onDidLaunchAddApp.event; }, get onDidLaunchCurationGame() { return apiEmitters.games.onDidLaunchCurationGame.event; }, + // Ardil TODO remove get onDidLaunchCurationAddApp() { return apiEmitters.games.onDidLaunchCurationAddApp.event; }, diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 571fc484d..771b893e4 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -1,7 +1,6 @@ import { ApiEmitter } from '@back/extensions/ApiEmitter'; import { chunkArray } from '@back/util/misc'; import { validateSqlName, validateSqlOrder } from '@back/util/sql'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Playlist } from '@database/entity/Playlist'; import { PlaylistGame } from '@database/entity/PlaylistGame'; @@ -16,8 +15,9 @@ import { Coerce } from '@shared/utils/Coerce'; import * as fs from 'fs'; import * as path from 'path'; import * as TagManager from './TagManager'; -import { Brackets, FindOneOptions, getManager, SelectQueryBuilder } from 'typeorm'; +import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; +import { isNull } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -36,10 +36,10 @@ export const onDidRemovePlaylistGame = new ApiEmitter(); export async function countGames(): Promise { const gameRepository = getManager().getRepository(Game); - return gameRepository.count(); + return gameRepository.count({ parentGameId: IsNull() }); } -/** Find the game with the specified ID. */ +/** Find the game with the specified ID. Ardil TODO find refs*/ export async function findGame(id?: string, filter?: FindOneOptions): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); @@ -50,7 +50,7 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom return game; } } - +/** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } @@ -58,13 +58,15 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`); + .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) + .where("game.parentGameId is null"); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } - subQ.where(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); + subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); } if (filterOpts) { - applyFlatGameFilters('game', subQ, filterOpts, index ? 1 : 0); + // The "whereCount" param doesn't make much sense now, TODO change it. + applyFlatGameFilters('game', subQ, filterOpts, index ? 2 : 1); } if (orderBy) { subQ.orderBy(`game.${orderBy}`, direction); } @@ -78,11 +80,19 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o // console.log(`${Date.now() - startTime}ms for row`); return raw ? Coerce.num(raw.row_num) : -1; // Coerce it, even though it is probably of type number or undefined } - +/** + * Randomly selects a number of games from the database + * @param count The number of games to find. + * @param broken Whether to include broken games. + * @param excludedLibraries A list of libraries to exclude. + * @param flatFilters A set of filters on tags. + * @returns A ViewGame[] representing the results. + */ export async function findRandomGames(count: number, broken: boolean, excludedLibraries: string[], flatFilters: string[]): Promise { const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); + query.where("game.parentGameId is null"); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -172,7 +182,7 @@ export type FindGamesOpts = { export async function findAllGames(): Promise { const gameRepository = getManager().getRepository(Game); - return gameRepository.find(); + return gameRepository.find({parentGameId: IsNull()}); } /** Search the database for games. */ @@ -188,6 +198,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const range = ranges[i]; query = await getGameQuery('game', opts.filter, opts.orderBy, opts.direction, range.start, range.length, range.index); + query.where("game.parentGameId is null"); // Select games // @TODO Make it infer the type of T from the value of "shallow", and then use that to make "games" get the correct type, somehow? @@ -210,15 +221,15 @@ export async function findGames(opts: FindGamesOpts, shallow: return rangesOut; } -/** Find an add apps with the specified ID. */ -export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { +/** Find an add app with the specified ID. */ +export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { if (id || filter) { if (!filter) { filter = { - relations: ['parentGame'] + relations: ['parentGameId'] }; } - const addAppRepository = getManager().getRepository(AdditionalApp); + const addAppRepository = getManager().getRepository(Game); return addAppRepository.findOne(id, filter); } } @@ -229,6 +240,7 @@ export async function findPlatformAppPaths(platform: string): Promise .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) + .andWhere("game.parentGameId is null") .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -261,6 +273,7 @@ export async function findPlatforms(library: string): Promise { const gameRepository = getManager().getRepository(Game); const libraries = await gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) + .andWhere("game.parentGameId is null") .select('game.platform') .distinct() .getRawMany(); @@ -286,9 +299,10 @@ export async function save(game: Game): Promise { return savedGame; } -export async function removeGameAndAddApps(gameId: string, dataPacksFolderPath: string): Promise { +// Ardil TODO fix this. +export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); - const addAppRepository = getManager().getRepository(AdditionalApp); + //const addAppRepository = getManager().getRepository(AdditionalApp); const game = await findGame(gameId); if (game) { // Delete GameData @@ -298,9 +312,10 @@ export async function removeGameAndAddApps(gameId: string, dataPacksFolderPath: } await GameDataManager.remove(gameData.id); } - // Delete Add Apps - for (const addApp of game.addApps) { - await addAppRepository.remove(addApp); + // Delete children + // Ardil TODO do Seirade's suggestion. + for (const child of game.children) { + await gameRepository.remove(child); } // Delete Game await gameRepository.remove(game); @@ -472,6 +487,10 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi return whereCount; } +/** + * Add a position-independent search term (whitelist or blacklist) in or'd WHERE clauses on title, alternateTitles, + * developer, and publisher. + */ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: string, count: number, whitelist: boolean): void { validateSqlName(alias); @@ -500,6 +519,16 @@ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: str } } +/** + * Add a search term in a WHERE clause on the given field to a selectquerybuilder. + * @param alias The name of the table. + * @param query The query to add to. + * @param field The field (column) to search on. + * @param value The value to search for. If it's a string, it will be interpreted as position-independent + * if the field is not on the exactFields list. + * @param count How many conditions we've already filtered. Determines whether we use .where() or .andWhere(). + * @param whitelist Whether this is a whitelist or a blacklist search. + */ function doWhereField(alias: string, query: SelectQueryBuilder, field: string, value: any, count: number, whitelist: boolean) { // Create comparator const typing = typeof value; diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 0a7287ab9..9068cb822 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -1,12 +1,11 @@ import * as GameDataManager from '@back/game/GameDataManager'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Tag } from '@database/entity/Tag'; import { TagCategory } from '@database/entity/TagCategory'; import { validateSemiUUID } from '@renderer/util/uuid'; import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { convertEditToCurationMetaFile } from '@shared/curate/metaToMeta'; -import { CurationIndexImage, EditAddAppCuration, EditAddAppCurationMeta, EditCuration, EditCurationMeta } from '@shared/curate/types'; +import { CurationIndexImage, EditCuration, EditCurationMeta } from '@shared/curate/types'; import { getCurationFolder } from '@shared/curate/util'; import * as child_process from 'child_process'; import { execFile } from 'child_process'; @@ -17,7 +16,7 @@ import { ApiEmitter } from './extensions/ApiEmitter'; import * as GameManager from './game/GameManager'; import * as TagManager from './game/TagManager'; import { GameManagerState } from './game/types'; -import { GameLauncher, GameLaunchInfo, LaunchAddAppOpts, LaunchGameOpts } from './GameLauncher'; +import { GameLauncher, GameLaunchInfo, LaunchExtrasOpts, LaunchGameOpts } from './GameLauncher'; import { OpenExternalFunc, ShowMessageBoxFunc } from './types'; import { getMklinkBatPath } from './util/elevate'; import { uuid } from './util/uuid'; @@ -52,6 +51,7 @@ export const onWillImportCuration: ApiEmitter = new ApiEmit * Import a curation. * @returns A promise that resolves when the import is complete. */ +// Ardil TODO export async function importCuration(opts: ImportCurationOpts): Promise { if (opts.date === undefined) { opts.date = new Date(); } const { @@ -88,10 +88,9 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Build content list const contentToMove = []; - const extrasAddApp = curation.addApps.find(a => a.meta.applicationPath === ':extras:'); - if (extrasAddApp && extrasAddApp.meta.launchCommand && extrasAddApp.meta.launchCommand.length > 0) { + if (curation.meta.extras && curation.meta.extras.length > 0) { // Add extras folder if meta has an entry - contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', extrasAddApp.meta.launchCommand)]); + contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', curation.meta.extras)]); } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); @@ -110,7 +109,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Add game to database - let game = await createGameFromCurationMeta(gameId, curation.meta, curation.addApps, date); + let game = await createGameFromCurationMeta(gameId, curation.meta, date); game = await GameManager.save(game); // Store curation state for extension use later @@ -156,7 +155,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { if (saveCuration) { // Save working meta const metaPath = path.join(getCurationFolder(curation, fpPath), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, opts.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, opts.tagCategories)); await fs.writeFile(metaPath, meta); // Date in form 'YYYY-MM-DD' for folder sorting const date = new Date(); @@ -237,7 +236,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { console.warn(error.message); if (game.id) { // Clean up half imported entries - GameManager.removeGameAndAddApps(game.id, dataPacksFolderPath); + GameManager.removeGameAndChildren(game.id, dataPacksFolderPath); } }); } @@ -246,11 +245,12 @@ export async function importCuration(opts: ImportCurationOpts): Promise { * Create and launch a game from curation metadata. * @param curation Curation to launch */ -export async function launchCuration(key: string, meta: EditCurationMeta, addAppMetas: EditAddAppCurationMeta[], symlinkCurationContent: boolean, +// Ardil TODO +export async function launchCuration(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit, onWillEvent:ApiEmitter, onDidEvent: ApiEmitter) { if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } curationLog(`Launching Curation ${meta.title}`); - const game = await createGameFromCurationMeta(key, meta, [], new Date()); + const game = await createGameFromCurationMeta(key, meta, new Date()); GameLauncher.launchGame({ ...opts, game: game, @@ -259,22 +259,17 @@ export async function launchCuration(key: string, meta: EditCurationMeta, addApp onDidEvent.fire(game); } -/** - * Create and launch an additional application from curation metadata. - * @param curationKey Key of the parent curation index - * @param appCuration Add App Curation to launch - */ -export async function launchAddAppCuration(curationKey: string, appCuration: EditAddAppCuration, symlinkCurationContent: boolean, - skipLink: boolean, opts: Omit, onWillEvent: ApiEmitter, onDidEvent: ApiEmitter) { - if (!skipLink || !symlinkCurationContent) { await linkContentFolder(curationKey, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } - const addApp = createAddAppFromCurationMeta(appCuration, createPlaceholderGame()); - await onWillEvent.fire(addApp); - GameLauncher.launchAdditionalApplication({ - ...opts, - addApp: addApp, - }); - onDidEvent.fire(addApp); -} +// Ardil TODO this won't work, fix it. +export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, + skipLink: boolean, opts: Omit) { + if (meta.extras) { + if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } + await GameLauncher.launchExtras({ + ...opts, + extrasPath: meta.extras + }); + } + } function logMessage(text: string, curation: EditCuration): void { console.log(`- ${text}\n (id: ${curation.key})`); @@ -285,10 +280,12 @@ function logMessage(text: string, curation: EditCuration): void { * @param curation Curation to get data from. * @param gameId ID to use for Game */ -async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, addApps : EditAddAppCuration[], date: Date): Promise { +// Ardil TODO +async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, date: Date): Promise { const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) + parentGameId: gameMeta.parentGameId, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -312,26 +309,13 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration extreme: gameMeta.extreme || false, library: gameMeta.library || '', orderTitle: '', // This will be set when saved - addApps: [], + children: [], placeholder: false, activeDataOnDisk: false }); - game.addApps = addApps.map(addApp => createAddAppFromCurationMeta(addApp, game)); return game; } -function createAddAppFromCurationMeta(addAppMeta: EditAddAppCuration, game: Game): AdditionalApp { - return { - id: addAppMeta.key, - name: addAppMeta.meta.heading || '', - applicationPath: addAppMeta.meta.applicationPath || '', - launchCommand: addAppMeta.meta.launchCommand || '', - autoRunBefore: false, - waitForExit: false, - parentGame: game - }; -} - async function importGameImage(image: CurationIndexImage, gameId: string, folder: typeof LOGOS | typeof SCREENSHOTS, fullImagePath: string): Promise { if (image.exists) { const last = path.join(gameId.substr(0, 2), gameId.substr(2, 2), gameId+'.png'); @@ -520,7 +504,7 @@ function createPlaceholderGame(): Game { language: '', library: '', orderTitle: '', - addApps: [], + children: [], placeholder: true, activeDataOnDisk: false }); diff --git a/src/back/index.ts b/src/back/index.ts index 0834feba1..eac042842 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -1,5 +1,4 @@ import * as GameDataManager from '@back/game/GameDataManager'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { GameData } from '@database/entity/GameData'; import { Playlist } from '@database/entity/Playlist'; @@ -128,14 +127,10 @@ const state: BackState = { onLog: new ApiEmitter(), games: { onWillLaunchGame: new ApiEmitter(), - onWillLaunchAddApp: new ApiEmitter(), onWillLaunchCurationGame: new ApiEmitter(), - onWillLaunchCurationAddApp: new ApiEmitter(), onWillUninstallGameData: GameDataManager.onWillUninstallGameData, onDidLaunchGame: new ApiEmitter(), - onDidLaunchAddApp: new ApiEmitter(), onDidLaunchCurationGame: new ApiEmitter(), - onDidLaunchCurationAddApp: new ApiEmitter(), onDidUpdateGame: GameManager.onDidUpdateGame, onDidRemoveGame: GameManager.onDidRemoveGame, onDidUpdatePlaylist: GameManager.onDidUpdatePlaylist, @@ -218,7 +213,7 @@ async function main() { // Curation BackIn.IMPORT_CURATION, BackIn.LAUNCH_CURATION, - BackIn.LAUNCH_CURATION_ADDAPP, + BackIn.LAUNCH_CURATION_EXTRAS, // ? BackIn.SYNC_GAME_METADATA, // Meta Edits @@ -315,7 +310,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { const options: ConnectionOptions = { type: 'sqlite', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), - entities: [Game, AdditionalApp, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], + entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109] }; diff --git a/src/back/responses.ts b/src/back/responses.ts index cc275c8c9..2c6e03dcf 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -34,12 +34,12 @@ import * as GameDataManager from './game/GameDataManager'; import * as GameManager from './game/GameManager'; import * as TagManager from './game/TagManager'; import { escapeArgsForShell, GameLauncher, GameLaunchInfo } from './GameLauncher'; -import { importCuration, launchAddAppCuration, launchCuration } from './importGame'; +import { importCuration, launchCuration, launchCurationExtras } from './importGame'; import { ManagedChildProcess } from './ManagedChildProcess'; import { importAllMetaEdits } from './MetaEdit'; import { BackState, BareTag, TagsFile } from './types'; import { pathToBluezip } from './util/Bluezip'; -import { copyError, createAddAppFromLegacy, createContainer, createGameFromLegacy, createPlaylistFromJson, exit, pathExists, procToService, removeService, runService } from './util/misc'; +import { copyError, createChildFromFromLegacyAddApp, createContainer, createGameFromLegacy, createPlaylistFromJson, exit, pathExists, procToService, removeService, runService } from './util/misc'; import { sanitizeFilename } from './util/sanitizeFilename'; import { uuid } from './util/uuid'; @@ -165,6 +165,7 @@ export function registerRequestCallbacks(state: BackState): void { return { done }; }); + // Ardil TODO state.socketServer.register(BackIn.GET_SUGGESTIONS, async (event) => { const startTime = Date.now(); const suggestions: GamePropSuggestions = { @@ -186,6 +187,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_TOTAL, async (event) => { return await GameManager.countGames(); }); @@ -198,46 +200,18 @@ export function registerRequestCallbacks(state: BackState): void { return data; }); + // Ardil TODO state.socketServer.register(BackIn.GET_EXEC, (event) => { return state.execMappings; }); - state.socketServer.register(BackIn.LAUNCH_ADDAPP, async (event, id) => { - const addApp = await GameManager.findAddApp(id); - if (addApp) { - // If it has GameData, make sure it's present - let gameData: GameData | undefined; - if (addApp.parentGame.activeDataId) { - gameData = await GameDataManager.findOne(addApp.parentGame.activeDataId); - if (gameData && !gameData.presentOnDisk) { - // Download GameData - const onProgress = (percent: number) => { - // Sent to PLACEHOLDER download dialog on client - state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_PERCENT, percent); - }; - state.socketServer.broadcast(BackOut.OPEN_PLACEHOLDER_DOWNLOAD_DIALOG); - try { - await GameDataManager.downloadGameData(gameData.id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath), onProgress) - .finally(() => { - // Close PLACEHOLDER download dialog on client, cosmetic delay to look nice - setTimeout(() => { - state.socketServer.broadcast(BackOut.CLOSE_PLACEHOLDER_DOWNLOAD_DIALOG); - }, 250); - }); - } catch (error) { - state.socketServer.broadcast(BackOut.OPEN_ALERT, error); - log.info('Game Launcher', `Game Launch Aborted: ${error}`); - return; - } - } - } - await state.apiEmitters.games.onWillLaunchAddApp.fire(addApp); - const platform = addApp.parentGame ? addApp.parentGame : ''; - GameLauncher.launchAdditionalApplication({ - addApp, + state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { + const game = await GameManager.findGame(id); + if (game && game.extras) { + await GameLauncher.launchExtras({ + extrasPath: game.extras, fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, - native: addApp.parentGame && state.preferences.nativePlatforms.some(p => p === platform) || false, execMappings: state.execMappings, lang: state.languageContainer, isDev: state.isDev, @@ -247,16 +221,20 @@ export function registerRequestCallbacks(state: BackState): void { proxy: state.preferences.browserModeProxy, openDialog: state.socketServer.showMessageBoxBack(event.client), openExternal: state.socketServer.openExternal(event.client), - runGame: runGameFactory(state) + runGame: runGameFactory(state), }); - state.apiEmitters.games.onDidLaunchAddApp.fire(addApp); } }); - + // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id); if (game) { + // Ardil TODO not needed? Temp fix, see if it happens. + if (game.parentGameId && !game.parentGame) { + log.debug("Game Launcher", "Fetching parent game."); + game.parentGame = await GameManager.findGame(game.parentGameId) + } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -296,6 +274,34 @@ export function registerRequestCallbacks(state: BackState): void { } } } + // Make sure the parent's GameData is present too. + if (game.parentGame && game.parentGame.activeDataId) { + gameData = await GameDataManager.findOne(game.parentGame.activeDataId); + if (gameData && !gameData.presentOnDisk) { + // Download GameData + const onDetails = (details: DownloadDetails) => { + state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_DETAILS, details); + }; + const onProgress = (percent: number) => { + // Sent to PLACEHOLDER download dialog on client + state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_PERCENT, percent); + }; + state.socketServer.broadcast(BackOut.OPEN_PLACEHOLDER_DOWNLOAD_DIALOG); + try { + await GameDataManager.downloadGameData(gameData.id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath), onProgress, onDetails) + .finally(() => { + // Close PLACEHOLDER download dialog on client, cosmetic delay to look nice + setTimeout(() => { + state.socketServer.broadcast(BackOut.CLOSE_PLACEHOLDER_DOWNLOAD_DIALOG); + }, 250); + }); + } catch (error) { + state.socketServer.broadcast(BackOut.OPEN_ALERT, error); + log.info('Game Launcher', `Game Launch Aborted: ${error}`); + return; + } + } + } // Launch game await GameLauncher.launchGame({ game, @@ -318,10 +324,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAMES, async (event, data) => { await GameManager.updateGames(data); }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME, async (event, data) => { try { const game = await GameManager.save(data); @@ -337,8 +345,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { - const game = await GameManager.removeGameAndAddApps(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); + // Ardil TODO figure out this thing. + const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); state.queries = {}; // Clear entire cache @@ -349,13 +359,16 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + // Ardil TODO check that this was the right move. + /*state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { // Copy and apply new IDs + const newGame = deepCopy(game); + /* Ardil TODO figure this out. const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); newGame.id = uuid(); for (let j = 0; j < newAddApps.length; j++) { @@ -399,8 +412,9 @@ export function registerRequestCallbacks(state: BackState): void { library: result && result.library, gamesTotal: await GameManager.countGames(), }; - }); + });*/ + // Ardil TODO state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); if (playlist) { @@ -417,6 +431,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_PLAYLIST, async (event, filePath, library) => { try { const rawData = await fs.promises.readFile(filePath, 'utf-8'); @@ -452,6 +467,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_ALL_PLAYLISTS, async (event) => { const playlists = await GameManager.findPlaylists(true); for (const playlist of playlists) { @@ -460,6 +476,7 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.send(event.client, BackOut.PLAYLISTS_CHANGE, await GameManager.findPlaylists(state.preferences.browsePageShowExtreme)); }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_PLAYLIST, async (event, id, location) => { const playlist = await GameManager.findPlaylist(id, true); if (playlist) { @@ -469,6 +486,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { const game = await GameManager.findGame(id); @@ -501,10 +519,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { return GameManager.findGame(id); }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); // Verify it's still on disk @@ -520,10 +540,12 @@ export function registerRequestCallbacks(state: BackState): void { return gameData; }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_GAME_DATA, async (event, id) => { return GameDataManager.findGameData(id); }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME_DATAS, async (event, data) => { // Ignore presentOnDisk, client isn't the most aware await Promise.all(data.map(async (d) => { @@ -536,6 +558,7 @@ export function registerRequestCallbacks(state: BackState): void { })); }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME_DATA, async (event, gameDataId) => { const gameData = await GameDataManager.findOne(gameDataId); if (gameData) { @@ -557,10 +580,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_GAME_DATA, async (event, gameId, filePath) => { return GameDataManager.importGameData(gameId, filePath, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); }); + // Ardil TODO state.socketServer.register(BackIn.DOWNLOAD_GAME_DATA, async (event, gameDataId) => { const onProgress = (percent: number) => { // Sent to PLACEHOLDER download dialog on client @@ -579,6 +604,7 @@ export function registerRequestCallbacks(state: BackState): void { }); }); + // Ardil TODO This actually is important, don't ignore! state.socketServer.register(BackIn.UNINSTALL_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); if (gameData && gameData.path && gameData.presentOnDisk) { @@ -605,6 +631,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO should be quick. state.socketServer.register(BackIn.ADD_SOURCE_BY_URL, async (event, url) => { const sourceDir = path.join(state.config.flashpointPath, 'Data/Sources'); await fs.promises.mkdir(sourceDir, { recursive: true }); @@ -613,27 +640,33 @@ export function registerRequestCallbacks(state: BackState): void { }); }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_SOURCE, async (event, id) => { return SourceManager.remove(id); }); + // Ardil TODO state.socketServer.register(BackIn.GET_SOURCES, async (event) => { return SourceManager.find(); }); + // Ardil TODO state.socketServer.register(BackIn.GET_SOURCE_DATA, async (event, hashes) => { return GameDataManager.findSourceDataForHashes(hashes); }); + // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { return GameManager.findAllGames(); }); + // Ardil TODO state.socketServer.register(BackIn.RANDOM_GAMES, async (event, data) => { const flatFilters = data.tagFilters ? data.tagFilters.reduce((prev, cur) => prev.concat(cur.tags), []) : []; return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_KEYSET, async (event, library, query) => { query.filter = adjustGameFilter(query.filter); const result = await GameManager.findGamePageKeyset(query.filter, query.orderBy, query.orderReverse, query.searchLimit); @@ -643,6 +676,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_PAGE, async (event, data) => { data.query.filter = adjustGameFilter(data.query.filter); const startTime = new Date(); @@ -771,6 +805,7 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, @@ -828,6 +863,7 @@ export function registerRequestCallbacks(state: BackState): void { catch (error) { log.error('Launcher', error); } }); + // Ardil TODO add pref to make add-apps searchable? Later? state.socketServer.register(BackIn.UPDATE_PREFERENCES, async (event, data, refresh) => { const dif = difObjects(defaultPreferencesData, state.preferences, data); if (dif) { @@ -939,13 +975,14 @@ export function registerRequestCallbacks(state: BackState): void { return playlistGame; }); + // Ardil done state.socketServer.register(BackIn.SAVE_LEGACY_PLATFORM, async (event, platform) => { const translatedGames = []; const tagCache: Record = {}; for (const game of platform.collection.games) { const addApps = platform.collection.additionalApplications.filter(a => a.gameId === game.id); const translatedGame = await createGameFromLegacy(game, tagCache); - translatedGame.addApps = createAddAppFromLegacy(addApps, translatedGame); + translatedGame.children = createChildFromFromLegacyAddApp(addApps, translatedGame); translatedGames.push(translatedGame); } await GameManager.updateGames(translatedGames); @@ -1014,6 +1051,7 @@ export function registerRequestCallbacks(state: BackState): void { return res; }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_CURATION, async (event, data) => { let error: any | undefined; try { @@ -1042,7 +1080,7 @@ export function registerRequestCallbacks(state: BackState): void { return { error: error || undefined }; }); - state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { + state.socketServer.register(BackIn.LAUNCH_CURATION_EXTRAS, async (event, data) => { const skipLink = (data.key === state.lastLinkedCurationKey); state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; try { @@ -1067,37 +1105,57 @@ export function registerRequestCallbacks(state: BackState): void { } } } - - await launchCuration(data.key, data.meta, data.addApps, data.symlinkCurationContent, skipLink, { - fpPath: path.resolve(state.config.flashpointPath), - htdocsPath: state.preferences.htdocsFolderPath, - native: state.preferences.nativePlatforms.some(p => p === data.meta.platform), - execMappings: state.execMappings, - lang: state.languageContainer, - isDev: state.isDev, - exePath: state.exePath, - appPathOverrides: state.preferences.appPathOverrides, - providers: await getProviders(state), - proxy: state.preferences.browserModeProxy, - openDialog: state.socketServer.showMessageBoxBack(event.client), - openExternal: state.socketServer.openExternal(event.client), - runGame: runGameFactory(state), - }, - state.apiEmitters.games.onWillLaunchCurationGame, - state.apiEmitters.games.onDidLaunchCurationGame); + if (data.meta.extras) { + await launchCurationExtras(data.key, data.meta, data.symlinkCurationContent, skipLink, { + fpPath: path.resolve(state.config.flashpointPath), + htdocsPath: state.preferences.htdocsFolderPath, + execMappings: state.execMappings, + lang: state.languageContainer, + isDev: state.isDev, + exePath: state.exePath, + appPathOverrides: state.preferences.appPathOverrides, + providers: await getProviders(state), + proxy: state.preferences.browserModeProxy, + openDialog: state.socketServer.showMessageBoxBack(event.client), + openExternal: state.socketServer.openExternal(event.client), + runGame: runGameFactory(state) + }); + } } catch (e) { log.error('Launcher', e + ''); } }); - - state.socketServer.register(BackIn.LAUNCH_CURATION_ADDAPP, async (event, data) => { - const skipLink = (data.curationKey === state.lastLinkedCurationKey); - state.lastLinkedCurationKey = data.curationKey; + // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { + const skipLink = (data.key === state.lastLinkedCurationKey); + state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; try { - await launchAddAppCuration(data.curationKey, data.curation, data.symlinkCurationContent, skipLink, { + if (state.serviceInfo) { + // Make sure all 3 relevant server infos are present before considering MAD4FP opt + const configServer = state.serviceInfo.server.find(s => s.name === state.config.server); + const mad4fpServer = state.serviceInfo.server.find(s => s.mad4fp); + const activeServer = state.services.get('server'); + const activeServerInfo = state.serviceInfo.server.find(s => (activeServer && 'name' in activeServer.info && s.name === activeServer.info?.name)); + if (activeServer && configServer && mad4fpServer) { + if (data.mad4fp && activeServerInfo && !activeServerInfo.mad4fp) { + // Swap to mad4fp server + const mad4fpServerCopy = deepCopy(mad4fpServer); + // Set the content folder path as the final parameter + mad4fpServerCopy.arguments.push(getContentFolderByKey(data.key, state.config.flashpointPath)); + await removeService(state, 'server'); + runService(state, 'server', 'Server', state.config.flashpointPath, {}, mad4fpServerCopy); + } else if (!data.mad4fp && activeServerInfo && activeServerInfo.mad4fp && !configServer.mad4fp) { + // Swap to mad4fp server + await removeService(state, 'server'); + runService(state, 'server', 'Server', state.config.flashpointPath, {}, configServer); + } + } + } + + await launchCuration(data.key, data.meta, data.symlinkCurationContent, skipLink, { fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, - native: state.preferences.nativePlatforms.some(p => p === data.platform) || false, + native: state.preferences.nativePlatforms.some(p => p === data.meta.platform), execMappings: state.execMappings, lang: state.languageContainer, isDev: state.isDev, @@ -1109,8 +1167,8 @@ export function registerRequestCallbacks(state: BackState): void { openExternal: state.socketServer.openExternal(event.client), runGame: runGameFactory(state), }, - state.apiEmitters.games.onWillLaunchCurationAddApp, - state.apiEmitters.games.onDidLaunchCurationAddApp); + state.apiEmitters.games.onWillLaunchCurationGame, + state.apiEmitters.games.onDidLaunchCurationGame); } catch (e) { log.error('Launcher', e + ''); } @@ -1157,6 +1215,7 @@ export function registerRequestCallbacks(state: BackState): void { exit(state); }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { const game = await GameManager.findGame(id); if (game) { @@ -1219,6 +1278,7 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); + // Ardil TODO what is this? state.socketServer.register(BackIn.RUN_COMMAND, async (event, command, args = []) => { // Find command const c = state.registry.commands.get(command); @@ -1329,6 +1389,7 @@ function adjustGameFilter(filterOpts: FilterGameOpts): FilterGameOpts { return filterOpts; } +// Ardil TODO /** * Creates a function that will run any game launch info given to it and register it as a service */ @@ -1366,6 +1427,7 @@ function runGameFactory(state: BackState) { }; } +// Ardil TODO function createCommand(filename: string, useWine: boolean, execFile: boolean): string { // This whole escaping thing is horribly broken. We probably want to switch // to an array representing the argv instead and not have a shell @@ -1389,6 +1451,7 @@ function createCommand(filename: string, useWine: boolean, execFile: boolean): s * @param command Command to run * @param args Arguments for the command */ +// Ardil TODO what is this? async function runCommand(state: BackState, command: string, args: any[] = []): Promise { const callback = state.registry.commands.get(command); let res = undefined; @@ -1408,6 +1471,7 @@ async function runCommand(state: BackState, command: string, args: any[] = []): /** * Returns a set of AppProviders from all extension registered Applications, complete with callbacks to run them. */ +// Ardil TODO async function getProviders(state: BackState): Promise { return state.extensionsService.getContributions('applications') .then(contributions => { diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index d697b47ec..fb739ec7f 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -3,7 +3,6 @@ import { createTagsFromLegacy } from '@back/importGame'; import { ManagedChildProcess, ProcessOpts } from '@back/ManagedChildProcess'; import { SocketServer } from '@back/SocketServer'; import { BackState, ShowMessageBoxFunc, ShowOpenDialogFunc, ShowSaveDialogFunc, StatusState } from '@back/types'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Playlist } from '@database/entity/Playlist'; import { Tag } from '@database/entity/Tag'; @@ -15,6 +14,7 @@ import { Legacy_IAdditionalApplicationInfo, Legacy_IGameInfo } from '@shared/leg import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import * as child_process from 'child_process'; import * as fs from 'fs'; +import { add } from 'node-7z'; import * as path from 'path'; import { promisify } from 'util'; import { uuid } from './uuid'; @@ -163,8 +163,53 @@ export async function execProcess(state: BackState, proc: IBackProcessInfo, sync } } -export function createAddAppFromLegacy(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): AdditionalApp[] { - return addApps.map(a => { +export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): Game[] { + let retVal: Game[] = []; + for (const addApp of addApps) { + if (addApp.applicationPath === ':message:') { + game.message = addApp.launchCommand; + } else if (addApp.applicationPath === ':extras:') { + game.extras = addApp.launchCommand; + game.extrasName = addApp.name; + } else { + let newGame = new Game(); + Object.assign(newGame, { + id: addApp.id, + title: addApp.name, + applicationPath: addApp.applicationPath, + launchCommand: addApp.launchCommand, + parentGame: game, + library: game.library, + alternateTitles: "", + series: "", + developer: "", + publisher: "", + dateAdded: "0000-00-00 00:00:00.000", + dateModified: "0000-00-00 00:00:00.000", + platform: "", + broken: false, + extreme: game.extreme, + playMode: "", + status: "", + notes: "", + source: "", + releaseDate: "", + version: "", + originalDescription: "", + language: "", + orderTitle: addApp.name.toLowerCase(), + activeDataId: undefined, + activeDataOnDisk: false, + tags: [], + extras: undefined, + extrasName: undefined, + message: undefined, + children: [] + }) + retVal.push(newGame); + } + } + /*return addApps.map(a => { return { id: a.id, name: a.name, @@ -174,14 +219,15 @@ export function createAddAppFromLegacy(addApps: Legacy_IAdditionalApplicationInf waitForExit: a.waitForExit, parentGame: game }; - }); + });*/ + return retVal; } export async function createGameFromLegacy(game: Legacy_IGameInfo, tagCache: Record): Promise { const newGame = new Game(); Object.assign(newGame, { id: game.id, - parentGameId: game.id, + parentGameId: null, title: game.title, alternateTitles: game.alternateTitles, series: game.series, @@ -206,7 +252,7 @@ export async function createGameFromLegacy(game: Legacy_IGameInfo, tagCache: Rec library: game.library, orderTitle: game.orderTitle, placeholder: false, - addApps: [], + children: [], activeDataOnDisk: false }); return newGame; diff --git a/src/database/entity/AdditionalApp.ts b/src/database/entity/AdditionalApp.ts deleted file mode 100644 index ac6f499ab..000000000 --- a/src/database/entity/AdditionalApp.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { Game } from './Game'; - -@Entity() -export class AdditionalApp { - @PrimaryGeneratedColumn('uuid') - /** ID of the additional application (unique identifier) */ - id: string; - @Column() - /** Path to the application that runs the additional application */ - applicationPath: string; - @Column() - /** - * If the additional application should run before the game. - * (If true, this will always run when the game is launched) - * (If false, this will only run when specifically launched) - */ - autoRunBefore: boolean; - @Column() - /** Command line argument(s) passed to the application to launch the game */ - launchCommand: string; - @Column() - /** Name of the additional application */ - @Column({collation: 'NOCASE'}) - name: string; - @Column() - /** Wait for this to exit before the Game will launch (if starting before launch) */ - waitForExit: boolean; - @ManyToOne(type => Game, game => game.addApps) - /** Parent of this add app */ - parentGame: Game; -} diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 6adba1c99..987b04cc3 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,5 +1,4 @@ import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -import { AdditionalApp } from './AdditionalApp'; import { GameData } from './GameData'; import { Tag } from './Tag'; @@ -17,12 +16,18 @@ export class Game { /** ID of the game (unique identifier) */ id: string; - @ManyToOne(type => Game) + @ManyToOne(type => Game, game => game.children) parentGame?: Game; @Column({ nullable: true }) parentGameId?: string; + @OneToMany(type => Game, game => game.parentGame, { + cascade: true, + eager: true + }) + children: Game[]; + @Column({collation: 'NOCASE'}) @Index('IDX_gameTitle') /** Full title of the game */ @@ -120,13 +125,6 @@ export class Game { /** The title but reconstructed to be suitable for sorting and ordering (and not be shown visually) */ orderTitle: string; - @OneToMany(type => AdditionalApp, addApp => addApp.parentGame, { - cascade: true, - eager: true - }) - /** All attached Additional Apps of a game */ - addApps: AdditionalApp[]; - /** If the game is a placeholder (and can therefore not be saved) */ placeholder: boolean; @@ -141,6 +139,15 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; + @Column({ nullable: true }) + extras?: string; + + @Column({ nullable: true }) + extrasName?: string; + + @Column({ nullable: true }) + message?: string; + // This doesn't run... sometimes. @BeforeUpdate() updateTagsStr() { diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 6eb6a101d..b9fdee162 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -26,7 +26,6 @@ import { toForcedURL } from '../Util'; import { LangContext } from '../util/lang'; import { pathTo7z } from '../util/SevenZip'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; -import { CurateBoxAddApp } from './CurateBoxAddApp'; import { CurateBoxRow } from './CurateBoxRow'; import { CurateBoxWarnings, CurationWarnings, getWarningCount } from './CurateBoxWarnings'; import { DropdownInputField } from './DropdownInputField'; @@ -87,7 +86,7 @@ export function CurateBox(props: CurateBoxProps) { saveThrottle(() => { if (props.curation) { const metaPath = path.join(getCurationFolder2(props.curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(props.curation.meta, props.tagCategories, props.curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(props.curation.meta, props.tagCategories)); fs.writeFile(metaPath, meta); console.log('Auto-Saved Curation'); } @@ -205,6 +204,10 @@ export function CurateBox(props: CurateBoxProps) { const onOriginalDescriptionChange = useOnInputChange('originalDescription', key, props.dispatch); const onCurationNotesChange = useOnInputChange('curationNotes', key, props.dispatch); const onMountParametersChange = useOnInputChange('mountParameters', key, props.dispatch); + const onParentGameIdChange = useOnInputChange('parentGameId', key, props.dispatch); + const onMessageChange = useOnInputChange('message', key, props.dispatch); + const onExtrasChange = useOnInputChange('extras', key, props.dispatch); + const onExtrasNameChange = useOnInputChange('extrasName', key, props.dispatch); // Callbacks for the fields (onItemSelect) const onPlayModeSelect = useCallback(transformOnItemSelect(onPlayModeChange), [onPlayModeChange]); const onStatusSelect = useCallback(transformOnItemSelect(onStatusChange), [onStatusChange]); @@ -307,7 +310,6 @@ export function CurateBox(props: CurateBoxProps) { await window.Shared.back.request(BackIn.LAUNCH_CURATION, { key: props.curation.key, meta: props.curation.meta, - addApps: props.curation.addApps.map(addApp => addApp.meta), mad4fp: mad4fp, symlinkCurationContent: props.symlinkCurationContent }); @@ -378,42 +380,6 @@ export function CurateBox(props: CurateBoxProps) { }); } }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when the new additional application button is clicked - const onNewAddApp = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'normal' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when adding an Extras add app - const onAddExtras = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'extras' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when adding a Message add app - const onAddMessage = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'message' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); // Callback for when the export button is clicked const onExportClick = useCallback(async () => { if (props.curation) { @@ -468,7 +434,7 @@ export function CurateBox(props: CurateBoxProps) { .catch((error) => { /* No file is okay, ignore error */ }); // Save working meta const metaPath = path.join(getCurationFolder2(curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories)); const statusProgress = newProgress(props.curation.key, progressDispatch); ProgressDispatch.setText(statusProgress, 'Exporting Curation...'); ProgressDispatch.setUsePercentDone(statusProgress, false); @@ -520,36 +486,6 @@ export function CurateBox(props: CurateBoxProps) { } return false; }, [props.curation]); - // Render additional application elements - const addApps = useMemo(() => ( - <> - { strings.browse.additionalApplications }: - { props.curation && props.curation.addApps.length > 0 ? ( - - - { props.curation.addApps.map(addApp => ( - - )) } - -
- ) : undefined } - - ), [ - props.curation && props.curation.addApps, - props.curation && props.curation.key, - props.symlinkCurationContent, - props.dispatch, - native, - disabled - ]); // Count the number of collisions const collisionCount: number | undefined = useMemo(() => { @@ -848,33 +784,30 @@ export function CurateBox(props: CurateBoxProps) { className={curationNotes.length > 0 ? 'input-field--info' : ''} { ...sharedInputProps } /> + + + + + + + + +
- {/* Additional Application */} -
- {addApps} -
-
- - - -
-
-
-
{/* Content */}
diff --git a/src/renderer/components/CurateBoxAddApp.tsx b/src/renderer/components/CurateBoxAddApp.tsx deleted file mode 100644 index 665ba2bd1..000000000 --- a/src/renderer/components/CurateBoxAddApp.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { BackIn } from '@shared/back/types'; -import { EditAddAppCuration, EditAddAppCurationMeta } from '@shared/curate/types'; -import * as React from 'react'; -import { useCallback } from 'react'; -import { CurationAction } from '../context/CurationContext'; -import { LangContext } from '../util/lang'; -import { CurateBoxRow } from './CurateBoxRow'; -import { InputField } from './InputField'; -import { SimpleButton } from './SimpleButton'; - -export type CurateBoxAddAppProps = { - /** Key of the curation the displayed additional application belongs to. */ - curationKey: string; - /** Meta data for the additional application to display. */ - curation: EditAddAppCuration; - /** If editing any fields of this should be disabled. */ - disabled?: boolean; - /** Dispatcher for the curate page state reducer. */ - dispatch: React.Dispatch; - /** Platform of the game this belongs to. */ - platform?: string; - /** Whether to symlink curation content before running */ - symlinkCurationContent: boolean; - /** Callback for the "onKeyDown" event for all input fields. */ - onInputKeyDown?: (event: React.KeyboardEvent) => void; -}; - -export function CurateBoxAddApp(props: CurateBoxAddAppProps) { - // Callbacks for the fields (onChange) - const curationKey = props.curationKey; - const key = props.curation.key; - const onHeadingChange = useOnInputChange('heading', key, curationKey, props.dispatch); - const onApplicationPathChange = useOnInputChange('applicationPath', key, curationKey, props.dispatch); - const onLaunchCommandChange = useOnInputChange('launchCommand', key, curationKey, props.dispatch); - // Misc. - const editable = true; - const disabled = props.disabled; - // Localized strings - const strings = React.useContext(LangContext); - const specialType = props.curation.meta.applicationPath === ':extras:' || props.curation.meta.applicationPath === ':message:'; - let lcString = strings.browse.launchCommand; - let lcPlaceholderString = strings.browse.noLaunchCommand; - // Change Launch Command strings depending on add app type - switch (props.curation.meta.applicationPath) { - case ':message:': - lcString = strings.curate.message; - lcPlaceholderString = strings.curate.noMessage; - break; - case ':extras:': - lcString = strings.curate.folderName; - lcPlaceholderString = strings.curate.noFolderName; - break; - } - // Callback for the "remove" button - const onRemove = useCallback(() => { - props.dispatch({ - type: 'remove-addapp', - payload: { - curationKey: props.curationKey, - key: props.curation.key - } - }); - }, [props.curationKey, props.curation.key, props.dispatch]); - // Callback for the "run" button - const onRun = useCallback(() => { - return window.Shared.back.request(BackIn.LAUNCH_CURATION_ADDAPP, { - curationKey: props.curationKey, - curation: props.curation, - platform: props.platform, - symlinkCurationContent: props.symlinkCurationContent - }); - }, [props.curation && props.curation.meta && props.curationKey, props.symlinkCurationContent, props.platform]); - // Render - return ( - - - - - { specialType ? undefined : ( - - - - ) } - - - - - - - ); -} - -type InputElement = HTMLInputElement | HTMLTextAreaElement; - -/** Subset of the input elements on change event, with only the properties used by the callbacks. */ -type InputElementOnChangeEvent = { - currentTarget: { - value: React.ChangeEvent['currentTarget']['value'] - } -} - -/** - * Create a callback for InputField's onChange. - * When called, the callback will set the value of a metadata property to the value of the input field. - * @param property Property the input field should change. - * @param curationKey Key of the curation the additional application belongs to. - * @param key Key of the additional application to edit. - * @param dispatch Dispatcher to use. - */ -function useOnInputChange(property: keyof EditAddAppCurationMeta, key: string, curationKey: string, dispatch: React.Dispatch) { - return useCallback((event: InputElementOnChangeEvent) => { - if (key !== undefined) { - dispatch({ - type: 'edit-addapp-meta', - payload: { - curationKey: curationKey, - key: key, - property: property, - value: event.currentTarget.value - } - }); - } - }, [dispatch, key]); -} diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 115f712b7..c035e15f7 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -28,7 +28,8 @@ import { GameImageSplit } from './GameImageSplit'; import { ImagePreview } from './ImagePreview'; import { InputElement, InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; -import { RightBrowseSidebarAddApp } from './RightBrowseSidebarAddApp'; +import { RightBrowseSidebarChild } from './RightBrowseSidebarAddApp'; +import { RightBrowseSidebarExtra } from './RightBrowseSidebarExtra'; import { SimpleButton } from './SimpleButton'; import { TagInputField } from './TagInputField'; @@ -53,6 +54,8 @@ type OwnProps = { onDeselectPlaylist: () => void; /** Called when the playlist notes for the selected game has been changed */ onEditPlaylistNotes: (text: string) => void; + /** Called when a child game needs to be deleted. */ + onDeleteGame: (gameId: string) => void; /** If the "edit mode" is currently enabled */ isEditing: boolean; /** If the selected game is a new game being created */ @@ -109,6 +112,7 @@ export class RightBrowseSidebar extends React.Component this.props.onEditGame({ applicationPath: text })); onNotesChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ notes: text })); onOriginalDescriptionChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ originalDescription: text })); + onMessageChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ message: text })); onBrokenChange = this.wrapOnCheckBoxChange(game => { if (this.props.currentGame) { this.props.onEditGame({ broken: !this.props.currentGame.broken }); @@ -194,7 +198,7 @@ export class RightBrowseSidebar extends React.Component
+ {game.message ? +
+

{strings.message}:

+ +
+ : undefined}

{strings.dateAdded}:

+ { game.broken || editable ? (
) : undefined } {/* -- Additional Applications -- */} - { editable || (currentAddApps && currentAddApps.length > 0) ? ( + { editable || (currentChildren && currentChildren.length > 0) ? (

{strings.additionalApplications}:

- { editable ? ( - - ) : undefined }
- { currentAddApps && currentAddApps.map((addApp) => ( - ( + + onLaunch={this.onChildLaunch} + onDelete={this.onChildDelete} /> )) } + {game.extras && game.extrasName ? + : undefined + }
) : undefined } {/* -- Application Path & Launch Command -- */} @@ -920,28 +938,26 @@ export class RightBrowseSidebar extends React.Component { + onChildDelete = (childId: string): void => { if (this.props.currentGame) { - const newAddApps = deepCopy(this.props.currentGame.addApps); - if (!newAddApps) { throw new Error('editAddApps is missing.'); } - const index = newAddApps.findIndex(addApp => addApp.id === addAppId); + const newChildren = deepCopy(this.props.currentGame.children); + if (!newChildren) { throw new Error('editAddApps is missing.'); } + const index = newChildren.findIndex(addApp => addApp.id === childId); if (index === -1) { throw new Error('Cant remove additional application because it was not found.'); } - newAddApps.splice(index, 1); - this.props.onEditGame({ addApps: newAddApps }); + newChildren.splice(index, 1); + this.props.onEditGame({children: newChildren}); + this.props.onDeleteGame(childId); } } - onNewAddAppClick = (): void => { - if (!this.props.currentGame) { throw new Error('Unable to add a new AddApp. "currentGame" is missing.'); } - const newAddApp = ModelUtils.createAddApp(this.props.currentGame); - newAddApp.id = uuid(); - this.props.onEditGame({ addApps: [...this.props.currentGame.addApps, ...[newAddApp]] }); - } - onScreenshotClick = (): void => { this.setState({ showPreview: true }); } diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 8890b1ed7..8b714a0ab 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -1,4 +1,4 @@ -import { AdditionalApp } from '@database/entity/AdditionalApp'; +import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; @@ -7,41 +7,39 @@ import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; -export type RightBrowseSidebarAddAppProps = { +export type RightBrowseSidebarChildProps = { /** Additional Application to show and edit */ - addApp: AdditionalApp; + child: Game; /** Called when a field is edited */ onEdit?: () => void; /** Called when a field is edited */ - onDelete?: (addAppId: string) => void; + onDelete?: (childId: string) => void; /** Called when the launch button is clicked */ - onLaunch?: (addAppId: string) => void; + onLaunch?: (childId: string) => void; /** If the editing is disabled (it cant go into "edit mode") */ editDisabled?: boolean; }; -export interface RightBrowseSidebarAddApp { +export interface RightBrowseSidebarChild { context: LangContainer; } /** Displays an additional application for a game in the right sidebar of BrowsePage. */ -export class RightBrowseSidebarAddApp extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.name = text; }); +export class RightBrowseSidebarChild extends React.Component { + onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); - onAutoRunBeforeChange = this.wrapOnCheckBoxChange((addApp) => { addApp.autoRunBefore = !addApp.autoRunBefore; }); - onWaitForExitChange = this.wrapOnCheckBoxChange((addApp) => { addApp.waitForExit = !addApp.waitForExit; }); render() { const allStrings = this.context; const strings = allStrings.browse; - const { addApp, editDisabled } = this.props; + const { child: addApp, editDisabled } = this.props; return (
{/* Title & Launch Button */}
@@ -71,27 +69,9 @@ export class RightBrowseSidebarAddApp extends React.Component
- {/* Auto Run Before */} -
-
- -

{strings.autoRunBefore}

-
-
+ {/* Wait for Exit */}
-
- -

{strings.waitForExit}

-
{/* Delete Button */} { !editDisabled ? ( { if (this.props.onLaunch) { - this.props.onLaunch(this.props.addApp.id); + this.props.onLaunch(this.props.child.id); } } onDeleteClick = (): void => { if (this.props.onDelete) { - this.props.onDelete(this.props.addApp.id); + this.props.onDelete(this.props.child.id); } } @@ -138,9 +118,9 @@ export class RightBrowseSidebarAddApp extends React.Component void): (event: React.ChangeEvent) => void { + wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { return (event) => { - const addApp = this.props.addApp; + const addApp = this.props.child; if (addApp) { func(addApp, event.currentTarget.value); this.forceUpdate(); @@ -149,10 +129,10 @@ export class RightBrowseSidebarAddApp extends React.Component void) { + wrapOnCheckBoxChange(func: (addApp: Game) => void) { return () => { if (!this.props.editDisabled) { - func(this.props.addApp); + func(this.props.child); this.onEdit(); this.forceUpdate(); } diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx new file mode 100644 index 000000000..f19c733ff --- /dev/null +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -0,0 +1,125 @@ +import { Game } from '@database/entity/Game'; +import { LangContainer } from '@shared/lang'; +import * as React from 'react'; +import { LangContext } from '../util/lang'; +import { CheckBox } from './CheckBox'; +import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; +import { InputField } from './InputField'; +import { OpenIcon } from './OpenIcon'; + +export type RightBrowseSidebarExtraProps = { + /** Extras to show and edit */ + // These two are xplicitly non-nullable. + extrasPath: string; + extrasName: string; + game: Game; + /** Called when a field is edited */ + onEdit?: () => void; + /** Called when a field is edited */ + onDelete?: (gameId: string) => void; + /** Called when the launch button is clicked */ + onLaunch?: (gameId: string) => void; + /** If the editing is disabled (it cant go into "edit mode") */ + editDisabled?: boolean; +}; + +export interface RightBrowseSidebarExtra { + context: LangContainer; +} + +/** Displays an additional application for a game in the right sidebar of BrowsePage. */ +export class RightBrowseSidebarExtra extends React.Component { + onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); + onExtrasNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); + onExtrasPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); + + render() { + const allStrings = this.context; + const strings = allStrings.browse; + const { extrasPath, extrasName, editDisabled } = this.props; + return ( +
+ {/* Title & Launch Button */} +
+ + +
+ { editDisabled ? undefined : ( + <> + {/* Launch Command */} +
+

{strings.extras}:

+ +
+ + ) } +
+ ); + } + + renderDeleteButton({ confirm, extra }: ConfirmElementArgs): JSX.Element { + const className = 'browse-right-sidebar__additional-application__delete-button'; + return ( +
+ +
+ ); + } + + onLaunchClick = (): void => { + if (this.props.onLaunch) { + this.props.onLaunch(this.props.game.id); + } + } + + onDeleteClick = (): void => { + if (this.props.onDelete) { + this.props.onDelete(this.props.game.id); + } + } + + onEdit(): void { + if (this.props.onEdit) { + this.props.onEdit(); + } + } + + /** Create a wrapper for a EditableTextWrap's onEditDone callback (this is to reduce redundancy). */ + wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { + return (event) => { + const addApp = this.props.game; + if (addApp) { + func(addApp, event.currentTarget.value); + this.forceUpdate(); + } + }; + } + + /** Create a wrapper for a CheckBox's onChange callback (this is to reduce redundancy). */ + wrapOnCheckBoxChange(func: (addApp: Game) => void) { + return () => { + if (!this.props.editDisabled) { + func(this.props.game); + this.onEdit(); + this.forceUpdate(); + } + }; + } + + static contextType = LangContext; +} diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index d9aa3997c..6e453d8c7 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -286,6 +286,7 @@ export class BrowsePage extends React.Component !c.delete)) { const metaPath = path.join(getCurationFolder2(curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories)); try { fs.writeFileSync(metaPath, meta); } catch (error) { @@ -743,7 +743,6 @@ async function loadCurationFolder(key: string, fullPath: string, defaultGameMeta const loadedCuration: EditCuration = { key: key, meta: {}, - addApps: [], thumbnail: createCurationIndexImage(), screenshot: createCurationIndexImage(), content: [], @@ -790,13 +789,6 @@ async function loadCurationFolder(key: string, fullPath: string, defaultGameMeta await readCurationMeta(metaYamlPath, defaultGameMetaValues) .then(async (parsedMeta) => { loadedCuration.meta = parsedMeta.game; - for (let i = 0; i < parsedMeta.addApps.length; i++) { - const meta = parsedMeta.addApps[i]; - loadedCuration.addApps.push({ - key: uuid(), - meta: meta, - }); - } }) .catch((error) => { const formedMessage = `Error Parsing Curation Meta at ${metaYamlPath} - ${error.message}`; diff --git a/src/renderer/context/CurationContext.ts b/src/renderer/context/CurationContext.ts index 295e19d7a..ccce946a4 100644 --- a/src/renderer/context/CurationContext.ts +++ b/src/renderer/context/CurationContext.ts @@ -1,6 +1,6 @@ import { GameMetaDefaults } from '@shared/curate/defaultValues'; -import { generateExtrasAddApp, generateMessageAddApp, ParsedCurationMeta } from '@shared/curate/parse'; -import { CurationIndexImage, EditAddAppCurationMeta, EditCuration, EditCurationMeta, IndexedContent } from '@shared/curate/types'; +import { ParsedCurationMeta } from '@shared/curate/parse'; +import { CurationIndexImage, EditCuration, EditCurationMeta, IndexedContent } from '@shared/curate/types'; import { createContextReducer } from '../context-reducer/contextReducer'; import { ReducerAction } from '../context-reducer/interfaces'; import { createCurationIndexImage } from '../curate/importCuration'; @@ -34,7 +34,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const index = nextCurations.findIndex(c => c.key === action.payload.key); if (index !== -1) { const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; // Mark curation for deletion nextCuration.delete = true; nextCurations[index] = nextCuration; @@ -47,7 +47,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const index = nextCurations.findIndex(c => c.key === action.payload.key); if (index !== -1) { const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; // Mark curation for deletion nextCuration.deleted = true; nextCurations[index] = nextCuration; @@ -59,17 +59,9 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; const parsedMeta = action.payload.parsedMeta; nextCuration.meta = parsedMeta.game; - nextCuration.addApps = []; - for (let i = 0; i < parsedMeta.addApps.length; i++) { - const meta = parsedMeta.addApps[i]; - nextCuration.addApps.push({ - key: uuid(), - meta: meta, - }); - } nextCurations[index] = nextCuration; return { ...prevState, curations: nextCurations }; } @@ -78,7 +70,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.thumbnail = action.payload.image; nextCuration.thumbnail.version = prevCuration.thumbnail.version + 1; nextCurations[index] = nextCuration; @@ -89,7 +81,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.screenshot = action.payload.image; nextCuration.screenshot.version = prevCuration.screenshot.version + 1; nextCurations[index] = nextCuration; @@ -100,59 +92,11 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.content = action.payload.content; nextCurations[index] = nextCuration; return { ...prevState, curations: nextCurations }; } - // Add an empty additional application to a curation - case 'new-addapp': { - const nextCurations = [ ...prevState.curations ]; - const index = nextCurations.findIndex(c => c.key === action.payload.key); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - switch (action.payload.type) { - case 'normal': - nextCuration.addApps.push({ - key: uuid(), - meta: {} - }); - break; - case 'extras': - nextCuration.addApps.push({ - key: uuid(), - meta: generateExtrasAddApp('') - }); - break; - case 'message': - nextCuration.addApps.push({ - key: uuid(), - meta: generateMessageAddApp('') - }); - break; - } - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } - // Remove an additional application from a curation - case 'remove-addapp': { - const nextCurations = [ ...prevState.curations ]; - const index = nextCurations.findIndex(c => c.key === action.payload.curationKey); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - const addAppIndex = nextCuration.addApps.findIndex(c => c.key === action.payload.key); - if (addAppIndex >= 0) { - nextCuration.addApps.splice(addAppIndex, 1); - } - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } // Edit curation's meta case 'edit-curation-meta': { // Find the curation @@ -169,30 +113,6 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur } return { ...prevState, curations: nextCurations }; } - // Edit additional application's meta - case 'edit-addapp-meta': { - // Find the curation - const nextCurations = [ ...prevState.curations ]; // (New curations array to replace the current) - const index = nextCurations.findIndex(c => c.key === action.payload.curationKey); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - // Find the additional application - const addAppIndex = prevCuration.addApps.findIndex(c => c.key === action.payload.key); - if (addAppIndex >= 0) { - const prevAddApp = prevCuration.addApps[addAppIndex]; - const nextAddApp = { ...prevAddApp }; - // Replace the value (in the copied meta) - nextAddApp.meta[action.payload.property] = action.payload.value; - // Replace the previous additional application with the new (in the copied array) - nextCuration.addApps[addAppIndex] = nextAddApp; - } - // Replace the previous curation with the new (in the copied array) - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } // Sorts all curations A-Z case 'sort-curations': { const newCurations = [...prevState.curations].sort((a, b) => { @@ -263,7 +183,6 @@ export function createEditCuration(key: string): EditCuration { key: key, meta: {}, content: [], - addApps: [], thumbnail: createCurationIndexImage(), screenshot: createCurationIndexImage(), locked: false, @@ -307,16 +226,6 @@ export type CurationAction = ( key: string; content: IndexedContent[]; }> | - /** Add an empty additional application to curation */ - ReducerAction<'new-addapp', { - key: string; - type: 'normal' | 'extras' | 'message'; - }> | - /** Remove an additional application (by key) from a curation */ - ReducerAction<'remove-addapp', { - curationKey: string; - key: string; - }> | /** Edit the value of a curation's meta's property. */ ReducerAction<'edit-curation-meta', { /** Key of the curation to change. */ @@ -326,17 +235,6 @@ export type CurationAction = ( /** Value to set the property to. */ value: EditCurationMeta[keyof EditCurationMeta]; }> | - /** Edit the value of an additional application's meta's property. */ - ReducerAction<'edit-addapp-meta', { - /** Key of the curation the additional application belongs to. */ - curationKey: string; - /** Key of the additional application to change. */ - key: string; - /** Name of the property to change. */ - property: keyof EditAddAppCurationMeta; - /** Value to set the property to. */ - value: EditAddAppCurationMeta[keyof EditAddAppCurationMeta]; - }> | /** Sort Curations A-Z */ ReducerAction<'sort-curations', {}> | /** Change the lock status of a curation. */ diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 122f1ed90..946350b79 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -14,7 +14,7 @@ import { SocketTemplate } from '@shared/socket/types'; import { MessageBoxOptions, OpenDialogOptions, OpenExternalOptions, SaveDialogOptions } from 'electron'; import { GameData, TagAlias, TagFilterGroup } from 'flashpoint-launcher'; import { AppConfigData, AppExtConfigData } from '../config/interfaces'; -import { EditAddAppCuration, EditAddAppCurationMeta, EditCuration, EditCurationMeta } from '../curate/types'; +import { EditCuration, EditCurationMeta } from '../curate/types'; import { ExecMapping, GamePropSuggestions, IService, ProcessAction } from '../interfaces'; import { LangContainer, LangFile } from '../lang'; import { ILogEntry, ILogPreEntry, LogLevel } from '../Log/interface'; @@ -47,7 +47,7 @@ export enum BackIn { DELETE_GAME, DUPLICATE_GAME, EXPORT_GAME, - LAUNCH_ADDAPP, + LAUNCH_EXTRAS, SAVE_IMAGE, DELETE_IMAGE, ADD_LOG, @@ -67,7 +67,7 @@ export enum BackIn { SAVE_LEGACY_PLATFORM, IMPORT_CURATION, LAUNCH_CURATION, - LAUNCH_CURATION_ADDAPP, + LAUNCH_CURATION_EXTRAS, QUIT, // Sources @@ -199,7 +199,7 @@ export type BackInTemplate = SocketTemplate BrowseChangeData; [BackIn.DUPLICATE_GAME]: (id: string, dupeImages: boolean) => BrowseChangeData; [BackIn.EXPORT_GAME]: (id: string, location: string, metaOnly: boolean) => void; - [BackIn.LAUNCH_ADDAPP]: (id: string) => void; + [BackIn.LAUNCH_EXTRAS]: (id: string) => void; [BackIn.SAVE_IMAGE]: (folder: string, id: string, content: string) => void; [BackIn.DELETE_IMAGE]: (folder: string, id: string) => void; [BackIn.ADD_LOG]: (data: ILogPreEntry & { logLevel: LogLevel }) => void; @@ -219,7 +219,7 @@ export type BackInTemplate = SocketTemplate void; [BackIn.IMPORT_CURATION]: (data: ImportCurationData) => ImportCurationResponseData; [BackIn.LAUNCH_CURATION]: (data: LaunchCurationData) => void; - [BackIn.LAUNCH_CURATION_ADDAPP]: (data: LaunchCurationAddAppData) => void; + [BackIn.LAUNCH_CURATION_EXTRAS]: (data: LaunchCurationData) => void; [BackIn.QUIT]: () => void; // Tag funcs @@ -493,18 +493,10 @@ export type ImportCurationResponseData = { export type LaunchCurationData = { key: string; meta: EditCurationMeta; - addApps: EditAddAppCurationMeta[]; mad4fp: boolean; symlinkCurationContent: boolean; } -export type LaunchCurationAddAppData = { - curationKey: string; - curation: EditAddAppCuration; - platform?: string; - symlinkCurationContent: boolean; -} - export type TagSuggestion = { alias?: string; primaryAlias: string; diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index 61fc21321..f0dac9d71 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -1,7 +1,7 @@ import { Game } from '@database/entity/Game'; import { TagCategory } from '@database/entity/TagCategory'; import { ParsedCurationMeta } from './parse'; -import { EditAddAppCuration, EditCurationMeta } from './types'; +import { EditCurationMeta } from './types'; /** * Convert game and its additional applications into a raw object representation in the curation format. @@ -34,38 +34,10 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor parsed['Launch Command'] = game.launchCommand; parsed['Game Notes'] = game.notes; parsed['Original Description'] = game.originalDescription; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - for (let i = 0; i < game.addApps.length; i++) { - const addApp = game.addApps[i]; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.name; - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.name, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Parent Game ID'] = game.parentGameId; + parsed['Extras'] = game.extras; + parsed['Extras Name'] = game.extrasName; + parsed['Message'] = game.message; // Return return parsed; } @@ -75,7 +47,7 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor * @param curation Curation to convert. * @param addApps Additional applications of the curation. */ -export function convertEditToCurationMetaFile(curation: EditCurationMeta, categories: TagCategory[], addApps?: EditAddAppCuration[]): CurationMetaFile { +export function convertEditToCurationMetaFile(curation: EditCurationMeta, categories: TagCategory[]): CurationMetaFile { const parsed: CurationMetaFile = {}; const tagCategories = curation.tags ? curation.tags.map(t => { const cat = categories.find(c => c.id === t.categoryId); @@ -104,42 +76,10 @@ export function convertEditToCurationMetaFile(curation: EditCurationMeta, catego parsed['Original Description'] = curation.originalDescription; parsed['Curation Notes'] = curation.curationNotes; parsed['Mount Parameters'] = curation.mountParameters; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - if (addApps) { - for (let i = 0; i < addApps.length; i++) { - const addApp = addApps[i].meta; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.heading; - if (heading) { - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.heading, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Parent Game ID'] = curation.parentGameId; + parsed['Extras'] = curation.extras; + parsed['Extras Name'] = curation.extrasName; + parsed['Message'] = curation.message; // Return return parsed; } @@ -178,42 +118,10 @@ export function convertParsedToCurationMeta(curation: ParsedCurationMeta, catego parsed['Original Description'] = curation.game.originalDescription; parsed['Curation Notes'] = curation.game.curationNotes; parsed['Mount Parameters'] = curation.game.mountParameters; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - if (curation.addApps) { - for (let i = 0; i < curation.addApps.length; i++) { - const addApp = curation.addApps[i]; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.heading; - if (heading) { - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.heading, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Extras'] = curation.game.extras; + parsed['Extras Name'] = curation.game.extrasName; + parsed['Message'] = curation.game.message; + parsed['Parent Game ID'] = curation.game.parentGameId; // Return return parsed; } @@ -239,20 +147,10 @@ type CurationMetaFile = { 'Alternate Titles'?: string; 'Library'?: string; 'Version'?: string; - 'Additional Applications'?: CurationFormatAddApps; 'Curation Notes'?: string; 'Mount Parameters'?: string; -}; - -type CurationFormatAddApps = { - [key: string]: CurationFormatAddApp; -} & { 'Extras'?: string; + 'Extras Name'?: string; 'Message'?: string; -}; - -type CurationFormatAddApp = { - 'Application Path'?: string; - 'Heading'?: string; - 'Launch Command'?: string; -}; + 'Parent Game ID'?: string; +}; \ No newline at end of file diff --git a/src/shared/curate/parse.ts b/src/shared/curate/parse.ts index bd067ca0a..b9a1574ba 100644 --- a/src/shared/curate/parse.ts +++ b/src/shared/curate/parse.ts @@ -3,7 +3,7 @@ import { Coerce } from '@shared/utils/Coerce'; import { IObjectParserProp, ObjectParser } from '../utils/ObjectParser'; import { CurationFormatObject, parseCurationFormat } from './format/parser'; import { CFTokenizer, tokenizeCurationFormat } from './format/tokenizer'; -import { EditAddAppCurationMeta, EditCurationMeta } from './types'; +import { EditCurationMeta } from './types'; import { getTagsFromStr } from './util'; const { str } = Coerce; @@ -12,8 +12,6 @@ const { str } = Coerce; export type ParsedCurationMeta = { /** Meta data of the game. */ game: EditCurationMeta; - /** Meta data of the additional applications. */ - addApps: EditAddAppCurationMeta[]; }; /** @@ -49,7 +47,6 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) // Default parsed data const parsed: ParsedCurationMeta = { game: {}, - addApps: [], }; // Make sure it exists before calling Object.keys if (!data) { @@ -93,6 +90,10 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) parser.prop('version', v => parsed.game.version = str(v)); parser.prop('library', v => parsed.game.library = str(v).toLowerCase()); // must be lower case parser.prop('mount parameters', v => parsed.game.mountParameters = str(v)); + parser.prop('extras', v => parsed.game.extras = str(v)); + parser.prop('extras name', v => parsed.game.extrasName = str(v)); + parser.prop('message', v => parsed.game.message = str(v)); + parser.prop('parent game id', v => parsed.game.parentGameId = str(v)); if (lowerCaseData.genre) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.genre), str(lowerCaseData['tag categories'])); } if (lowerCaseData.genres) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.genres), str(lowerCaseData['tag categories'])); } if (lowerCaseData.tags) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.tags), str(lowerCaseData['tag categories'])); } @@ -106,37 +107,10 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) } // property aliases parser.prop('animation notes', v => parsed.game.notes = str(v)); - // Add-apps - parser.prop('additional applications').map((item, label, map) => { - parsed.addApps.push(convertAddApp(item, label, map[label])); - }); // Return return parsed; } -/** - * Convert a "raw" curation additional application meta object into a more programmer friendly object. - * @param item Object parser, wrapped around the "raw" add-app meta object to convert. - * @param label Label of the object. - */ -function convertAddApp(item: IObjectParserProp, label: string | number | symbol, rawValue: any): EditAddAppCurationMeta { - const addApp: EditAddAppCurationMeta = {}; - const labelStr = str(label); - switch (labelStr.toLowerCase()) { - case 'extras': // (Extras add-app) - return generateExtrasAddApp(str(rawValue)); - case 'message': // (Message add-app) - return generateMessageAddApp(str(rawValue)); - default: // (Normal add-app) - addApp.heading = labelStr; - item.prop('Heading', v => addApp.heading = str(v), true); - item.prop('Application Path', v => addApp.applicationPath = str(v)); - item.prop('Launch Command', v => addApp.launchCommand = str(v)); - break; - } - return addApp; -} - // Coerce an object into a sensible string function arrayStr(rawStr: any): string { if (Array.isArray(rawStr)) { @@ -145,19 +119,3 @@ function arrayStr(rawStr: any): string { } return str(rawStr); } - -export function generateExtrasAddApp(folderName: string) : EditAddAppCurationMeta { - return { - heading: 'Extras', - applicationPath: ':extras:', - launchCommand: folderName - }; -} - -export function generateMessageAddApp(message: string) : EditAddAppCurationMeta { - return { - heading: 'Message', - applicationPath: ':message:', - launchCommand: message - }; -} diff --git a/src/shared/curate/types.ts b/src/shared/curate/types.ts index 10c7c1886..247f8bcfc 100644 --- a/src/shared/curate/types.ts +++ b/src/shared/curate/types.ts @@ -7,8 +7,6 @@ export type EditCuration = { key: string; /** Meta data of the curation. */ meta: EditCurationMeta; - /** Keys of additional applications that belong to this game. */ - addApps: EditAddAppCuration[]; /** Data of each file in the content folder (and sub-folders). */ content: IndexedContent[]; /** Screenshot. */ @@ -23,18 +21,11 @@ export type EditCuration = { deleted: boolean; } -/** Data of an additional application curation in the curation importer. */ -export type EditAddAppCuration = { - /** Unique key of the curation (UUIDv4). Generated when loaded. */ - key: string; - /** Meta data of the curation. */ - meta: EditAddAppCurationMeta; -} - /** Meta data of a curation. */ export type EditCurationMeta = Partial<{ // Game fields title: string; + parentGameId?: string; alternateTitles: string; series: string; developer: string; @@ -55,13 +46,9 @@ export type EditCurationMeta = Partial<{ originalDescription: string; language: string; mountParameters: string; -}> - -/** Meta data of an additional application curation. */ -export type EditAddAppCurationMeta = Partial<{ - heading: string; - applicationPath: string; - launchCommand: string; + extras?: string; + extrasName?: string; + message?: string; }> export type CurationIndex = { diff --git a/src/shared/game/interfaces.ts b/src/shared/game/interfaces.ts index 7f10ffa14..add83bd5e 100644 --- a/src/shared/game/interfaces.ts +++ b/src/shared/game/interfaces.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '../../database/entity/AdditionalApp'; import { Game } from '../../database/entity/Game'; import { Playlist } from '../../database/entity/Playlist'; import { OrderGamesOpts } from './GameFilter'; @@ -39,12 +38,6 @@ export type GameAddRequest = { game: Game; } -/** Client Request - Add an additional application */ -export type AppAddRequest = { - /** Add App to add */ - addApp: AdditionalApp; -} - /** Client Request - Information needed to make a search */ export type SearchRequest = { /** String to use as a search query */ diff --git a/src/shared/game/util.ts b/src/shared/game/util.ts index 3344e912e..2c1096c0e 100644 --- a/src/shared/game/util.ts +++ b/src/shared/game/util.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '../../database/entity/AdditionalApp'; import { Game } from '../../database/entity/Game'; export namespace ModelUtils { @@ -30,22 +29,10 @@ export namespace ModelUtils { language: '', library: '', orderTitle: '', - addApps: [], + children: [], placeholder: false, activeDataOnDisk: false }); return game; } - - export function createAddApp(game: Game): AdditionalApp { - return { - id: '', - parentGame: game, - applicationPath: '', - autoRunBefore: false, - launchCommand: '', - name: '', - waitForExit: false, - }; - } } diff --git a/src/shared/lang.ts b/src/shared/lang.ts index dc0354671..ff515109c 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -314,6 +314,12 @@ const langTemplate = { 'mountParameters', 'noMountParameters', 'showExtremeScreenshot', + 'extras', + 'noExtras', + 'message', + 'noMessage', + 'extrasName', + 'noExtrasName', ] as const, tags: [ 'name', diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 3164ffafe..201088420 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -457,6 +457,8 @@ declare module 'flashpoint-launcher' { id: string; /** ID of the game which owns this game */ parentGameId?: string; + /** A list of child games. */ + children: Game[]; /** Full title of the game */ title: string; /** Any alternate titles to match against search */ @@ -503,8 +505,6 @@ declare module 'flashpoint-launcher' { language: string; /** Library this game belongs to */ library: string; - /** All attached Additional Apps of a game */ - addApps: AdditionalApp[]; /** Unused */ orderTitle: string; /** If the game is a placeholder (and can therefore not be saved) */ @@ -513,6 +513,13 @@ declare module 'flashpoint-launcher' { activeDataId?: number; /** Whether the data is present on disk */ activeDataOnDisk: boolean; + /** The path to any extras. */ + extras?: string; + /** The name to be displayed for those extras. */ + extrasName?: string; + /** The message to display when the game starts. */ + message?: string; + data?: GameData[]; updateTagsStr: () => void; }; From 65cce7cf0372415bff9af47621a9579630079a01 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 20 Mar 2022 22:26:18 -0400 Subject: [PATCH 03/83] Fix silly database mistake. --- src/back/game/GameManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 771b893e4..d51fb1d61 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -124,13 +124,14 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // console.log('FindGamePageKeyset:'); const subQ = await getGameQuery('sub', filterOpts, orderBy, direction); - subQ.select(`sub.${orderBy}, sub.title, sub.id, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); + subQ.select(`sub.${orderBy}, sub.title, sub.id, sub.parentGameId, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); subQ.orderBy(`sub.${orderBy} ${direction}, sub.title`, direction); let query = getManager().createQueryBuilder() .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') + .andWhere('g.parentGameId is null') .setParameters(subQ.getParameters()); if (searchLimit) { From 5c0256b0e4b384174bc620a8a50dc000776b8a4b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Fri, 25 Mar 2022 07:07:03 -0400 Subject: [PATCH 04/83] Searching and launching broken, but browsing works now. --- src/back/game/GameManager.ts | 55 +++++++++++++++++++++--------------- src/back/responses.ts | 3 ++ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index d51fb1d61..e6c10dd3c 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -17,7 +17,7 @@ import * as path from 'path'; import * as TagManager from './TagManager'; import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; -import { isNull } from 'util'; +import { isNull, isNullOrUndefined } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -53,13 +53,14 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom /** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } + log.debug('GameManager', 'findGameRow'); // const startTime = Date.now(); const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) - .where("game.parentGameId is null"); + .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) + .where("game.parentGameId IS NULL"); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); @@ -74,7 +75,9 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o .setParameters(subQ.getParameters()) .select('row_num') .from('(' + subQ.getQuery() + ')', 'g') - .where('g.id = :gameId', { gameId: gameId }); + .where('g.id = :gameId', { gameId: gameId }) + // Shouldn't be needed, but doing it anyway. + .andWhere('g.parentGameId IS NULL'); const raw = await query.getRawOne(); // console.log(`${Date.now() - startTime}ms for row`); @@ -92,7 +95,7 @@ export async function findRandomGames(count: number, broken: boolean, excludedLi const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); - query.where("game.parentGameId is null"); + query.where("game.parentGameId IS NULL"); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -165,6 +168,15 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga } // console.log(` Count: ${Date.now() - startTime}ms`); + let i = 0; + while (i < total) { + if (keyset[i]) { + // @ts-ignore + log.debug('GameManager', keyset[i].title); + i = total; + } + i++; + } return { keyset, @@ -191,7 +203,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const ranges = opts.ranges || [{ start: 0, length: undefined }]; const rangesOut: ResponseGameRange[] = []; - // console.log('FindGames:'); + console.log('FindGames:'); let query: SelectQueryBuilder | undefined; for (let i = 0; i < ranges.length; i++) { @@ -199,7 +211,6 @@ export async function findGames(opts: FindGamesOpts, shallow: const range = ranges[i]; query = await getGameQuery('game', opts.filter, opts.orderBy, opts.direction, range.start, range.length, range.index); - query.where("game.parentGameId is null"); // Select games // @TODO Make it infer the type of T from the value of "shallow", and then use that to make "games" get the correct type, somehow? @@ -207,6 +218,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const games = (shallow) ? (await query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.extreme, game.tagsStr').getRawMany()) as ViewGame[] : await query.getMany(); + rangesOut.push({ start: range.start, length: range.length, @@ -222,26 +234,13 @@ export async function findGames(opts: FindGamesOpts, shallow: return rangesOut; } -/** Find an add app with the specified ID. */ -export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { - if (id || filter) { - if (!filter) { - filter = { - relations: ['parentGameId'] - }; - } - const addAppRepository = getManager().getRepository(Game); - return addAppRepository.findOne(id, filter); - } -} - export async function findPlatformAppPaths(platform: string): Promise { const gameRepository = getManager().getRepository(Game); const values = await gameRepository.createQueryBuilder('game') .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) - .andWhere("game.parentGameId is null") + .andWhere("game.parentGameId IS NULL") .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -274,7 +273,6 @@ export async function findPlatforms(library: string): Promise { const gameRepository = getManager().getRepository(Game); const libraries = await gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) - .andWhere("game.parentGameId is null") .select('game.platform') .distinct() .getRawMany(); @@ -450,7 +448,7 @@ async function chunkedFindByIds(gameIds: string[]): Promise { const chunks = chunkArray(gameIds, 100); let gamesFound: Game[] = []; for (const chunk of chunks) { - gamesFound = gamesFound.concat(await gameRepository.findByIds(chunk)); + gamesFound = gamesFound.concat(await gameRepository.findByIds(chunk, {parentGameId: IsNull()})); } return gamesFound; @@ -477,10 +475,12 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi } for (const phrase of searchQuery.genericWhitelist) { doWhereTitle(alias, query, phrase, whereCount, true); + log.debug("GameManager", `Whitelist title string: ${phrase}`); whereCount++; } for (const phrase of searchQuery.genericBlacklist) { doWhereTitle(alias, query, phrase, whereCount, false); + log.debug("GameManager", `Blacklist title string: ${phrase}`); whereCount++; } } @@ -586,6 +586,7 @@ async function getGameQuery( alias: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, offset?: number, limit?: number, index?: PageTuple ): Promise> { validateSqlName(alias); + log.debug('GameManager', 'getGameQuery'); if (orderBy) { validateSqlName(orderBy); } if (direction) { validateSqlOrder(direction); } @@ -600,6 +601,12 @@ async function getGameQuery( query.where(`(${alias}.${orderBy}, ${alias}.title, ${alias}.id) ${comparator} (:orderVal, :title, :id)`, { orderVal: index.orderVal, title: index.title, id: index.id }); whereCount++; } + if (whereCount === 0) { + query.where('parentGameId IS NULL'); + } else { + query.andWhere('parentGameId IS NULL'); + } + whereCount++; // Apply all flat game filters if (filterOpts) { whereCount = applyFlatGameFilters(alias, query, filterOpts, whereCount); @@ -615,8 +622,10 @@ async function getGameQuery( query.orderBy('pg.order', 'ASC'); if (whereCount === 0) { query.where('pg.playlistId = :playlistId', { playlistId: filterOpts.playlistId }); } else { query.andWhere('pg.playlistId = :playlistId', { playlistId: filterOpts.playlistId }); } + whereCount++; query.skip(offset); // TODO: Why doesn't offset work here? } + // Tag filtering if (filterOpts && filterOpts.searchQuery) { const aliasWhitelist = filterOpts.searchQuery.whitelist.filter(f => f.field === 'tag').map(f => f.value); diff --git a/src/back/responses.ts b/src/back/responses.ts index 2c6e03dcf..add28a21c 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -225,6 +225,9 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); + state.socketServer.registerAny((event, type, args) => { + log.debug('Responses', BackIn[type]); + }); // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id); From 019ea4e2dd61aeb8cfef0622ef925a77c18cc3e5 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 29 Mar 2022 19:11:53 -0400 Subject: [PATCH 05/83] Fix the extensions API. --- src/back/extensions/ApiImplementation.ts | 18 +-------------- src/back/types.ts | 4 ---- typings/flashpoint-launcher.d.ts | 29 ++---------------------- 3 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index aefd5af3c..de6036501 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -143,7 +143,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, updateGame: GameManager.save, updateGames: GameManager.updateGames, // Ardil TODO - removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), + removeGameAndChildren: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); return game.tagsStr.split(';').findIndex(t => extremeTags.includes(t.trim())) !== -1; @@ -157,31 +157,15 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, get onWillLaunchGame() { return apiEmitters.games.onWillLaunchGame.event; }, - // Ardil TODO remove - get onWillLaunchAddApp() { - return apiEmitters.games.onWillLaunchAddApp.event; - }, get onWillLaunchCurationGame() { return apiEmitters.games.onWillLaunchCurationGame.event; }, - // Ardil TODO remove - get onWillLaunchCurationAddApp() { - return apiEmitters.games.onWillLaunchCurationAddApp.event; - }, get onDidLaunchGame() { return apiEmitters.games.onDidLaunchGame.event; }, - // Ardil TODO remove - get onDidLaunchAddApp() { - return apiEmitters.games.onDidLaunchAddApp.event; - }, get onDidLaunchCurationGame() { return apiEmitters.games.onDidLaunchCurationGame.event; }, - // Ardil TODO remove - get onDidLaunchCurationAddApp() { - return apiEmitters.games.onDidLaunchCurationAddApp.event; - }, get onDidUpdateGame() { return apiEmitters.games.onDidUpdateGame.event; }, diff --git a/src/back/types.ts b/src/back/types.ts index 5caa67b3e..5fc342b62 100644 --- a/src/back/types.ts +++ b/src/back/types.ts @@ -163,14 +163,10 @@ export type ApiEmittersState = Readonly<{ onLog: ApiEmitter; games: Readonly<{ onWillLaunchGame: ApiEmitter; - onWillLaunchAddApp: ApiEmitter; onWillLaunchCurationGame: ApiEmitter; - onWillLaunchCurationAddApp: ApiEmitter; onWillUninstallGameData: ApiEmitter; onDidLaunchGame: ApiEmitter; - onDidLaunchAddApp: ApiEmitter; onDidLaunchCurationGame: ApiEmitter; - onDidLaunchCurationAddApp: ApiEmitter; onDidUpdateGame: ApiEmitter<{oldGame: flashpoint.Game, newGame: flashpoint.Game}>; onDidRemoveGame: ApiEmitter; onDidUpdatePlaylist: ApiEmitter<{oldPlaylist: flashpoint.Playlist, newPlaylist: flashpoint.Playlist}>; diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 201088420..8a1f0637a 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -188,7 +188,7 @@ declare module 'flashpoint-launcher' { * Removes a Game and all its AddApps * @param gameId ID of Game to remove */ - function removeGameAndAddApps(gameId: string): Promise; + function removeGameAndChildren(gameId: string): Promise; // Misc /** @@ -210,14 +210,10 @@ declare module 'flashpoint-launcher' { // Events const onWillLaunchGame: Event; - const onWillLaunchAddApp: Event; const onWillLaunchCurationGame: Event; - const onWillLaunchCurationAddApp: Event; const onWillUninstallGameData: Event; const onDidLaunchGame: Event; - const onDidLaunchAddApp: Event; const onDidLaunchCurationGame: Event; - const onDidLaunchCurationAddApp: Event; const onDidInstallGameData: Event; const onDidUninstallGameData: Event; @@ -573,28 +569,7 @@ declare module 'flashpoint-launcher' { lastUpdated: Date; /** Any data provided by this Source */ data?: SourceData[]; - } - - type AdditionalApp = { - /** ID of the additional application (unique identifier) */ - id: string; - /** Path to the application that runs the additional application */ - applicationPath: string; - /** - * If the additional application should run before the game. - * (If true, this will always run when the game is launched) - * (If false, this will only run when specifically launched) - */ - autoRunBefore: boolean; - /** Command line argument(s) passed to the application to launch the game */ - launchCommand: string; - /** Name of the additional application */ - name: string; - /** Wait for this to exit before the Game will launch (if starting before launch) */ - waitForExit: boolean; - /** Parent of this add app */ - parentGame: Game; - }; + } type Tag = { /** ID of the tag (unique identifier) */ From 63b08b99968a573a4a7fbbd11762cb526dcd05b0 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:24:17 -0400 Subject: [PATCH 06/83] Update deps, add migration. --- package.json | 2 +- src/back/game/GameManager.ts | 16 +--------------- src/database/entity/Game.ts | 1 - .../migration/1648251821422-ChildCurations.ts | 19 +++++++++++++++++++ 4 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 src/database/migration/1648251821422-ChildCurations.ts diff --git a/package.json b/package.json index 35f72016d..93e38fcdf 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", - "sqlite3": "4.2.0", + "sqlite3": "^5.0.2", "tail": "2.0.3", "tree-kill": "1.2.2", "typeorm": "0.2.37", diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index e6c10dd3c..b4255bdd5 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -53,7 +53,6 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom /** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } - log.debug('GameManager', 'findGameRow'); // const startTime = Date.now(); const gameRepository = getManager().getRepository(Game); @@ -134,7 +133,6 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') - .andWhere('g.parentGameId is null') .setParameters(subQ.getParameters()); if (searchLimit) { @@ -168,15 +166,6 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga } // console.log(` Count: ${Date.now() - startTime}ms`); - let i = 0; - while (i < total) { - if (keyset[i]) { - // @ts-ignore - log.debug('GameManager', keyset[i].title); - i = total; - } - i++; - } return { keyset, @@ -203,7 +192,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const ranges = opts.ranges || [{ start: 0, length: undefined }]; const rangesOut: ResponseGameRange[] = []; - console.log('FindGames:'); + // console.log('FindGames:'); let query: SelectQueryBuilder | undefined; for (let i = 0; i < ranges.length; i++) { @@ -475,12 +464,10 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi } for (const phrase of searchQuery.genericWhitelist) { doWhereTitle(alias, query, phrase, whereCount, true); - log.debug("GameManager", `Whitelist title string: ${phrase}`); whereCount++; } for (const phrase of searchQuery.genericBlacklist) { doWhereTitle(alias, query, phrase, whereCount, false); - log.debug("GameManager", `Blacklist title string: ${phrase}`); whereCount++; } } @@ -586,7 +573,6 @@ async function getGameQuery( alias: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, offset?: number, limit?: number, index?: PageTuple ): Promise> { validateSqlName(alias); - log.debug('GameManager', 'getGameQuery'); if (orderBy) { validateSqlName(orderBy); } if (direction) { validateSqlOrder(direction); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 987b04cc3..bb4be1dad 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -23,7 +23,6 @@ export class Game { parentGameId?: string; @OneToMany(type => Game, game => game.parentGame, { - cascade: true, eager: true }) children: Game[]; diff --git a/src/database/migration/1648251821422-ChildCurations.ts b/src/database/migration/1648251821422-ChildCurations.ts new file mode 100644 index 000000000..865d02d92 --- /dev/null +++ b/src/database/migration/1648251821422-ChildCurations.ts @@ -0,0 +1,19 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class ChildCurations1648251821422 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE game ADD extras varchar`, undefined); + await queryRunner.query(`ALTER TABLE game ADD extrasName varchar`, undefined); + await queryRunner.query(`ALTER TABLE game ADD message varchar`, undefined); + await queryRunner.query(`UPDATE game SET message = a.launchCommand FROM additional_app a WHERE game.id=a.parentGameId AND a.applicationPath=':message:'`); + await queryRunner.query(`UPDATE game SET extras = a.launchCommand, extrasName = a.name FROM additional_app a WHERE game.id = a.parentGameId AND a.applicationPath = ':extras:'`, undefined); + await queryRunner.query(`UPDATE game SET parentGameId = NULL WHERE id IS parentGameId`, undefined); + await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"0000-00-00 00:00:00.000" AS dateAdded,"0000-00-00 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); + await queryRunner.query(`DROP TABLE additional_app`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} From 03e069ab5357fa4a749799a9b6896a29cfd16be3 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:25:24 -0400 Subject: [PATCH 07/83] Add migration at database init. --- src/back/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/back/index.ts b/src/back/index.ts index eac042842..8ad2b07f2 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -16,6 +16,7 @@ import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-So import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; +import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; import { BackIn, BackInit, BackInitArgs, BackOut } from '@shared/back/types'; import { ILogoSet, LogoSet } from '@shared/extensions/interfaces'; import { IBackProcessInfo, RecursivePartial } from '@shared/interfaces'; @@ -312,7 +313,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, - SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109] + SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109, ChildCurations1648251821422] }; state.connection = await createConnection(options); // TypeORM forces on but breaks Playlist Game links to unimported games From 87c445a1eac9b0b41a762eb454e996de92e2bc74 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:55:37 -0400 Subject: [PATCH 08/83] Fix child-loading, make children[] nullable, add option not to fetch children, fix a few typos, and a few small bugfixes. --- src/back/game/GameManager.ts | 25 +++++++++++++--- src/back/importGame.ts | 4 +-- src/back/responses.ts | 30 +++++++++++-------- src/back/util/misc.ts | 4 +-- src/database/entity/Game.ts | 11 ++++--- .../components/RightBrowseSidebar.tsx | 3 +- .../components/RightBrowseSidebarExtra.tsx | 2 +- typings/flashpoint-launcher.d.ts | 4 ++- 8 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index b4255bdd5..e096fe4c3 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -40,12 +40,27 @@ export async function countGames(): Promise { } /** Find the game with the specified ID. Ardil TODO find refs*/ -export async function findGame(id?: string, filter?: FindOneOptions): Promise { +export async function findGame(id?: string, filter?: FindOneOptions, noChildren?: boolean): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); const game = await gameRepository.findOne(id, filter); + // Only fetch the children if the game exists, the caller didn't ask us not to, and it's not a child itself. + // This enforces the no-multiple-generations rule. + if (game && !noChildren && !game.parentGameId) { + game.children = await gameRepository.createQueryBuilder() + .relation("children") + .of(game) + .loadMany(); + } if (game) { - game.tags.sort(tagSort); + if (game.tags) { + game.tags.sort(tagSort); + } + // Not sure why the standard "if (game.children)" wasn't working here, but it wasn't. + // Sort the child games. It's probably a good idea. + if (game.children !== undefined && game.children !== null && game.children.length > 1) { + game.children.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + } } return game; } @@ -302,8 +317,10 @@ export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: } // Delete children // Ardil TODO do Seirade's suggestion. - for (const child of game.children) { - await gameRepository.remove(child); + if (game.children) { + for (const child of game.children) { + await gameRepository.remove(child); + } } // Delete Game await gameRepository.remove(game); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 9068cb822..6a28f9e40 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -71,7 +71,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { where: { launchCommand: curation.meta.launchCommand } - }); + }, true); if (existingGame) { // Warn user of possible duplicate const response = await opts.openDialog({ @@ -94,7 +94,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); - const oldGame = await GameManager.findGame(gameId); + const oldGame = await GameManager.findGame(gameId, undefined, true); if (oldGame) { const response = await opts.openDialog({ title: 'Overwriting Game', diff --git a/src/back/responses.ts b/src/back/responses.ts index add28a21c..653edb5a9 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -206,7 +206,7 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game && game.extras) { await GameLauncher.launchExtras({ extrasPath: game.extras, @@ -225,19 +225,18 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); - state.socketServer.registerAny((event, type, args) => { - log.debug('Responses', BackIn[type]); - }); // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { - // Ardil TODO not needed? Temp fix, see if it happens. if (game.parentGameId && !game.parentGame) { log.debug("Game Launcher", "Fetching parent game."); - game.parentGame = await GameManager.findGame(game.parentGameId) + // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. + game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } + // Ensure that the children is an array. Also enforce the no-multiple-generations rule. + //game.children = game.parentGameId ? [] : await game.children; // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -489,10 +488,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO + // Ardil TODO ensure that we really don't need children for this. state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { // Save to file try { @@ -524,7 +523,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { - return GameManager.findGame(id); + return await GameManager.findGame(id); }); // Ardil TODO @@ -660,7 +659,8 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { - return GameManager.findAllGames(); + let games: Game[] = await GameManager.findAllGames(); + return games; }); // Ardil TODO @@ -959,6 +959,10 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.register(BackIn.GET_PLAYLIST_GAME, async (event, playlistId, gameId) => { const playlistGame = await GameManager.findPlaylistGame(playlistId, gameId); + if (playlistGame && playlistGame.game) { + // Ensure that the children is an array. Also enforce the no-multiple-generations rule. + //playlistGame.game.children = playlistGame.game.parentGameId ? [] : await playlistGame.game.children; + } return playlistGame; }); @@ -985,7 +989,7 @@ export function registerRequestCallbacks(state: BackState): void { for (const game of platform.collection.games) { const addApps = platform.collection.additionalApplications.filter(a => a.gameId === game.id); const translatedGame = await createGameFromLegacy(game, tagCache); - translatedGame.children = createChildFromFromLegacyAddApp(addApps, translatedGame); + translatedGames.push(...createChildFromFromLegacyAddApp(addApps, translatedGame)); translatedGames.push(translatedGame); } await GameManager.updateGames(translatedGames); @@ -1220,7 +1224,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { const meta: MetaEditMeta = { id: game.id, diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index fb739ec7f..63b71db9a 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -179,6 +179,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli applicationPath: addApp.applicationPath, launchCommand: addApp.launchCommand, parentGame: game, + parentGameId: game.id, library: game.library, alternateTitles: "", series: "", @@ -203,8 +204,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli tags: [], extras: undefined, extrasName: undefined, - message: undefined, - children: [] + message: undefined }) retVal.push(newGame); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index bb4be1dad..0e863c126 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,4 +1,4 @@ -import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Tree, UpdateDateColumn } from 'typeorm'; import { GameData } from './GameData'; import { Tag } from './Tag'; @@ -16,16 +16,15 @@ export class Game { /** ID of the game (unique identifier) */ id: string; - @ManyToOne(type => Game, game => game.children) + @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; @Column({ nullable: true }) parentGameId?: string; - @OneToMany(type => Game, game => game.parentGame, { - eager: true - }) - children: Game[]; + // Careful: potential infinite loop here. DO NOT eager-load this. + @OneToMany((type) => Game, (game) => game.parentGame) + children?: Game[]; @Column({collation: 'NOCASE'}) @Index('IDX_gameTitle') diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index c035e15f7..2dcddf72b 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -953,8 +953,9 @@ export class RightBrowseSidebar extends React.Component addApp.id === childId); if (index === -1) { throw new Error('Cant remove additional application because it was not found.'); } newChildren.splice(index, 1); - this.props.onEditGame({children: newChildren}); this.props.onDeleteGame(childId); + // @TODO make this better. + this.props.onEditGame({children:newChildren}); } } diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index f19c733ff..6b7626797 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -9,7 +9,7 @@ import { OpenIcon } from './OpenIcon'; export type RightBrowseSidebarExtraProps = { /** Extras to show and edit */ - // These two are xplicitly non-nullable. + // These two are explicitly non-nullable. extrasPath: string; extrasName: string; game: Game; diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 8a1f0637a..a8b530598 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -451,10 +451,12 @@ declare module 'flashpoint-launcher' { type Game = { /** ID of the game (unique identifier) */ id: string; + /** This game's parent game. */ + parentGame?: Game; /** ID of the game which owns this game */ parentGameId?: string; /** A list of child games. */ - children: Game[]; + children?: Game[]; /** Full title of the game */ title: string; /** Any alternate titles to match against search */ From 2f10d62fba08d77dd3920f559246f1597f1e54d4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:57:28 -0400 Subject: [PATCH 09/83] perf: don't fetch the game twice. --- src/back/MetaEdit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/back/MetaEdit.ts b/src/back/MetaEdit.ts index 9e708cc8d..eebffad61 100644 --- a/src/back/MetaEdit.ts +++ b/src/back/MetaEdit.ts @@ -171,7 +171,7 @@ export async function importAllMetaEdits(fullMetaEditsFolderPath: string, openDi const game = await GameManager.findGame(id); if (game) { - games[id] = await GameManager.findGame(id); + games[id] = game; } else { // Game not found const combined = combinedMetas[id]; if (!combined) { throw new Error(`Failed to check for collisions. "combined meta" is missing (id: "${id}") (bug)`); } From c64d1f71c6458f6e5a730e1c8e2b54a62346b462 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:59:16 -0400 Subject: [PATCH 10/83] build: switch to better-sqlite3 I was getting meh performance with sqlite3, and I heard better-sqlite3 was faster. It seems to be so. --- ormconfig.json | 4 ++-- package.json | 2 +- src/back/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ormconfig.json b/ormconfig.json index 45a4124b7..beef81b3d 100644 --- a/ormconfig.json +++ b/ormconfig.json @@ -1,5 +1,5 @@ { - "type": "sqlite", + "type": "better-sqlite3", "host": "localhost", "port": 3306, "username": "flashpoint", @@ -22,4 +22,4 @@ "migrationsDir": "src/database/migration", "subscribersDir": "src/database/subscriber" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 93e38fcdf..278934465 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "axios": "0.21.4", + "better-sqlite3": "^7.5.0", "connected-react-router": "6.8.0", "electron-updater": "4.3.1", "electron-util": "0.14.2", @@ -56,7 +57,6 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", - "sqlite3": "^5.0.2", "tail": "2.0.3", "tree-kill": "1.2.2", "typeorm": "0.2.37", diff --git a/src/back/index.ts b/src/back/index.ts index 8ad2b07f2..2473b64c1 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -35,7 +35,7 @@ import * as mime from 'mime'; import * as path from 'path'; import 'reflect-metadata'; // Required for the DB Models to function -import 'sqlite3'; +import 'better-sqlite3'; import { Tail } from 'tail'; import { ConnectionOptions, createConnection } from 'typeorm'; import { ConfigFile } from './ConfigFile'; @@ -309,7 +309,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { // Setup DB if (!state.connection) { const options: ConnectionOptions = { - type: 'sqlite', + type: 'better-sqlite3', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, From 3a01d1d88a0596db56ca659c3e06b6ca84d82349 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 20:16:59 -0400 Subject: [PATCH 11/83] Fix comment for removeGameAndChildren() --- typings/flashpoint-launcher.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index a8b530598..6779f6d0c 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -185,7 +185,7 @@ declare module 'flashpoint-launcher' { */ function updateGames(games: Game[]): Promise; /** - * Removes a Game and all its AddApps + * Removes a Game and all its children * @param gameId ID of Game to remove */ function removeGameAndChildren(gameId: string): Promise; From bba3e862168692e176ad2374abe0e6f6570a5460 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Fri, 1 Apr 2022 12:26:39 -0400 Subject: [PATCH 12/83] Fix extras. --- src/back/GameLauncher.ts | 2 +- src/back/responses.ts | 1 - .../components/RightBrowseSidebar.tsx | 26 +++++++++++++++++-- .../components/RightBrowseSidebarExtra.tsx | 19 +------------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index 74a2ae64d..5b09e15a1 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -59,7 +59,7 @@ export namespace GameLauncher { export async function launchExtras(opts: LaunchExtrasOpts): Promise { const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.extrasPath))); - return opts.openExternal(folderPath, { activate: true }) + return opts.openExternal(`file://${folderPath}`, { activate: true }) .catch(error => { if (error) { opts.openDialog({ diff --git a/src/back/responses.ts b/src/back/responses.ts index 653edb5a9..4c1441b27 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -521,7 +521,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { return await GameManager.findGame(id); }); diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 2dcddf72b..80bc7b516 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -113,6 +113,8 @@ export class RightBrowseSidebar extends React.Component this.props.onEditGame({ notes: text })); onOriginalDescriptionChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ originalDescription: text })); onMessageChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ message: text })); + onExtrasChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ extras: text})); + onExtrasNameChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ extrasName: text})); onBrokenChange = this.wrapOnCheckBoxChange(game => { if (this.props.currentGame) { this.props.onEditGame({ broken: !this.props.currentGame.broken }); @@ -516,8 +518,28 @@ export class RightBrowseSidebar extends React.Component -
+
: undefined} +
+

{strings.extrasName}:

+ +
+
+

{strings.extras}:

+ +

{strings.dateAdded}:

) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) ? ( + { editable || (currentChildren && currentChildren.length > 0) || game.extras ? (

{strings.additionalApplications}:

diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index 6b7626797..9c0527509 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -29,9 +29,6 @@ export interface RightBrowseSidebarExtra { /** Displays an additional application for a game in the right sidebar of BrowsePage. */ export class RightBrowseSidebarExtra extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); - onExtrasNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); - onExtrasPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); render() { const allStrings = this.context; @@ -44,27 +41,13 @@ export class RightBrowseSidebarExtra extends React.Component + editable={false} />
- { editDisabled ? undefined : ( - <> - {/* Launch Command */} -
-

{strings.extras}:

- -
- - ) }
); } From 1ad82e785d2662c859fd590c73d91ac4470fedbd Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 10:25:48 -0400 Subject: [PATCH 13/83] Make the linter happy, also fix curation. That is: I hope so. I haven't tested curation yet, but I *think* this will fix some portions of it. --- src/back/GameLauncher.ts | 40 ++---------- src/back/game/GameManager.ts | 42 +++++++++--- src/back/importGame.ts | 34 ++++++++-- src/back/responses.ts | 26 +++----- src/back/util/misc.ts | 39 ++++++----- src/database/entity/Game.ts | 2 +- src/renderer/components/CurateBox.tsx | 50 ++++++++++++--- src/renderer/components/CurateBoxWarnings.tsx | 2 + .../components/RightBrowseSidebar.tsx | 43 ++++++------- .../components/RightBrowseSidebarAddApp.tsx | 2 - .../components/RightBrowseSidebarExtra.tsx | 64 +------------------ src/renderer/components/pages/BrowsePage.tsx | 2 +- .../components/pages/DeveloperPage.tsx | 2 +- src/renderer/context/CurationContext.ts | 1 - src/shared/back/types.ts | 2 +- src/shared/curate/metaToMeta.ts | 2 +- src/shared/curate/parse.ts | 2 +- src/shared/lang.ts | 3 + 18 files changed, 168 insertions(+), 190 deletions(-) diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index 5b09e15a1..e5b55bd8c 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -1,11 +1,9 @@ import { Game } from '@database/entity/Game'; import { AppProvider } from '@shared/extensions/interfaces'; -import { ExecMapping, Omit } from '@shared/interfaces'; +import { ExecMapping } from '@shared/interfaces'; import { LangContainer } from '@shared/lang'; -import { fixSlashes, padStart, stringifyArray } from '@shared/Util'; +import { fixSlashes } from '@shared/Util'; import { Coerce } from '@shared/utils/Coerce'; -import { ChildProcess, exec } from 'child_process'; -import { EventEmitter } from 'events'; import { AppPathOverride, GameData, ManagedChildProcess } from 'flashpoint-launcher'; import * as path from 'path'; import { ApiEmitter } from './extensions/ApiEmitter'; @@ -81,9 +79,9 @@ export namespace GameLauncher { // Abort if placeholder (placeholders are not "actual" games) if (opts.game.placeholder) { return; } if (opts.game.message) { - await opts.openDialog({type: 'info', - title: 'About This Game', - message: opts.game.message, + await opts.openDialog({type: 'info', + title: 'About This Game', + message: opts.game.message, buttons: ['Ok'], }); } @@ -300,34 +298,6 @@ export namespace GameLauncher { throw Error('Unsupported platform'); } } - - function logProcessOutput(proc: ChildProcess): void { - // Log for debugging purposes - // (might be a bad idea to fill the console with junk?) - const logInfo = (event: string, args: any[]): void => { - log.info(logSource, `${event} (PID: ${padStart(proc.pid, 5)}) ${stringifyArray(args, stringifyArrayOpts)}`); - }; - const logErr = (event: string, args: any[]): void => { - log.error(logSource, `${event} (PID: ${padStart(proc.pid, 5)}) ${stringifyArray(args, stringifyArrayOpts)}`); - }; - registerEventListeners(proc, [/* 'close', */ 'disconnect', 'exit', 'message'], logInfo); - registerEventListeners(proc, ['error'], logErr); - if (proc.stdout) { proc.stdout.on('data', (data) => { logInfo('stdout', [data.toString('utf8')]); }); } - if (proc.stderr) { proc.stderr.on('data', (data) => { logErr('stderr', [data.toString('utf8')]); }); } - } -} - -const stringifyArrayOpts = { - trimStrings: true, -}; - -function registerEventListeners(emitter: EventEmitter, events: string[], callback: (event: string, args: any[]) => void): void { - for (let i = 0; i < events.length; i++) { - const e: string = events[i]; - emitter.on(e, (...args: any[]) => { - callback(e, args); - }); - } } /** diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index e096fe4c3..39475b659 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -17,7 +17,6 @@ import * as path from 'path'; import * as TagManager from './TagManager'; import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; -import { isNull, isNullOrUndefined } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -48,7 +47,7 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi // This enforces the no-multiple-generations rule. if (game && !noChildren && !game.parentGameId) { game.children = await gameRepository.createQueryBuilder() - .relation("children") + .relation('children') .of(game) .loadMany(); } @@ -74,7 +73,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const subQ = gameRepository.createQueryBuilder('game') .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) - .where("game.parentGameId IS NULL"); + .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); @@ -109,7 +108,7 @@ export async function findRandomGames(count: number, broken: boolean, excludedLi const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); - query.where("game.parentGameId IS NULL"); + query.where('game.parentGameId IS NULL'); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -222,7 +221,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const games = (shallow) ? (await query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.extreme, game.tagsStr').getRawMany()) as ViewGame[] : await query.getMany(); - + rangesOut.push({ start: range.start, length: range.length, @@ -244,7 +243,7 @@ export async function findPlatformAppPaths(platform: string): Promise .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) - .andWhere("game.parentGameId IS NULL") + .andWhere('game.parentGameId IS NULL') .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -288,6 +287,19 @@ export async function updateGames(games: Game[]): Promise { for (const chunk of chunks) { await getManager().transaction(async transEntityManager => { for (const game of chunk) { + // Set nullable properties to null if they're empty. + if (game.parentGameId === '') { + game.parentGameId = undefined; + } + if (game.extras === '') { + game.extras = undefined; + } + if (game.extrasName === '') { + game.extrasName = undefined; + } + if (game.message === '') { + game.message = undefined; + } await transEntityManager.save(Game, game); } }); @@ -296,6 +308,19 @@ export async function updateGames(games: Game[]): Promise { export async function save(game: Game): Promise { const gameRepository = getManager().getRepository(Game); + // Set nullable properties to null if they're empty. + if (game.parentGameId === '') { + game.parentGameId = undefined; + } + if (game.extras === '') { + game.extras = undefined; + } + if (game.extrasName === '') { + game.extrasName = undefined; + } + if (game.message === '') { + game.message = undefined; + } log.debug('Launcher', 'Saving game...'); const savedGame = await gameRepository.save(game); if (savedGame) { onDidUpdateGame.fire({oldGame: game, newGame: savedGame}); } @@ -305,7 +330,6 @@ export async function save(game: Game): Promise { // Ardil TODO fix this. export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); - //const addAppRepository = getManager().getRepository(AdditionalApp); const game = await findGame(gameId); if (game) { // Delete GameData @@ -529,7 +553,7 @@ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: str * @param alias The name of the table. * @param query The query to add to. * @param field The field (column) to search on. - * @param value The value to search for. If it's a string, it will be interpreted as position-independent + * @param value The value to search for. If it's a string, it will be interpreted as position-independent * if the field is not on the exactFields list. * @param count How many conditions we've already filtered. Determines whether we use .where() or .andWhere(). * @param whitelist Whether this is a whitelist or a blacklist search. @@ -628,7 +652,7 @@ async function getGameQuery( whereCount++; query.skip(offset); // TODO: Why doesn't offset work here? } - + // Tag filtering if (filterOpts && filterOpts.searchQuery) { const aliasWhitelist = filterOpts.searchQuery.whitelist.filter(f => f.field === 'tag').map(f => f.value); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 6a28f9e40..38ed863de 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -86,6 +86,26 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } } } + // @TODO ditto as above. + if (curation.meta.parentGameId && curation.meta.parentGameId != '') { + const existingGame = await GameManager.findGame(curation.meta.parentGameId, undefined, true); + if (existingGame == undefined) { + // Warn user of invalid parent + const response = await opts.openDialog({ + title: 'Invalid Parent', + message: 'This curation has an invalid parent game id.\nContinue importing this curation? Warning: this will make the game be parentless!\n\n' + + `Curation:\n\tTitle: ${curation.meta.title}\n\tLaunch Command: ${curation.meta.launchCommand}\n\tPlatform: ${curation.meta.platform}\n\n`, + buttons: ['Yes', 'No'] + }); + if (response === 1) { + throw new Error('User Cancelled Import'); + } else { + curation.meta.parentGameId = undefined; + } + } + } else if (curation.meta.parentGameId == '') { + curation.meta.parentGameId = undefined; + } // Build content list const contentToMove = []; if (curation.meta.extras && curation.meta.extras.length > 0) { @@ -259,17 +279,17 @@ export async function launchCuration(key: string, meta: EditCurationMeta, symlin onDidEvent.fire(game); } -// Ardil TODO this won't work, fix it. +// Ardil TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit) { - if (meta.extras) { - if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } + if (meta.extras) { + if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } await GameLauncher.launchExtras({ ...opts, extrasPath: meta.extras }); - } } +} function logMessage(text: string, curation: EditCuration): void { console.log(`- ${text}\n (id: ${curation.key})`); @@ -285,7 +305,7 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) - parentGameId: gameMeta.parentGameId, + parentGameId: gameMeta.parentGameId === '' ? undefined : gameMeta.parentGameId, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -475,7 +495,7 @@ function curationLog(content: string): void { // return buffer.equals(secondBuffer); // } -function createPlaceholderGame(): Game { +/* function createPlaceholderGame(): Game { const id = uuid(); const game = new Game(); Object.assign(game, { @@ -509,7 +529,7 @@ function createPlaceholderGame(): Game { activeDataOnDisk: false }); return game; -} +}*/ export async function createTagsFromLegacy(tags: string, tagCache: Record): Promise { const allTags: Tag[] = []; diff --git a/src/back/responses.ts b/src/back/responses.ts index 4c1441b27..ce7eec7b8 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -205,11 +205,10 @@ export function registerRequestCallbacks(state: BackState): void { return state.execMappings; }); - state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { - const game = await GameManager.findGame(id, undefined, true); - if (game && game.extras) { + state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, extrasPath) => { + if (extrasPath) { await GameLauncher.launchExtras({ - extrasPath: game.extras, + extrasPath: extrasPath, fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, execMappings: state.execMappings, @@ -231,12 +230,10 @@ export function registerRequestCallbacks(state: BackState): void { if (game) { if (game.parentGameId && !game.parentGame) { - log.debug("Game Launcher", "Fetching parent game."); + log.debug('Game Launcher', 'Fetching parent game.'); // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } - // Ensure that the children is an array. Also enforce the no-multiple-generations rule. - //game.children = game.parentGameId ? [] : await game.children; // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -362,13 +359,13 @@ export function registerRequestCallbacks(state: BackState): void { }); // Ardil TODO check that this was the right move. - /*state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + /* state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { // Copy and apply new IDs - + const newGame = deepCopy(game); /* Ardil TODO figure this out. const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); @@ -658,7 +655,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { - let games: Game[] = await GameManager.findAllGames(); + const games: Game[] = await GameManager.findAllGames(); return games; }); @@ -957,12 +954,7 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.GET_PLAYLIST_GAME, async (event, playlistId, gameId) => { - const playlistGame = await GameManager.findPlaylistGame(playlistId, gameId); - if (playlistGame && playlistGame.game) { - // Ensure that the children is an array. Also enforce the no-multiple-generations rule. - //playlistGame.game.children = playlistGame.game.parentGameId ? [] : await playlistGame.game.children; - } - return playlistGame; + return await GameManager.findPlaylistGame(playlistId, gameId); }); state.socketServer.register(BackIn.ADD_PLAYLIST_GAME, async (event, playlistId, gameId) => { @@ -1121,7 +1113,7 @@ export function registerRequestCallbacks(state: BackState): void { exePath: state.exePath, appPathOverrides: state.preferences.appPathOverrides, providers: await getProviders(state), - proxy: state.preferences.browserModeProxy, + proxy: state.preferences.browserModeProxy, openDialog: state.socketServer.showMessageBoxBack(event.client), openExternal: state.socketServer.openExternal(event.client), runGame: runGameFactory(state) diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index 63b71db9a..37e56f6f7 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -14,7 +14,6 @@ import { Legacy_IAdditionalApplicationInfo, Legacy_IGameInfo } from '@shared/leg import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import * as child_process from 'child_process'; import * as fs from 'fs'; -import { add } from 'node-7z'; import * as path from 'path'; import { promisify } from 'util'; import { uuid } from './uuid'; @@ -164,7 +163,7 @@ export async function execProcess(state: BackState, proc: IBackProcessInfo, sync } export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): Game[] { - let retVal: Game[] = []; + const retVal: Game[] = []; for (const addApp of addApps) { if (addApp.applicationPath === ':message:') { game.message = addApp.launchCommand; @@ -172,7 +171,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli game.extras = addApp.launchCommand; game.extrasName = addApp.name; } else { - let newGame = new Game(); + const newGame = new Game(); Object.assign(newGame, { id: addApp.id, title: addApp.name, @@ -181,23 +180,23 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli parentGame: game, parentGameId: game.id, library: game.library, - alternateTitles: "", - series: "", - developer: "", - publisher: "", - dateAdded: "0000-00-00 00:00:00.000", - dateModified: "0000-00-00 00:00:00.000", - platform: "", + alternateTitles: '', + series: '', + developer: '', + publisher: '', + dateAdded: '0000-00-00 00:00:00.000', + dateModified: '0000-00-00 00:00:00.000', + platform: '', broken: false, extreme: game.extreme, - playMode: "", - status: "", - notes: "", - source: "", - releaseDate: "", - version: "", - originalDescription: "", - language: "", + playMode: '', + status: '', + notes: '', + source: '', + releaseDate: '', + version: '', + originalDescription: '', + language: '', orderTitle: addApp.name.toLowerCase(), activeDataId: undefined, activeDataOnDisk: false, @@ -205,11 +204,11 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli extras: undefined, extrasName: undefined, message: undefined - }) + }); retVal.push(newGame); } } - /*return addApps.map(a => { + /* return addApps.map(a => { return { id: a.id, name: a.name, diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 0e863c126..aaba46526 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,4 +1,4 @@ -import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Tree, UpdateDateColumn } from 'typeorm'; +import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { GameData } from './GameData'; import { Tag } from './Tag'; diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index b9fdee162..91f84e092 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -480,6 +480,8 @@ export function CurateBox(props: CurateBoxProps) { const disabled = props.curation ? props.curation.locked : false; // Whether the platform used by the curation is native locked + // Ardil TODO what is this used for? + // eslint-disable-next-line @typescript-eslint/no-unused-vars const native = useMemo(() => { if (props.curation && props.curation.meta.platform) { isPlatformNativeLocked(props.curation.meta.platform); @@ -622,6 +624,14 @@ export function CurateBox(props: CurateBoxProps) { onChange={onTitleChange} { ...sharedInputProps } /> + + + v function useOnInputChange(property: keyof EditCurationMeta, key: string | undefined, dispatch: React.Dispatch) { return useCallback((event: InputElementOnChangeEvent) => { if (key !== undefined) { - dispatch({ - type: 'edit-curation-meta', - payload: { - key: key, - property: property, - value: event.currentTarget.value - } - }); + // If it's one of the nullable types, treat '' as undefined. + if (property == 'parentGameId' || property == 'extras' || property == 'extrasName' || property == 'message') { + dispatch({ + type: 'edit-curation-meta', + payload: { + key: key, + property: property, + value: event.currentTarget.value == '' ? undefined : event.currentTarget.value + } + }); + } else { + dispatch({ + type: 'edit-curation-meta', + payload: { + key: key, + property: property, + value: event.currentTarget.value + } + }); + } } }, [dispatch, key]); } @@ -1135,6 +1157,18 @@ export function getCurationWarnings(curation: EditCuration, suggestions: Partial if (!warns.noLaunchCommand) { warns.invalidLaunchCommand = invalidLaunchCommandWarnings(getContentFolderByKey2(curation.key), launchCommand, strings); } + // @TODO check that the parentGameId is valid in some synchronous manner. + /* const parentId = curation.meta.parentGameId || ''; + log.debug("getCurationWarnings", "parentId: "+parentId); + if (parentId !== '') { + warns.invalidParentGameId = true; + window.Shared.back.request(BackIn.GET_GAME, parentId).then((result) => { + warns.invalidParentGameId = result == undefined; + }); + } else { + // If the parentGameId is undefined/empty, it's just a non-child game. That's fine. + warns.invalidParentGameId = false; + }*/ warns.noLogo = !curation.thumbnail.exists; warns.noScreenshot = !curation.screenshot.exists; warns.noTags = (!curation.meta.tags || curation.meta.tags.length === 0); diff --git a/src/renderer/components/CurateBoxWarnings.tsx b/src/renderer/components/CurateBoxWarnings.tsx index ec99a68c4..ccc0775f7 100644 --- a/src/renderer/components/CurateBoxWarnings.tsx +++ b/src/renderer/components/CurateBoxWarnings.tsx @@ -13,6 +13,8 @@ export type CurationWarnings = { noLaunchCommand?: boolean; /** If the launch command is not a url with the "http" protocol and doesn't point to a file in 'content' */ invalidLaunchCommand?: string[]; + /** If the parentGameId is not valid. */ + invalidParentGameId?: boolean; /** If the release date is invalid (incorrectly formatted). */ releaseDateInvalid?: boolean; /** If the application path value isn't used by any other game. */ diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 80bc7b516..ad5ff64eb 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -7,7 +7,6 @@ import { WithConfirmDialogProps } from '@renderer/containers/withConfirmDialog'; import { BackIn, BackOut, BackOutTemplate, TagSuggestion } from '@shared/back/types'; import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { wrapSearchTerm } from '@shared/game/GameFilter'; -import { ModelUtils } from '@shared/game/util'; import { GamePropSuggestions, PickType, ProcessAction } from '@shared/interfaces'; import { LangContainer } from '@shared/lang'; import { deepCopy, generateTagFilterGroup, sizeToString } from '@shared/Util'; @@ -19,7 +18,6 @@ import { WithPreferencesProps } from '../containers/withPreferences'; import { WithSearchProps } from '../containers/withSearch'; import { getGameImagePath, getGameImageURL } from '../Util'; import { LangContext } from '../util/lang'; -import { uuid } from '../util/uuid'; import { CheckBox } from './CheckBox'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { DropdownInputField } from './DropdownInputField'; @@ -510,20 +508,20 @@ export class RightBrowseSidebar extends React.Component {game.message ?
-

{strings.message}:

- -
- : undefined} +

{strings.message}:

+ +
+ : undefined}

{strings.extrasName}:

{strings.extras}:

- { game.broken || editable ? (
)) } - {game.extras && game.extrasName ? - : undefined - } + onLaunch={this.onExtrasLaunch} /> + : undefined}
) : undefined } {/* -- Application Path & Launch Command -- */} @@ -963,9 +958,9 @@ export class RightBrowseSidebar extends React.Component { diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 8b714a0ab..9062d0b06 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -2,7 +2,6 @@ import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; -import { CheckBox } from './CheckBox'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; @@ -69,7 +68,6 @@ export class RightBrowseSidebarChild extends React.Component
- {/* Wait for Exit */}
{/* Delete Button */} diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index 9c0527509..bd03aec6a 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -1,26 +1,15 @@ -import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; -import { CheckBox } from './CheckBox'; -import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; -import { OpenIcon } from './OpenIcon'; export type RightBrowseSidebarExtraProps = { /** Extras to show and edit */ // These two are explicitly non-nullable. extrasPath: string; extrasName: string; - game: Game; - /** Called when a field is edited */ - onEdit?: () => void; - /** Called when a field is edited */ - onDelete?: (gameId: string) => void; /** Called when the launch button is clicked */ - onLaunch?: (gameId: string) => void; - /** If the editing is disabled (it cant go into "edit mode") */ - editDisabled?: boolean; + onLaunch?: (extrasPath: string) => void; }; export interface RightBrowseSidebarExtra { @@ -33,13 +22,12 @@ export class RightBrowseSidebarExtra extends React.Component {/* Title & Launch Button */}
): JSX.Element { - const className = 'browse-right-sidebar__additional-application__delete-button'; - return ( -
- -
- ); - } - onLaunchClick = (): void => { if (this.props.onLaunch) { - this.props.onLaunch(this.props.game.id); - } - } - - onDeleteClick = (): void => { - if (this.props.onDelete) { - this.props.onDelete(this.props.game.id); + this.props.onLaunch(this.props.extrasPath); } } - onEdit(): void { - if (this.props.onEdit) { - this.props.onEdit(); - } - } - - /** Create a wrapper for a EditableTextWrap's onEditDone callback (this is to reduce redundancy). */ - wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { - return (event) => { - const addApp = this.props.game; - if (addApp) { - func(addApp, event.currentTarget.value); - this.forceUpdate(); - } - }; - } - - /** Create a wrapper for a CheckBox's onChange callback (this is to reduce redundancy). */ - wrapOnCheckBoxChange(func: (addApp: Game) => void) { - return () => { - if (!this.props.editDisabled) { - func(this.props.game); - this.onEdit(); - this.forceUpdate(); - } - }; - } - static contextType = LangContext; } diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 6e453d8c7..34b31370b 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -721,7 +721,7 @@ export class BrowsePage extends React.Component { - this.setState({ text: text + filePath + '\n' + createTextBarProgress(current, files.length) }) + this.setState({ text: text + filePath + '\n' + createTextBarProgress(current, files.length) }); }) .catch((error) => { text = text + `Failure - ${fileName} - ERROR: ${error}\n`; diff --git a/src/renderer/context/CurationContext.ts b/src/renderer/context/CurationContext.ts index ccce946a4..52947d36a 100644 --- a/src/renderer/context/CurationContext.ts +++ b/src/renderer/context/CurationContext.ts @@ -4,7 +4,6 @@ import { CurationIndexImage, EditCuration, EditCurationMeta, IndexedContent } fr import { createContextReducer } from '../context-reducer/contextReducer'; import { ReducerAction } from '../context-reducer/interfaces'; import { createCurationIndexImage } from '../curate/importCuration'; -import { uuid } from '../util/uuid'; const curationDefaultState: CurationsState = { defaultMetaData: undefined, diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 946350b79..be492e070 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -199,7 +199,7 @@ export type BackInTemplate = SocketTemplate BrowseChangeData; [BackIn.DUPLICATE_GAME]: (id: string, dupeImages: boolean) => BrowseChangeData; [BackIn.EXPORT_GAME]: (id: string, location: string, metaOnly: boolean) => void; - [BackIn.LAUNCH_EXTRAS]: (id: string) => void; + [BackIn.LAUNCH_EXTRAS]: (extrasPath: string) => void; [BackIn.SAVE_IMAGE]: (folder: string, id: string, content: string) => void; [BackIn.DELETE_IMAGE]: (folder: string, id: string) => void; [BackIn.ADD_LOG]: (data: ILogPreEntry & { logLevel: LogLevel }) => void; diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index f0dac9d71..1ac00414c 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -153,4 +153,4 @@ type CurationMetaFile = { 'Extras Name'?: string; 'Message'?: string; 'Parent Game ID'?: string; -}; \ No newline at end of file +}; diff --git a/src/shared/curate/parse.ts b/src/shared/curate/parse.ts index b9a1574ba..3b141288c 100644 --- a/src/shared/curate/parse.ts +++ b/src/shared/curate/parse.ts @@ -1,6 +1,6 @@ import { BackIn } from '@shared/back/types'; import { Coerce } from '@shared/utils/Coerce'; -import { IObjectParserProp, ObjectParser } from '../utils/ObjectParser'; +import { ObjectParser } from '../utils/ObjectParser'; import { CurationFormatObject, parseCurationFormat } from './format/parser'; import { CFTokenizer, tokenizeCurationFormat } from './format/tokenizer'; import { EditCurationMeta } from './types'; diff --git a/src/shared/lang.ts b/src/shared/lang.ts index ff515109c..b25992fb2 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -410,6 +410,9 @@ const langTemplate = { 'ilc_notHttp', 'ilc_nonExistant', 'sort', + 'parentGameId', + 'noParentGameId', + 'invalidParentGameId', ] as const, playlist: [ 'enterDescriptionHere', From 4214be3dac4aa3204eb485991d584f45815f6a6c Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 11:23:50 -0400 Subject: [PATCH 14/83] fix: child launching - children inherit on empty platform. --- src/back/responses.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/back/responses.ts b/src/back/responses.ts index ce7eec7b8..de9b77f6f 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -234,6 +234,13 @@ export function registerRequestCallbacks(state: BackState): void { // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } + // Inherit empty fields. + if (game.parentGame) { + if (game.platform === '') { + game.platform = game.parentGame.platform; + } + // Ardil TODO any more I should add? + } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { From cc824b11df204f386bd9302c9d4aa50f6f2c2029 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:47:32 -0400 Subject: [PATCH 15/83] Update the download button on child launch. --- src/renderer/components/RightBrowseSidebar.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index ad5ff64eb..dc4da6b88 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -626,7 +626,10 @@ export class RightBrowseSidebar extends React.Component { + addApp && this.props.onGameLaunch(addApp.id) + .then(this.onForceUpdateGameData); + }} onDelete={this.onChildDelete} /> )) } {game.extras && game.extrasName ? @@ -955,10 +958,6 @@ export class RightBrowseSidebar extends React.Component Date: Sat, 2 Apr 2022 18:06:14 -0400 Subject: [PATCH 16/83] Add option in findPlatforms to exclude children. Previously, the blank platform strings of children were included in the results of findPlatforms(). --- src/back/game/GameManager.ts | 11 +++++++---- src/back/responses.ts | 3 ++- typings/flashpoint-launcher.d.ts | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 39475b659..5311979d6 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -272,13 +272,16 @@ export async function findUniqueValuesInOrder(entity: any, column: string): Prom return Coerce.strArray(values.map(v => v[`entity_${column}`])); } -export async function findPlatforms(library: string): Promise { +export async function findPlatforms(library: string, includeChildren?: boolean): Promise { const gameRepository = getManager().getRepository(Game); - const libraries = await gameRepository.createQueryBuilder('game') + const query = gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) .select('game.platform') - .distinct() - .getRawMany(); + .distinct(); + if (!includeChildren) { + query.andWhere('game.parentGameId IS NULL'); + } + const libraries = await query.getRawMany(); return Coerce.strArray(libraries.map(l => l.game_platform)); } diff --git a/src/back/responses.ts b/src/back/responses.ts index de9b77f6f..9fbc185ce 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -95,7 +95,8 @@ export function registerRequestCallbacks(state: BackState): void { const mad4fpEnabled = state.serviceInfo ? (state.serviceInfo.server.findIndex(s => s.mad4fp === true) !== -1) : false; const platforms: Record = {}; for (const library of libraries) { - platforms[library] = (await GameManager.findPlatforms(library)).sort(); + // Explicitly exclude the platforms of child curations - they're mostly blank. + platforms[library] = (await GameManager.findPlatforms(library, false)).sort(); } // Fire after return has sent diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 6779f6d0c..e9bfbbde7 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -194,8 +194,9 @@ declare module 'flashpoint-launcher' { /** * Returns all unique Platform strings in a library * @param library Library to search + * @param includeChildren Whether to include child curations in the platform search. Default: false. */ - function findPlatforms(library: string): Promise; + function findPlatforms(library: string, includeChildren?: boolean): Promise; /** * Parses a Playlist JSON file and returns an object you can save later. * @param jsonData Raw JSON data of the Playlist file From b49d9c17c2d228f57debbf121c533d35c32b4782 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:00:36 -0400 Subject: [PATCH 17/83] Make the nullable properties actually nullable. Also make some == into ===, and make the message bar appear in when browsing. --- src/back/game/GameManager.ts | 16 ++++++------- src/back/importGame.ts | 5 +++- src/back/responses.ts | 2 +- src/database/entity/Game.ts | 20 ++++++++-------- src/renderer/components/CurateBox.tsx | 6 ++--- .../components/RightBrowseSidebar.tsx | 24 +++++++++---------- src/shared/curate/metaToMeta.ts | 9 +++---- typings/flashpoint-launcher.d.ts | 10 ++++---- 8 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 5311979d6..d632aa53c 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -292,16 +292,16 @@ export async function updateGames(games: Game[]): Promise { for (const game of chunk) { // Set nullable properties to null if they're empty. if (game.parentGameId === '') { - game.parentGameId = undefined; + game.parentGameId = null; } if (game.extras === '') { - game.extras = undefined; + game.extras = null; } if (game.extrasName === '') { - game.extrasName = undefined; + game.extrasName = null; } if (game.message === '') { - game.message = undefined; + game.message = null; } await transEntityManager.save(Game, game); } @@ -313,16 +313,16 @@ export async function save(game: Game): Promise { const gameRepository = getManager().getRepository(Game); // Set nullable properties to null if they're empty. if (game.parentGameId === '') { - game.parentGameId = undefined; + game.parentGameId = null; } if (game.extras === '') { - game.extras = undefined; + game.extras = null; } if (game.extrasName === '') { - game.extrasName = undefined; + game.extrasName = null; } if (game.message === '') { - game.message = undefined; + game.message = null; } log.debug('Launcher', 'Saving game...'); const savedGame = await gameRepository.save(game); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 38ed863de..de050cdea 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -305,7 +305,7 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) - parentGameId: gameMeta.parentGameId === '' ? undefined : gameMeta.parentGameId, + parentGameId: gameMeta.parentGameId === '' ? null : gameMeta.parentGameId ? gameMeta.parentGameId : null, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -323,6 +323,9 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration version: gameMeta.version || '', originalDescription: gameMeta.originalDescription || '', language: gameMeta.language || '', + message: gameMeta.message || null, + extrasName: gameMeta.extrasName || null, + extras: gameMeta.extras || null, dateAdded: date.toISOString(), dateModified: date.toISOString(), broken: false, diff --git a/src/back/responses.ts b/src/back/responses.ts index 9fbc185ce..0dec478d7 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -578,7 +578,7 @@ export function registerRequestCallbacks(state: BackState): void { } const game = await GameManager.findGame(gameData.gameId); if (game) { - game.activeDataId = undefined; + game.activeDataId = null; game.activeDataOnDisk = false; await GameManager.save(game); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index aaba46526..68dfe9cef 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -19,8 +19,8 @@ export class Game { @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; - @Column({ nullable: true }) - parentGameId?: string; + @Column({ type: "varchar", nullable: true }) + parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. @OneToMany((type) => Game, (game) => game.parentGame) @@ -127,8 +127,8 @@ export class Game { placeholder: boolean; /** ID of the active data */ - @Column({ nullable: true }) - activeDataId?: number; + @Column({ type: "integer", nullable: true }) + activeDataId: number | null; /** Whether the data is present on disk */ @Column({ default: false }) @@ -137,14 +137,14 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; - @Column({ nullable: true }) - extras?: string; + @Column({ type: "varchar", nullable: true }) + extras: string | null; - @Column({ nullable: true }) - extrasName?: string; + @Column({ type: "varchar", nullable: true }) + extrasName: string | null; - @Column({ nullable: true }) - message?: string; + @Column({ type: "varchar", nullable: true }) + message: string | null; // This doesn't run... sometimes. @BeforeUpdate() diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 91f84e092..159ea3dc2 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -945,13 +945,13 @@ function useOnInputChange(property: keyof EditCurationMeta, key: string | undefi return useCallback((event: InputElementOnChangeEvent) => { if (key !== undefined) { // If it's one of the nullable types, treat '' as undefined. - if (property == 'parentGameId' || property == 'extras' || property == 'extrasName' || property == 'message') { + if (property === 'parentGameId' || property === 'extras' || property === 'extrasName' || property === 'message') { dispatch({ type: 'edit-curation-meta', payload: { key: key, property: property, - value: event.currentTarget.value == '' ? undefined : event.currentTarget.value + value: event.currentTarget.value === '' ? undefined : event.currentTarget.value } }); } else { @@ -1163,7 +1163,7 @@ export function getCurationWarnings(curation: EditCuration, suggestions: Partial if (parentId !== '') { warns.invalidParentGameId = true; window.Shared.back.request(BackIn.GET_GAME, parentId).then((result) => { - warns.invalidParentGameId = result == undefined; + warns.invalidParentGameId = result === undefined; }); } else { // If the parentGameId is undefined/empty, it's just a non-child game. That's fine. diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index dc4da6b88..7481d8170 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -506,18 +506,16 @@ export class RightBrowseSidebar extends React.Component
- {game.message ? -
-

{strings.message}:

- -
- : undefined} +
+

{strings.message}:

+ +

{strings.extrasName}:

) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) || game.extras ? ( + { editable || (currentChildren && currentChildren.length > 0) || (game.extras && game.extrasName) ? (

{strings.additionalApplications}:

diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index 1ac00414c..8900829e8 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -34,10 +34,11 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor parsed['Launch Command'] = game.launchCommand; parsed['Game Notes'] = game.notes; parsed['Original Description'] = game.originalDescription; - parsed['Parent Game ID'] = game.parentGameId; - parsed['Extras'] = game.extras; - parsed['Extras Name'] = game.extrasName; - parsed['Message'] = game.message; + // The meta files use undefined, the DB uses null. + parsed['Parent Game ID'] = game.parentGameId ? game.parentGameId : undefined; + parsed['Extras'] = game.extras ? game.extras : undefined; + parsed['Extras Name'] = game.extrasName ? game.extrasName : undefined; + parsed['Message'] = game.message ? game.message : undefined; // Return return parsed; } diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index e9bfbbde7..4898a720d 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -455,7 +455,7 @@ declare module 'flashpoint-launcher' { /** This game's parent game. */ parentGame?: Game; /** ID of the game which owns this game */ - parentGameId?: string; + parentGameId: string | null; /** A list of child games. */ children?: Game[]; /** Full title of the game */ @@ -509,15 +509,15 @@ declare module 'flashpoint-launcher' { /** If the game is a placeholder (and can therefore not be saved) */ placeholder: boolean; /** ID of the active data */ - activeDataId?: number; + activeDataId: number | null; /** Whether the data is present on disk */ activeDataOnDisk: boolean; /** The path to any extras. */ - extras?: string; + extras: string | null; /** The name to be displayed for those extras. */ - extrasName?: string; + extrasName: string | null; /** The message to display when the game starts. */ - message?: string; + message: string | null; data?: GameData[]; updateTagsStr: () => void; From a88c90e4487846026814b18766d5d375889e2d59 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:10:13 -0400 Subject: [PATCH 18/83] Make the linter happy about b49d9c1. --- src/database/entity/Game.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 68dfe9cef..0d8b984d0 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -19,7 +19,7 @@ export class Game { @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. @@ -127,7 +127,7 @@ export class Game { placeholder: boolean; /** ID of the active data */ - @Column({ type: "integer", nullable: true }) + @Column({ type: 'integer', nullable: true }) activeDataId: number | null; /** Whether the data is present on disk */ @@ -137,13 +137,13 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) extras: string | null; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) extrasName: string | null; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) message: string | null; // This doesn't run... sometimes. From 6999c0cda2cf5fb3af0f2eb979ce2e5e9c628481 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:17:40 -0400 Subject: [PATCH 19/83] Add new lang strings for English. --- lang/en.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lang/en.json b/lang/en.json index 30d6fd331..2dad855cd 100644 --- a/lang/en.json +++ b/lang/en.json @@ -301,7 +301,13 @@ "noGameMatchedSearch": "Try searching for something less restrictive.", "mountParameters": "Mount Parameters", "noMountParameters": "No Mount Parameters", - "showExtremeScreenshot": "Show Extreme Screenshot" + "showExtremeScreenshot": "Show Extreme Screenshot", + "extras": "Extras", + "noExtras": "No Extras", + "message": "Launch Message", + "noMessage": "No Message", + "extrasName": "Extras Name", + "noExtrasName": "No Extras Name" }, "tags": { "name": "Name", @@ -390,7 +396,10 @@ "noScreenshot": "There is no screenshot on this curation.", "ilc_notHttp": "Use HTTP.", "ilc_nonExistant": "Point to an existing file in your curation's 'content' folder.", - "sort": "Sort Curations (A-Z)" + "sort": "Sort Curations (A-Z)", + "parentGameId": "Parent Game ID", + "noParentGameId": "No Parent Game ID", + "invalidParentGameId": "Invalid Parent Game ID!" }, "playlist": { "enterDescriptionHere": "Enter a description here...", From bf035a05e7f934cff1a73c7f0747bc2b66f9225a Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:40:15 -0400 Subject: [PATCH 20/83] Give extras launch button its own section in sidebar. --- .../components/RightBrowseSidebar.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 7481d8170..5494f1ec5 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -613,8 +613,19 @@ export class RightBrowseSidebar extends React.Component
) : undefined } + {(game.extras && game.extrasName) ? ( +
+
+

{strings.extras}:

+
+ +
+ ) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) || (game.extras && game.extrasName) ? ( + { editable || (currentChildren && currentChildren.length > 0) ? (

{strings.additionalApplications}:

@@ -630,12 +641,6 @@ export class RightBrowseSidebar extends React.Component )) } - {game.extras && game.extrasName ? - - : undefined}
) : undefined } {/* -- Application Path & Launch Command -- */} From 2467415796741e1d6c9b5c6c0e29eb7268f94525 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 23:55:18 -0400 Subject: [PATCH 21/83] Fix game count update on delete, remove TODOs. --- src/back/responses.ts | 16 +--------------- src/renderer/app.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index 0dec478d7..7ab8b8f86 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -188,7 +188,6 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_TOTAL, async (event) => { return await GameManager.countGames(); }); @@ -201,7 +200,6 @@ export function registerRequestCallbacks(state: BackState): void { return data; }); - // Ardil TODO state.socketServer.register(BackIn.GET_EXEC, (event) => { return state.execMappings; }); @@ -225,7 +223,7 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); - // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id, undefined, true); @@ -331,12 +329,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAMES, async (event, data) => { await GameManager.updateGames(data); }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME, async (event, data) => { try { const game = await GameManager.save(data); @@ -352,7 +348,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { // Ardil TODO figure out this thing. const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); @@ -421,7 +416,6 @@ export function registerRequestCallbacks(state: BackState): void { }; });*/ - // Ardil TODO state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); if (playlist) { @@ -438,7 +432,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_PLAYLIST, async (event, filePath, library) => { try { const rawData = await fs.promises.readFile(filePath, 'utf-8'); @@ -474,7 +467,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_ALL_PLAYLISTS, async (event) => { const playlists = await GameManager.findPlaylists(true); for (const playlist of playlists) { @@ -483,7 +475,6 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.send(event.client, BackOut.PLAYLISTS_CHANGE, await GameManager.findPlaylists(state.preferences.browsePageShowExtreme)); }); - // Ardil TODO state.socketServer.register(BackIn.EXPORT_PLAYLIST, async (event, id, location) => { const playlist = await GameManager.findPlaylist(id, true); if (playlist) { @@ -493,7 +484,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO ensure that we really don't need children for this. state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { const game = await GameManager.findGame(id, undefined, true); @@ -530,7 +520,6 @@ export function registerRequestCallbacks(state: BackState): void { return await GameManager.findGame(id); }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); // Verify it's still on disk @@ -546,12 +535,10 @@ export function registerRequestCallbacks(state: BackState): void { return gameData; }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_GAME_DATA, async (event, id) => { return GameDataManager.findGameData(id); }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME_DATAS, async (event, data) => { // Ignore presentOnDisk, client isn't the most aware await Promise.all(data.map(async (d) => { @@ -564,7 +551,6 @@ export function registerRequestCallbacks(state: BackState): void { })); }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME_DATA, async (event, gameDataId) => { const gameData = await GameDataManager.findOne(gameDataId); if (gameData) { diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index a120563d5..c8c8a72ce 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -888,8 +888,13 @@ export class App extends React.Component { const strings = this.props.main.lang; const library = getBrowseSubPath(this.props.location.pathname); window.Shared.back.request(BackIn.DELETE_GAME, gameId) - .then(() => { this.setViewQuery(library); }) - .catch((error) => { + .then((deleteResults) => { + this.props.dispatchMain({ + type: MainActionType.SET_GAMES_TOTAL, + total: deleteResults.gamesTotal, + }); + this.setViewQuery(library); + }).catch((error) => { log.error('Launcher', `Error deleting game: ${error}`); alert(strings.dialog.unableToDeleteGame + '\n\n' + error); }); From 8778191bc473a6564637e99e5a2e0bd20000822b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 3 Apr 2022 15:11:26 -0400 Subject: [PATCH 22/83] Fix game data browser for single-pack games. --- src/renderer/components/GameDataBrowser.tsx | 6 +++--- src/renderer/components/RightBrowseSidebar.tsx | 2 +- src/renderer/components/pages/BrowsePage.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/GameDataBrowser.tsx b/src/renderer/components/GameDataBrowser.tsx index 2686e4b18..f222285ad 100644 --- a/src/renderer/components/GameDataBrowser.tsx +++ b/src/renderer/components/GameDataBrowser.tsx @@ -31,7 +31,7 @@ export type GameDataBrowserProps = { game: Game; onClose: () => void; onEditGame: (game: Partial) => void; - onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId?: number) => void; + onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId: number | null) => void; onForceUpdateGameData: () => void; } @@ -122,7 +122,7 @@ export class GameDataBrowser extends React.Component { await window.Shared.back.request(BackIn.DELETE_GAME_DATA, id); if (this.props.game.activeDataId === id) { - this.props.onUpdateActiveGameData(false); + this.props.onUpdateActiveGameData(false, null); } const newPairedData = [...this.state.pairedData]; const idx = newPairedData.findIndex(pr => pr.id === id); @@ -146,7 +146,7 @@ export class GameDataBrowser extends React.Component this.onUpdateTitle(index, title)} onUpdateParameters={(parameters) => this.onUpdateParameters(index, parameters)} onActiveToggle={() => { - this.props.onUpdateActiveGameData(data.presentOnDisk, data.id); + this.props.onUpdateActiveGameData(data.presentOnDisk, data.id === this.props.game.activeDataId ? null : data.id); }} onUninstall={() => { window.Shared.back.request(BackIn.UNINSTALL_GAME_DATA, data.id) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 5494f1ec5..de3d297ac 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -69,7 +69,7 @@ type OwnProps = { onOpenExportMetaEdit: (gameId: string) => void; onEditGame: (game: Partial) => void; - onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId?: number) => void; + onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId: number | null) => void; }; export type RightBrowseSidebarProps = OwnProps & WithPreferencesProps & WithSearchProps & WithConfirmDialogProps; diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 34b31370b..c11e091a8 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -697,7 +697,7 @@ export class BrowsePage extends React.Component { + onUpdateActiveGameData = (activeDataOnDisk: boolean, activeDataId: number | null): void => { if (this.state.currentGame) { const newGame = new Game(); Object.assign(newGame, {...this.state.currentGame, activeDataOnDisk, activeDataId }); From 8dda5284291c734d186eaf353c1b979be657ef37 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 4 Apr 2022 08:09:48 -0400 Subject: [PATCH 23/83] Fix getRootPath() --- src/renderer/curate/importCuration.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/curate/importCuration.ts b/src/renderer/curate/importCuration.ts index a4f8fc525..3f4b8e059 100644 --- a/src/renderer/curate/importCuration.ts +++ b/src/renderer/curate/importCuration.ts @@ -168,11 +168,14 @@ async function getRootPath(dir: string): Promise { // Convert it to lower-case, because the extensions we're matching against // are lower-case. if (endsWithList(fullpath.toLowerCase(), validMetaNames)) { - return fullpath; + return path.dirname(fullpath); } } else if (stats.isDirectory()) { + const contents: string[] = await fs.readdir(fullpath); // We have a directory. Push all of the directory's contents onto the end of the queue. - queue.push(...(await fs.readdir(fullpath))); + for (const k of contents) { + queue.push(path.join(entry, k)); + } } } } From cffe7683cfd0b915b39759bef065c881191435dd Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 6 Apr 2022 20:53:38 -0400 Subject: [PATCH 24/83] Revert "build: switch to better-sqlite3" This reverts commit c64d1f71c6458f6e5a730e1c8e2b54a62346b462. --- ormconfig.json | 4 ++-- package.json | 2 +- src/back/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ormconfig.json b/ormconfig.json index beef81b3d..45a4124b7 100644 --- a/ormconfig.json +++ b/ormconfig.json @@ -1,5 +1,5 @@ { - "type": "better-sqlite3", + "type": "sqlite", "host": "localhost", "port": 3306, "username": "flashpoint", @@ -22,4 +22,4 @@ "migrationsDir": "src/database/migration", "subscribersDir": "src/database/subscriber" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 278934465..93e38fcdf 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "axios": "0.21.4", - "better-sqlite3": "^7.5.0", "connected-react-router": "6.8.0", "electron-updater": "4.3.1", "electron-util": "0.14.2", @@ -57,6 +56,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", + "sqlite3": "^5.0.2", "tail": "2.0.3", "tree-kill": "1.2.2", "typeorm": "0.2.37", diff --git a/src/back/index.ts b/src/back/index.ts index 2473b64c1..8ad2b07f2 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -35,7 +35,7 @@ import * as mime from 'mime'; import * as path from 'path'; import 'reflect-metadata'; // Required for the DB Models to function -import 'better-sqlite3'; +import 'sqlite3'; import { Tail } from 'tail'; import { ConnectionOptions, createConnection } from 'typeorm'; import { ConfigFile } from './ConfigFile'; @@ -309,7 +309,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { // Setup DB if (!state.connection) { const options: ConnectionOptions = { - type: 'better-sqlite3', + type: 'sqlite', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, From fe7a8f979154a46a105ee360c512aee0443d2720 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:18:28 -0400 Subject: [PATCH 25/83] fix: load tags for children. Also: slight cleanup. Remove some temporary statements, and add a relation-loading statement for the tags of child games. --- src/back/game/GameManager.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index d632aa53c..ca4f1325b 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -38,7 +38,7 @@ export async function countGames(): Promise { return gameRepository.count({ parentGameId: IsNull() }); } -/** Find the game with the specified ID. Ardil TODO find refs*/ +/** Find the game with the specified ID. */ export async function findGame(id?: string, filter?: FindOneOptions, noChildren?: boolean): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); @@ -50,6 +50,13 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi .relation('children') .of(game) .loadMany(); + // Load tags for the children too. + for (const child of game.children) { + child.tags = await gameRepository.createQueryBuilder() + .relation('tags') + .of(child) + .loadMany(); + } } if (game) { if (game.tags) { @@ -72,7 +79,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) + .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } @@ -88,9 +95,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o .setParameters(subQ.getParameters()) .select('row_num') .from('(' + subQ.getQuery() + ')', 'g') - .where('g.id = :gameId', { gameId: gameId }) - // Shouldn't be needed, but doing it anyway. - .andWhere('g.parentGameId IS NULL'); + .where('g.id = :gameId', { gameId: gameId }); const raw = await query.getRawOne(); // console.log(`${Date.now() - startTime}ms for row`); From c1892463670265fca3b14eeaae2dd08492c08d7b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:19:24 -0400 Subject: [PATCH 26/83] test: GameManager: findGame() and countGames(). --- jest.config.js | 1 + tests/src/back/game/GameManager.test.ts | 473 ++++++++---------------- tests/src/back/game/exampleDB.ts | 224 +++++++++++ 3 files changed, 378 insertions(+), 320 deletions(-) create mode 100644 tests/src/back/game/exampleDB.ts diff --git a/jest.config.js b/jest.config.js index a31ca6194..97baed5af 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,7 @@ module.exports = { '^@main(.*)$': '/src/main/$1', '^@renderer(.*)$': '/src/renderer/$1', '^@back(.*)$': '/src/back/$1', + '^@database(.*)$': '/src/database/$1', '^@tests(.*)$': '/tests/$1' } }; diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index be47ba560..677b909bd 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -1,334 +1,167 @@ import * as GameManager from '@back/game/GameManager'; -import { GameManagerState } from '@back/game/types'; -import { EventQueue } from '@back/util/EventQueue'; import { uuid } from '@back/util/uuid'; -import { deepCopy } from '@shared/Util'; -import { RESULT_PATH, STATIC_PATH } from '@tests/setup'; -import * as path from 'path'; +import { Game } from '@database/entity/Game'; +import { GameData } from '@database/entity/GameData'; +import { Playlist } from '@database/entity/Playlist'; +import { PlaylistGame } from '@database/entity/PlaylistGame'; +import { Source } from '@database/entity/Source'; +import { SourceData } from '@database/entity/SourceData'; +import { Tag } from '@database/entity/Tag'; +import { TagAlias } from '@database/entity/TagAlias'; +import { TagCategory } from '@database/entity/TagCategory'; +import { Initial1593172736527 } from '@database/migration/1593172736527-Initial'; +import { AddExtremeToPlaylist1599706152407 } from '@database/migration/1599706152407-AddExtremeToPlaylist'; +import { GameData1611753257950 } from '@database/migration/1611753257950-GameData'; +import { SourceDataUrlPath1612434225789 } from '@database/migration/1612434225789-SourceData_UrlPath'; +import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-Source_FileURL'; +import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; +import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; +import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; +import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; +import { + ConnectionOptions, + createConnection, + getConnection, + getManager +} from 'typeorm'; +import { gameArray } from './exampleDB'; -const STATIC_PLATFORMS_PATH = path.join(STATIC_PATH, 'GameManager/platforms'); -const RESULT_PLATFORMS_PATH = path.join(RESULT_PATH, 'GameManager/platforms'); +const formatLocal = (input: Game): Partial => { + const partial: Partial = input; + delete partial.placeholder; + delete partial.updateTagsStr; + return partial; +}; +const formatDB = (input?: Game): Game | undefined => { + if (input) { + input.dateAdded = new Date(input.dateAdded).toISOString(); + } + return input; +}; +const formatDBMany = (input?: Game[]): Partial[] | undefined => { + if (input) { + input.forEach((game) => { + // TODO It seems the types aren't quite right? This conversion *should* be unnecessary, but here we are? + game.dateAdded = new Date(game.dateAdded).toISOString(); + }); + } + return input; +}; -describe('GameManager', () => { - test('Load Platforms', async () => { - const state = createState(); - state.platformsPath = STATIC_PLATFORMS_PATH; - const errors = await loadPlatforms(state); - expect(state.platforms.length).toBe(3); // Total number of platforms loaded - expect(errors.length).toBe(0); // No platforms should fail to load - // @TODO Compare that parsed content to a "snapshot" to verify that it was parsed correctly - }); +beforeAll(async () => { + const options: ConnectionOptions = { + type: 'sqlite', + database: ':memory:', + entities: [ + Game, + Playlist, + PlaylistGame, + Tag, + TagAlias, + TagCategory, + GameData, + Source, + SourceData, + ], + migrations: [ + Initial1593172736527, + AddExtremeToPlaylist1599706152407, + GameData1611753257950, + SourceDataUrlPath1612434225789, + SourceFileURL1612435692266, + SourceFileCount1612436426353, + GameTagsStr1613571078561, + GameDataParams1619885915109, + ChildCurations1648251821422, + ], + }; + const connection = await createConnection(options); + // TypeORM forces on but breaks Playlist Game links to unimported games + await connection.query('PRAGMA foreign_keys=off;'); + await connection.runMigrations(); +}); - test('Add Games & AddApps (to the same and already existing platform)', () => { - // Setup - const state = createState(); - const platform = createPlatform('test_platform', 'test_library', state.platformsPath); - state.platforms.push(platform); - for (let i = 0; i < 10; i++) { - const before = deepCopy(platform); - // Add Game & AddApps - const game = createGame(before.name, before.library); - const addApps = createAddApps(game.id, i % 3); // Try different numbers of add-apps - GameManager.updateMeta(state, { - game: game, - addApps: addApps, - }); - // Compare - expect(platform).toEqual({ // Game & AddApps have been added to the end of the collections in the correct order - ...before, - data: { - LaunchBox: { - Game: [ - ...before.data.LaunchBox.Game, - GameParser.reverseParseGame(game), - ], - AdditionalApplication: [ - ...before.data.LaunchBox.AdditionalApplication, - ...addApps.map(GameParser.reverseParseAdditionalApplication), - ], - }, - }, - collection: { - games: [ ...before.collection.games, game, ], - additionalApplications: [ ...before.collection.additionalApplications, ...addApps ], - }, - }); - } - }); +afterAll(async () => { + await getConnection().close(); +}); - test('Add Games & AddApps (to different and non-existing platforms)', () => { - // Setup - const state = createState(); - for (let i = 0; i < 10; i++) { - // Add Game - const game = createGame(`platform_${i}`, 'some_library'); - const addApps = createAddApps(game.id, i % 3); // Try different numbers of add-apps - GameManager.updateMeta(state, { - game: game, - addApps: addApps, - }); - // Compare - const platform = state.platforms.find(p => (p.name === game.platform) && (p.library === game.library)); - expect(platform).toEqual({ // Platform has been created and contains the game and add-apps - filePath: path.join(state.platformsPath, game.library, game.platform + '.xml'), - name: game.platform, - library: game.library, - data: { - LaunchBox: { - Game: [ GameParser.reverseParseGame(game) ], - AdditionalApplication: addApps.map(GameParser.reverseParseAdditionalApplication), - }, - }, - collection: { - games: [ game ], - additionalApplications: addApps, - }, - }); - } - }); +/* ASSUMPTIONS MADE: + * Each testing block will receive a clean database. Ensure that each testing block leaves a clean DB. + */ - test('Move Game & AddApps (between existing platforms)', () => { - // Setup - const state = createState(); - const fromPlatform = createPlatform('from_platform', 'some_library', state.platformsPath); - const toPlatform = createPlatform('to_platform', 'another_library', state.platformsPath); - state.platforms.push(fromPlatform, toPlatform); - // Add Game - const game = createGame(fromPlatform.name, fromPlatform.library); - const addApps = createAddApps(game.id, 5); - GameManager.updateMeta(state, { - game: game, - addApps: addApps, - }); - // Move Game - const sameGame: IGameInfo = { - ...game, - platform: toPlatform.name, - library: toPlatform.library, - }; - GameManager.updateMeta(state, { - game: sameGame, - addApps: addApps, - }); - // Compare - expect(fromPlatform).toEqual({ // First platform is empty - ...fromPlatform, - data: { - LaunchBox: { - Game: [], - AdditionalApplication: [], - }, - }, - collection: { - games: [], - additionalApplications: [], - }, - }); - expect(toPlatform).toEqual({ // Second platform has the game and add-apps - ...toPlatform, - data: { - LaunchBox: { - Game: [ GameParser.reverseParseGame(sameGame) ], - AdditionalApplication: addApps.map(GameParser.reverseParseAdditionalApplication), - }, - }, - collection: { - games: [ sameGame ], - additionalApplications: addApps, - }, - }); +describe('GameManager.findGame()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); }); - - test('Update Game (update the value of a field)', () => { - // Setup - const state = createState(); - const platform = createPlatform('test_platform', 'test_library', state.platformsPath); - state.platforms.push(platform); - // Add Game - const game = createGame(platform.name, platform.library); - GameManager.updateMeta(state, { - game: game, - addApps: [], - }); - // Update Game - const before = deepCopy(platform); - const updatedGame: IGameInfo = { - ...game, - title: 'New Title', - }; - GameManager.updateMeta(state, { - game: updatedGame, - addApps: [], - }); - // Compare - expect(platform).not.toEqual(before); // Platform has been changed - expect(platform).toEqual({ // Game has been added to the platform - ...before, - data: { - LaunchBox: { - Game: [ GameParser.reverseParseGame(updatedGame) ], - AdditionalApplication: [], - }, - }, - collection: { - games: [ updatedGame ], - additionalApplications: [], - }, - }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); }); - - test('Remove Games & AddApps (from one platform)', () => { - // Setup - const state = createState(); - const platform = createPlatform('test_platform', 'test_library', state.platformsPath); - state.platforms.push(platform); - // Add Games & AddApps - for (let i = 0; i < 10; i++) { - const game = createGame(platform.name, platform.library); - const addApp = createAddApp(game.id); - GameManager.updateMeta(state, { - game: game, - addApps: [ addApp ], - }); - } - // Remove Games & AddApps - for (let i = platform.collection.games.length - 1; i >= 0; i--) { - const before = deepCopy(platform); - const index = ((i + 7) ** 3) % platform.collection.games.length; // Pick a "random" index - const gameId = platform.collection.games[index].id; - // Remove Game & AddApps - GameManager.removeGameAndAddApps(state, gameId); - // Compare - expect(platform).toEqual({ // Game & AddApps have been removed - ...before, - data: { - LaunchBox: { - Game: before.data.LaunchBox.Game.filter(g => g.ID !== gameId), - AdditionalApplication: before.data.LaunchBox.AdditionalApplication.filter(a => a.GameID !== gameId), - } - }, - collection: { - games: before.collection.games.filter(g => g.id !== gameId), - additionalApplications: before.collection.additionalApplications.filter(a => a.gameId !== gameId), - }, - }); - } + test('Find game by UUID', async () => { + expect( + formatDB(await GameManager.findGame(gameArray[0].id, undefined, true)) + ).toEqual(formatLocal(gameArray[0])); }); - - test('Save Games & AddApps to file (multiple times)', () => { - // Setup - const state = createState(); - const platform = createPlatform('test_platform', 'test_library', state.platformsPath); - state.platforms.push(platform); - // Add content to platform - for (let i = 0; i < 10; i++) { - const game = createGame(platform.name, platform.library); - GameManager.updateMeta(state, { - game: game, - addApps: createAddApps(game.id, i % 3), // Try different numbers of add-apps - }); - } - // Save file multiple times - const saves: Promise[] = []; - for (let i = 0; i < 5; i++) { - saves.push(expect(GameManager.savePlatforms(state, [ platform ])).resolves.toBe(undefined)); - } - return Promise.all(saves); + test('Dont find game by UUID', async () => { + // Generate a new UUID and try to fetch it. Should fail. + expect(formatDB(await GameManager.findGame(uuid()))).toBeUndefined(); }); - - test('Find Game', () => { - // Setup - const state = createState(); - const platform = createPlatform('test_platform', 'test_library', state.platformsPath); - const game = createGame('', ''); - game.title = 'Sonic'; - platform.collection.games.push(game); - // Find Sonic (not Tails) - expect(GameManager.findGame([platform], g => g.title === 'Tails')).toBe(undefined); - expect(GameManager.findGame([platform], g => g.title === 'Sonic')).toHaveProperty('title', 'Sonic'); + test('Find game by property', async () => { + expect( + formatDB( + await GameManager.findGame( + undefined, + { where: { title: gameArray[0].title } }, + true + ) + ) + ).toEqual(formatLocal(gameArray[0])); + }); + test('Dont find game by property', async () => { + // At this point, I'm just using uuid() as a random string generator. + expect( + formatDB( + await GameManager.findGame(undefined, { where: { title: uuid() } }) + ) + ).toBeUndefined(); + }); + test('Find game including children', async () => { + expect( + formatDBMany((await GameManager.findGame(gameArray[0].id))?.children) + ).toEqual([formatLocal(gameArray[1])]); + }); + test('Find game excluding children', async () => { + expect( + formatDBMany( + (await GameManager.findGame(gameArray[0].id, undefined, true))?.children + ) + ).toBeUndefined(); + }); + test('Find game lacking children', async () => { + expect( + formatDBMany((await GameManager.findGame(gameArray[1].id))?.children) + ).toBeUndefined(); }); - - // @TODO Add tests for adding, moving and removing add-apps - // @TODO Test that edited games and add-apps retain their position in the arrays - // @TODO Test that added games and add-apps get pushed to the end of the arrays - - // @TODO Test "GameManager.findGame" - // @TODO Test functions in the "LaunchBox" namespace? }); -function createState(): GameManagerState { - return { - platforms: [], - platformsPath: RESULT_PLATFORMS_PATH, - saveQueue: new EventQueue(), - log: () => {}, // Don't log - }; -} - -function createPlatform(name: string, library: string, folderPath: string): GamePlatform { - return { - filePath: path.join(folderPath, library, name + '.xml'), - name: name, - library: library, - data: { - LaunchBox: { - Game: [], - AdditionalApplication: [], - }, - }, - collection: { - games: [], - additionalApplications: [], - }, - }; -} - -function createGame(platform: string, library: string): IGameInfo { - const id = uuid(); - return { - library: library, - orderTitle: '', - placeholder: false, - title: '', - alternateTitles: '', - id: id, - parentGameId: id, - series: '', - developer: '', - publisher: '', - dateAdded: '', - platform: platform, - broken: false, - extreme: false, - playMode: '', - status: '', - notes: '', - tags: '', - source: '', - originalDescription: '', - applicationPath: '', - language: '', - launchCommand: '', - releaseDate: '', - version: '', - }; -} - -function createAddApp(gameId: string): IAdditionalApplicationInfo { - return { - id: uuid(), - name: '', - gameId: gameId, - applicationPath: '', - launchCommand: '', - autoRunBefore: false, - waitForExit: false, - }; -} -function createAddApps(gameId: string, length: number): IAdditionalApplicationInfo[] { - const result: IAdditionalApplicationInfo[] = []; - for (let i = 0; i < length; i++) { - result.push(createAddApp(gameId)); - } - return result; -} +describe('GameManager.countGames()', () => { + beforeEach(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterEach(async () => { + await getManager().getRepository(Game).clear(); + }); + test('Count games', async () => { + // Count the number of games that have a null parentGameId. + let count = 0; + gameArray.forEach((game) => { + if (!game.parentGameId) { + count++; + } + }); + expect(await GameManager.countGames()).toBe(count); + }); + test('Count zero games', async () => { + getManager().getRepository(Game).clear(); + expect(await GameManager.countGames()).toBe(0); + }); +}); diff --git a/tests/src/back/game/exampleDB.ts b/tests/src/back/game/exampleDB.ts new file mode 100644 index 000000000..f5f330ebf --- /dev/null +++ b/tests/src/back/game/exampleDB.ts @@ -0,0 +1,224 @@ +import { Game } from '@database/entity/Game'; + +export const gameArray: Game[] = [ + { + id: 'c6ca5ded-42f4-4251-9423-55700140b096', + parentGameId: null, + title: '"Alone"', + alternateTitles: '', + series: '', + developer: 'Natpat', + publisher: 'MoFunZone; Sketchy', + dateAdded: '2019-11-24T23:39:57.629Z', + dateModified: '2021-03-07T02:08:12.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Platformer; Puzzle; Pixel', + source: 'https://www.newgrounds.com/portal/view/578326', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: 'http://uploads.ungrounded.net/578000/578326_Preloader.swf', + releaseDate: '2011-08-28', + version: '', + originalDescription: + 'Play as a penguin and a tortoise solving puzzles using a jetpack and a gun in this challenging, funky, colourful platformer!\nPlay alongside a beautiful soundtrack with 3 different songs, and funky graphics. Can you beat all 20 levels?\nI\'m so glad I\'m finally getting this out. Finally! :D Enjoy :)', + language: 'en', + library: 'arcade', + orderTitle: '"alone"', + activeDataId: null, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: '6fabbb7a-f614-455c-a239-360b6b69ea24', + // This is not the real parent game id. It doesn't matter, deal with it. + parentGameId: 'c6ca5ded-42f4-4251-9423-55700140b096', + title: '"Game feel" demo', + alternateTitles: '', + series: '', + developer: 'Sebastien Benard', + publisher: 'Deepnight.net', + dateAdded: '2021-01-25T23:30:49.267Z', + dateModified: '2022-04-03T17:37:21.000Z', + platform: 'HTML5', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: + 'Demonstration; Action; Platformer; Shooter; Pixel; Side-Scrolling', + source: 'http://deepnight.net/games/game-feel/', + applicationPath: 'FPSoftware\\Basilisk-Portable\\Basilisk-Portable.exe', + launchCommand: 'http://deepnight.net/games/game-feel/', + releaseDate: '2019-12-19', + version: '', + originalDescription: + 'This prototype is not exactly an actual game. It was developed to serve as a demonstration for a “Game feel” talk in 2019 at the ENJMIN school.\n\nIt shows the impact of small details on the overall quality of a game.\n\nYou will need a GAMEPAD to test it. You can enable or disable game features in this demo by pressing the START button.\n\nGAMEPAD is required to play\nA\njump\nB\ndash\nX\nshoot\nSTART\nenable/disable features\nSELECT\nrestart', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 8656, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: '4b1c582f-c953-48d4-b839-22897adc8406', + parentGameId: null, + title: '"Eight Planets and a Dwarf" Sudoku', + alternateTitles: '', + series: '', + developer: 'Julia Genyuk; Dave Fisher', + publisher: 'Windows to the Universe', + dateAdded: '2022-02-16T03:34:35.697Z', + dateModified: '2022-02-16T04:08:14.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Space; Sudoku', + source: 'https://www.windows2universe.org/games/sudoku/sudoku.html', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://www.windows2universe.org/games/sudoku/planets_sudoku.swf', + releaseDate: '', + version: '', + originalDescription: '', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 96874, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'f82c01e0-a30e-49e2-84b2-9b45c437eda6', + parentGameId: null, + title: '"Mind Realm"', + alternateTitles: '', + series: '', + developer: 'Wisdomchild', + publisher: 'WET GAMIN', + dateAdded: '2021-09-03T13:42:06.533Z', + dateModified: '2021-12-13T02:09:25.000Z', + platform: 'HTML5', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Puzzle; Score-Attack; Pixel; Arcade', + source: 'http://wetgamin.com/mindrealm.php', + applicationPath: 'FPSoftware\\Basilisk-Portable\\Basilisk-Portable.exe', + launchCommand: 'http://wetgamin.com/html5/mindrealm/index.html', + releaseDate: '', + version: '', + originalDescription: '', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 44546, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'b7dbd71b-d099-4fdf-9127-6e0a341b7f2d', + parentGameId: null, + title: '"Build a Tree" Dendrochronology Activity', + alternateTitles: '', + series: '', + developer: '', + publisher: 'Windows to the Universe', + dateAdded: '2022-02-18T04:20:19.301Z', + dateModified: '2022-02-18T04:31:29.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Creative; Educational; Object Creator; Toy', + source: + 'https://www.windows2universe.org/earth/climate/dendrochronology_build_tree.html', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://www.windows2universe.org/earth/climate/images/dendrochronology_build_tree.swf', + releaseDate: '', + version: '', + originalDescription: + 'The interactive diagram below demonstrates a very simple model of tree ring growth.\n\nSelect a temperature range (Normal, Cool, or Warm) and a precipitation amount (Normal, Dry, or Wet) for the coming year. Click the "Add Yearly Growth" button. The tree (which you are viewing a cross-section of the trunk of) grows one year\'s worth, adding a new ring.\n\nAdd some rings while varying the temperature and precipitation. Which of these factors has a stronger influence on the growth of the type of tree being modeled here?\n\nUse the "Reset" button to start over.\n\nThe "Show Specimen Tree" button displays a section of an "actual" tree specimen. Can you model the annual climate during each year of the specimen tree\'s life, matching your diagram with the specimen, to determine the climate history "written" in the rings of the specimen tree? (The "answer" is listed below, lower down on this page).', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 97171, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'a95d0ff7-3ee9-460f-a4f9-0e0c77764d13', + parentGameId: null, + title: '!BETA! little bullet hell', + alternateTitles: '', + series: '', + developer: 'leonidoss341', + publisher: 'Newgrounds', + dateAdded: '2021-08-04T06:09:06.114Z', + dateModified: '2021-08-04T06:09:44.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Action; Shooter', + source: 'https://www.newgrounds.com/portal/view/624363', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://uploads.ungrounded.net/624000/624363_touhou_project_tutorial.swf', + releaseDate: '2013-08-29', + version: 'Beta', + originalDescription: + 'Testing bullets. Just some fun\n\nAuthor Comments:\n\nWARNING! It\'s just beta, no need to say me that you wanna game, please tell me: everything work good or not? Also i want to know about graphic.\nMovement: keys or WASD, Shift-focus, Z-shooting.', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 32195, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, +]; From 756e158a797ea8b8ebcdebf6a03d61d436ae6678 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:24:33 -0400 Subject: [PATCH 27/83] fix: make orderBy required in findGameRow() Previously, the param was marked as optional. However, failure to supply it resulted in an error (column not found). --- src/back/game/GameManager.ts | 2 +- src/back/responses.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index ca4f1325b..2215b6b69 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -72,7 +72,7 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi } } /** Get the row number of an entry, specified by its gameId. */ -export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { +export async function findGameRow(gameId: string, orderBy: GameOrderBy, direction?: GameOrderReverse, filterOpts?: FilterGameOpts, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } // const startTime = Date.now(); diff --git a/src/back/responses.ts b/src/back/responses.ts index 7ab8b8f86..eeed566d2 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -802,9 +802,9 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, - query.filter, query.orderBy, query.orderReverse, + query.filter, undefined); return position - 1; // ("position" starts at 1, while "index" starts at 0) From 579459fa5b3eabb014fa476ed1649633ea798d5b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:56:50 -0400 Subject: [PATCH 28/83] fix: findGameRow() keyset pagination directions Switch the comparison for keyset pagination when orderBy is different. --- src/back/game/GameManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 2215b6b69..9b97f0931 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -79,11 +79,11 @@ export async function findGameRow(gameId: string, orderBy: GameOrderBy, directio const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) + .select(`game.id, row_number() over (order by game.${orderBy} ${direction ? direction : ''}) row_num`) .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } - subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); + subQ.andWhere(`(game.${orderBy}, game.id) ${direction === 'DESC' ? '<' : '>'} (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); } if (filterOpts) { // The "whereCount" param doesn't make much sense now, TODO change it. From 3114494fd9ced5914e3a64769706e4b46a22e7f4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:57:40 -0400 Subject: [PATCH 29/83] test: GameManager.findGameRow() --- tests/src/back/game/GameManager.test.ts | 312 +++++++++++++++++++++++- 1 file changed, 309 insertions(+), 3 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 677b909bd..f3e22bdf7 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -22,16 +22,37 @@ import { ConnectionOptions, createConnection, getConnection, - getManager + getManager, } from 'typeorm'; import { gameArray } from './exampleDB'; +import * as v8 from 'v8'; + +// Only the keys of T that can't be null or undefined. +type DefinedKeysOf = { + [k in keyof T]-?: null extends T[k] + ? never + : undefined extends T[k] + ? never + : k; +}[keyof T]; + +// This will be a copy of the array that I can feel comfortable mutating. I want to leave gameArray clean. +let arrayCopy: Game[]; const formatLocal = (input: Game): Partial => { - const partial: Partial = input; + const partial = input as Partial; delete partial.placeholder; delete partial.updateTagsStr; return partial; }; +const formatLocalMany = (input: Game[]): Partial[] => { + const partial = input as Partial[]; + partial.forEach((game) => { + delete game.placeholder; + delete game.updateTagsStr; + }); + return partial; +}; const formatDB = (input?: Game): Game | undefined => { if (input) { input.dateAdded = new Date(input.dateAdded).toISOString(); @@ -47,6 +68,33 @@ const formatDBMany = (input?: Game[]): Partial[] | undefined => { } return input; }; +/** + * Filters and then sorts an array of Game objects. + * @param array The array to filter and sort. + * @param filterFunc The function that determines if an element should be left in by the filter. + * @param sortColumn The column to sort the array on. + * @param reverse Whether or not to sort the array backwards. + * @returns The filtered and sorted array. + */ +const filterAndSort = ( + array: Game[], + filterFunc: (game: Game) => boolean, + sortColumn: DefinedKeysOf, + reverse?: boolean +): Game[] => { + const filtered = array.filter(filterFunc); + const flip = reverse ? -1 : 1; + filtered.sort((a: Game, b: Game) => { + if (a[sortColumn] > b[sortColumn]) { + return flip * 1; + } + if (a[sortColumn] < b[sortColumn]) { + return flip * -1; + } + return 0; + }); + return filtered; +}; beforeAll(async () => { const options: ConnectionOptions = { @@ -161,7 +209,265 @@ describe('GameManager.countGames()', () => { expect(await GameManager.countGames()).toBe(count); }); test('Count zero games', async () => { - getManager().getRepository(Game).clear(); + await getManager().getRepository(Game).clear(); expect(await GameManager.countGames()).toBe(0); }); }); + +describe('GameManager.findGameRow()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); + }); + test('Valid game ID, orderBy title', async () => { + // People on the internet say that this will be suboptimal. I don't care too much. + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + expect(await GameManager.findGameRow(gameArray[3].id, 'title', 'ASC')).toBe( + 1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id) + ); + }); + test('Invalid game ID, orderBy title', async () => { + expect(await GameManager.findGameRow(uuid(), 'title', 'ASC')).toBe(-1); + }); + test('Reasonable game filter, orderBy title', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => + game.originalDescription.includes('t') && game.parentGameId == null, + 'title' + ); + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: 'originalDescription', + value: 't', + }, + ], + }, + }) + ) + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Exclusive game filter, orderBy title', async () => { + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: 'originalDescription', + // Again, just a random string generator, essentially. + value: uuid(), + }, + ], + }, + }) + ).toBe(-1); + }); + test('Valid game ID, orderBy developer', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'developer' + ); + //console.log(JSON.stringify(arrayCopy)); + expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Invalid game filter', async () => { + // Invalid game filters should be ignored. + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => + game.originalDescription.includes('t') && game.parentGameId == null, + 'title' + ); + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: uuid(), + value: 't', + }, + { + field: 'originalDescription', + value: 't', + }, + ], + }, + }) + ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Valid game ID, orderBy title reverse', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + expect( + await GameManager.findGameRow(gameArray[3].id, 'title', 'DESC') + ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id)); + }); + test('Child game ID, orderBy title', async () => { + expect(await GameManager.findGameRow(gameArray[1].id, 'title', 'ASC')).toBe( + -1 + ); + }); + test('Valid game ID, orderBy title, with index before', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[5].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'ASC', + undefined, + { + orderVal: gameArray[5].title, + title: gameArray[5].title, + id: gameArray[5].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title, with index after', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[4].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'ASC', + undefined, + { + orderVal: gameArray[4].title, + title: gameArray[4].title, + id: gameArray[4].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title reverse, with index before', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[4].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'DESC', + undefined, + { + orderVal: gameArray[4].title, + title: gameArray[4].title, + id: gameArray[4].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title reverse, with index after', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[5].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'DESC', + undefined, + { + orderVal: gameArray[5].title, + title: gameArray[5].title, + id: gameArray[5].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); +}); From a5a45afce768b9c7dcd21cc2777dc87ee92dc333 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 22:07:14 -0400 Subject: [PATCH 30/83] style: make the linter happy --- tests/src/back/game/GameManager.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index f3e22bdf7..915a34129 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -32,8 +32,8 @@ type DefinedKeysOf = { [k in keyof T]-?: null extends T[k] ? never : undefined extends T[k] - ? never - : k; + ? never + : k; }[keyof T]; // This will be a copy of the array that I can feel comfortable mutating. I want to leave gameArray clean. @@ -263,8 +263,8 @@ describe('GameManager.findGameRow()', () => { }, }) ) - // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); }); test('Exclusive game filter, orderBy title', async () => { expect( @@ -293,10 +293,9 @@ describe('GameManager.findGameRow()', () => { (game: Game) => game.parentGameId == null, 'developer' ); - //console.log(JSON.stringify(arrayCopy)); expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) - // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); }); test('Invalid game filter', async () => { // Invalid game filters should be ignored. From 919a11fdee87dad30e8f9d535ca9e2e9cac44522 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:48:04 -0400 Subject: [PATCH 31/83] test: allow connection to be managed outside Don't open a new database connection if one already exists, and don't close the connection at the end of testing. --- tests/src/back/game/GameManager.test.ts | 69 ++++++++++++------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 915a34129..825fe1293 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -21,7 +21,7 @@ import { ChildCurations1648251821422 } from '@database/migration/1648251821422-C import { ConnectionOptions, createConnection, - getConnection, + getConnectionManager, getManager, } from 'typeorm'; import { gameArray } from './exampleDB'; @@ -97,40 +97,38 @@ const filterAndSort = ( }; beforeAll(async () => { - const options: ConnectionOptions = { - type: 'sqlite', - database: ':memory:', - entities: [ - Game, - Playlist, - PlaylistGame, - Tag, - TagAlias, - TagCategory, - GameData, - Source, - SourceData, - ], - migrations: [ - Initial1593172736527, - AddExtremeToPlaylist1599706152407, - GameData1611753257950, - SourceDataUrlPath1612434225789, - SourceFileURL1612435692266, - SourceFileCount1612436426353, - GameTagsStr1613571078561, - GameDataParams1619885915109, - ChildCurations1648251821422, - ], - }; - const connection = await createConnection(options); - // TypeORM forces on but breaks Playlist Game links to unimported games - await connection.query('PRAGMA foreign_keys=off;'); - await connection.runMigrations(); -}); - -afterAll(async () => { - await getConnection().close(); + if (!getConnectionManager().has('default')) { + const options: ConnectionOptions = { + type: 'sqlite', + database: ':memory:', + entities: [ + Game, + Playlist, + PlaylistGame, + Tag, + TagAlias, + TagCategory, + GameData, + Source, + SourceData, + ], + migrations: [ + Initial1593172736527, + AddExtremeToPlaylist1599706152407, + GameData1611753257950, + SourceDataUrlPath1612434225789, + SourceFileURL1612435692266, + SourceFileCount1612436426353, + GameTagsStr1613571078561, + GameDataParams1619885915109, + ChildCurations1648251821422, + ], + }; + const connection = await createConnection(options); + // TypeORM forces on but breaks Playlist Game links to unimported games + await connection.query('PRAGMA foreign_keys=off;'); + await connection.runMigrations(); + } }); /* ASSUMPTIONS MADE: @@ -470,3 +468,4 @@ describe('GameManager.findGameRow()', () => { ).toBe(diff > 0 ? diff : -1); }); }); + From 6e3959042aa1b884fca388542eefafe06a4b7dc7 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:05:46 -0400 Subject: [PATCH 32/83] fix: sorting for findGamePageKeyset() Also: add some jsdoc and a possible todo, and remove an unneeded selected colum. --- src/back/game/GameManager.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 9b97f0931..3ab0f9a11 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -136,7 +136,16 @@ export type GetPageKeysetResult = { total: number; } -export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: GameOrderBy, direction: GameOrderReverse, searchLimit?: number): Promise { +/** + * Gets the elements that occur before each page, and the total number of games that satisfy the filter. + * @param filterOpts The options that should be used to filter the results. + * @param orderBy The column to sort results by. + * @param direction The direction to sort results. + * @param searchLimit A limit on the number of returned results + * @param viewPageSize The size of a page. Mostly used for testing. + * @returns The elements that occur before each page, and the total number of games that satisfy the filter. + */ +export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: GameOrderBy, direction: GameOrderReverse, searchLimit?: number, viewPageSize = VIEW_PAGE_SIZE): Promise { // let startTime = Date.now(); validateSqlName(orderBy); @@ -145,11 +154,11 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // console.log('FindGamePageKeyset:'); const subQ = await getGameQuery('sub', filterOpts, orderBy, direction); - subQ.select(`sub.${orderBy}, sub.title, sub.id, sub.parentGameId, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); - subQ.orderBy(`sub.${orderBy} ${direction}, sub.title`, direction); + subQ.select(`sub.${orderBy}, sub.title, sub.id, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id ${direction}) % ${viewPageSize} when 0 then 1 else 0 end page_boundary`); + subQ.orderBy(`sub.${orderBy} ${direction}, sub.title ${direction}, sub.id`, direction); let query = getManager().createQueryBuilder() - .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) + .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}, g.id ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') .setParameters(subQ.getParameters()); @@ -169,6 +178,7 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // Count games let total = -1; // startTime = Date.now(); + // TODO reuse subQ? const subGameQuery = await getGameQuery('sub', filterOpts, orderBy, direction, 0, searchLimit ? searchLimit : undefined, undefined); query = getManager().createQueryBuilder() .select('COUNT(*)') From eb639c41a9178eede999b9b783dad97526557dae Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:07:19 -0400 Subject: [PATCH 33/83] test: GameManager.findGamePageKeyset() Also switch filtering funcs to === from ==. --- tests/src/back/game/GameManager.test.ts | 203 +++++++++++++++++++++--- 1 file changed, 181 insertions(+), 22 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 825fe1293..68a6a1c24 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -226,11 +226,11 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); expect(await GameManager.findGameRow(gameArray[3].id, 'title', 'ASC')).toBe( - 1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id) + 1 + filtered.findIndex((game: Game) => game.id === gameArray[3].id) ); }); test('Invalid game ID, orderBy title', async () => { @@ -243,7 +243,7 @@ describe('GameManager.findGameRow()', () => { const filtered = filterAndSort( arrayCopy, (game: Game) => - game.originalDescription.includes('t') && game.parentGameId == null, + game.originalDescription.includes('t') && game.parentGameId === null, 'title' ); expect( @@ -262,7 +262,7 @@ describe('GameManager.findGameRow()', () => { }) ) // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + .toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Exclusive game filter, orderBy title', async () => { expect( @@ -288,12 +288,12 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'developer' ); expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + .toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Invalid game filter', async () => { // Invalid game filters should be ignored. @@ -303,7 +303,7 @@ describe('GameManager.findGameRow()', () => { const filtered = filterAndSort( arrayCopy, (game: Game) => - game.originalDescription.includes('t') && game.parentGameId == null, + game.originalDescription.includes('t') && game.parentGameId === null, 'title' ); expect( @@ -324,7 +324,7 @@ describe('GameManager.findGameRow()', () => { ], }, }) - ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + ).toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Valid game ID, orderBy title reverse', async () => { arrayCopy = v8.deserialize( @@ -332,13 +332,13 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); expect( await GameManager.findGameRow(gameArray[3].id, 'title', 'DESC') - ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id)); + ).toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[3].id)); }); test('Child game ID, orderBy title', async () => { expect(await GameManager.findGameRow(gameArray[1].id, 'title', 'ASC')).toBe( @@ -351,14 +351,14 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[5].id + (game: Game) => game.id === gameArray[5].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -381,14 +381,14 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[4].id + (game: Game) => game.id === gameArray[4].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -411,15 +411,15 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[4].id + (game: Game) => game.id === gameArray[4].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -442,15 +442,15 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[5].id + (game: Game) => game.id === gameArray[5].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -469,3 +469,162 @@ describe('GameManager.findGameRow()', () => { }); }); +describe('GameManager.findGamePageKeyset()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); + }); + test('No filters, orderby title, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby title reverse, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title', + true + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'DESC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby developer, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'developer' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'developer', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('Filter out UUID, orderby title, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null && game.id !== gameArray[0].id, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [ + { + field: 'id', + value: gameArray[0].id, + }, + ], + whitelist: [], + }, + }, + 'title', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby title, pagesize 1, limit 3', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(3); + }); + test('No filters, orderby title, pagesize 2', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'ASC', + undefined, + 2 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[2 * (Number(key) - 2) + 1].id, + filtered[2 * (Number(key) - 2) + 1].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); +}); From fe192a4ad137db4b411d6b059e1f864b3d78d7f4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:32:53 -0400 Subject: [PATCH 34/83] feat: restore DUPLICATE_GAME. Also clean up some TODOs. --- src/back/responses.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index eeed566d2..62946e1f9 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -361,8 +361,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO check that this was the right move. - /* state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { @@ -370,14 +369,15 @@ export function registerRequestCallbacks(state: BackState): void { // Copy and apply new IDs const newGame = deepCopy(game); - /* Ardil TODO figure this out. - const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); + const newChildren = game.children?.map(addApp => deepCopy(addApp)); newGame.id = uuid(); - for (let j = 0; j < newAddApps.length; j++) { - newAddApps[j].id = uuid(); - newAddApps[j].parentGame = newGame; + if (newChildren) { + for (let j = 0; j < newChildren.length; j++) { + newChildren[j].id = uuid(); + newChildren[j].parentGameId = newGame.id; + } } - newGame.addApps = newAddApps; + newGame.children = newChildren; // Add copies result = await GameManager.save(newGame); @@ -414,7 +414,7 @@ export function registerRequestCallbacks(state: BackState): void { library: result && result.library, gamesTotal: await GameManager.countGames(), }; - });*/ + }); state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); @@ -572,12 +572,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_GAME_DATA, async (event, gameId, filePath) => { return GameDataManager.importGameData(gameId, filePath, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); }); - // Ardil TODO state.socketServer.register(BackIn.DOWNLOAD_GAME_DATA, async (event, gameDataId) => { const onProgress = (percent: number) => { // Sent to PLACEHOLDER download dialog on client @@ -596,7 +594,6 @@ export function registerRequestCallbacks(state: BackState): void { }); }); - // Ardil TODO This actually is important, don't ignore! state.socketServer.register(BackIn.UNINSTALL_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); if (gameData && gameData.path && gameData.presentOnDisk) { @@ -623,7 +620,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO should be quick. state.socketServer.register(BackIn.ADD_SOURCE_BY_URL, async (event, url) => { const sourceDir = path.join(state.config.flashpointPath, 'Data/Sources'); await fs.promises.mkdir(sourceDir, { recursive: true }); @@ -632,17 +628,14 @@ export function registerRequestCallbacks(state: BackState): void { }); }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_SOURCE, async (event, id) => { return SourceManager.remove(id); }); - // Ardil TODO state.socketServer.register(BackIn.GET_SOURCES, async (event) => { return SourceManager.find(); }); - // Ardil TODO state.socketServer.register(BackIn.GET_SOURCE_DATA, async (event, hashes) => { return GameDataManager.findSourceDataForHashes(hashes); }); @@ -659,7 +652,6 @@ export function registerRequestCallbacks(state: BackState): void { return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_KEYSET, async (event, library, query) => { query.filter = adjustGameFilter(query.filter); const result = await GameManager.findGamePageKeyset(query.filter, query.orderBy, query.orderReverse, query.searchLimit); @@ -967,7 +959,6 @@ export function registerRequestCallbacks(state: BackState): void { return playlistGame; }); - // Ardil done state.socketServer.register(BackIn.SAVE_LEGACY_PLATFORM, async (event, platform) => { const translatedGames = []; const tagCache: Record = {}; @@ -1043,7 +1034,6 @@ export function registerRequestCallbacks(state: BackState): void { return res; }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_CURATION, async (event, data) => { let error: any | undefined; try { @@ -1117,7 +1107,7 @@ export function registerRequestCallbacks(state: BackState): void { log.error('Launcher', e + ''); } }); - // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { const skipLink = (data.key === state.lastLinkedCurationKey); state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; @@ -1207,7 +1197,6 @@ export function registerRequestCallbacks(state: BackState): void { exit(state); }); - // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { const game = await GameManager.findGame(id, undefined, true); if (game) { @@ -1270,7 +1259,6 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); - // Ardil TODO what is this? state.socketServer.register(BackIn.RUN_COMMAND, async (event, command, args = []) => { // Find command const c = state.registry.commands.get(command); From c9734407fb4374fc9c8990d00637d7a65903d9b7 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:57:31 -0400 Subject: [PATCH 35/83] make the linter happy --- tests/src/back/game/GameManager.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 68a6a1c24..b5ea4b210 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -485,7 +485,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'ASC', @@ -510,7 +510,7 @@ describe('GameManager.findGamePageKeyset()', () => { 'title', true ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'DESC', @@ -534,7 +534,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'developer' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'developer', 'ASC', @@ -558,7 +558,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null && game.id !== gameArray[0].id, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( { searchQuery: { genericBlacklist: [], @@ -594,7 +594,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); + const result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); for (const key in result.keyset) { expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ filtered[Number(key) - 2].id, @@ -612,7 +612,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'ASC', From 882e82586667b8920af37f67cac7e300977358f4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 09:28:15 -0400 Subject: [PATCH 36/83] fix: implement child editing, fix default date. Implement the child edit function and turn on cascades for children. Set the default child modified and added dates to the unix epoch. Previously, the all-zeroes date was resulting in a null datetime object. --- src/database/entity/Game.ts | 2 +- .../migration/1648251821422-ChildCurations.ts | 3 ++- .../components/RightBrowseSidebar.tsx | 16 ++++++++++++- .../components/RightBrowseSidebarAddApp.tsx | 23 +++++-------------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 0d8b984d0..d9118e76a 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -23,7 +23,7 @@ export class Game { parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. - @OneToMany((type) => Game, (game) => game.parentGame) + @OneToMany((type) => Game, (game) => game.parentGame, { cascade: true }) children?: Game[]; @Column({collation: 'NOCASE'}) diff --git a/src/database/migration/1648251821422-ChildCurations.ts b/src/database/migration/1648251821422-ChildCurations.ts index 865d02d92..146fac36a 100644 --- a/src/database/migration/1648251821422-ChildCurations.ts +++ b/src/database/migration/1648251821422-ChildCurations.ts @@ -9,7 +9,8 @@ export class ChildCurations1648251821422 implements MigrationInterface { await queryRunner.query(`UPDATE game SET message = a.launchCommand FROM additional_app a WHERE game.id=a.parentGameId AND a.applicationPath=':message:'`); await queryRunner.query(`UPDATE game SET extras = a.launchCommand, extrasName = a.name FROM additional_app a WHERE game.id = a.parentGameId AND a.applicationPath = ':extras:'`, undefined); await queryRunner.query(`UPDATE game SET parentGameId = NULL WHERE id IS parentGameId`, undefined); - await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"0000-00-00 00:00:00.000" AS dateAdded,"0000-00-00 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); + // Default value for dateAdded and dateModified values is the unix epoch. Later UI will have to realize this. + await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"1970-01-01 00:00:00.000" AS dateAdded,"1970-01-01 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); await queryRunner.query(`DROP TABLE additional_app`, undefined); } diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index de3d297ac..2994d4f66 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -635,6 +635,7 @@ export class RightBrowseSidebar extends React.Component { addApp && this.props.onGameLaunch(addApp.id) .then(this.onForceUpdateGameData); @@ -974,10 +975,23 @@ export class RightBrowseSidebar extends React.Component) => { + if (this.props.currentGame && this.props.currentGame.children) { + const newChildren = [...this.props.currentGame.children]; + const childIndex = this.props.currentGame.children.findIndex(child => child.id === childId); + if (childIndex !== -1) { + newChildren[childIndex] = {...newChildren[childIndex], ...diff} as Game; + this.props.onEditGame({children: newChildren}); + } else { + throw new Error('Can\'t edit additional application because it was not found.'); + } + } + }; + onScreenshotClick = (): void => { this.setState({ showPreview: true }); } diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 9062d0b06..b0ba87d73 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -10,7 +10,7 @@ export type RightBrowseSidebarChildProps = { /** Additional Application to show and edit */ child: Game; /** Called when a field is edited */ - onEdit?: () => void; + onEdit?: (childId: string, diff: Partial) => void; /** Called when a field is edited */ onDelete?: (childId: string) => void; /** Called when the launch button is clicked */ @@ -25,9 +25,9 @@ export interface RightBrowseSidebarChild { /** Displays an additional application for a game in the right sidebar of BrowsePage. */ export class RightBrowseSidebarChild extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); - onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); - onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); + onNameEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({title: text}); }); + onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({applicationPath: text}); }); + onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({launchCommand: text}); }); render() { const allStrings = this.context; @@ -109,9 +109,9 @@ export class RightBrowseSidebarChild extends React.Component): void { if (this.props.onEdit) { - this.props.onEdit(); + this.props.onEdit(this.props.child.id, diff); } } @@ -126,16 +126,5 @@ export class RightBrowseSidebarChild extends React.Component void) { - return () => { - if (!this.props.editDisabled) { - func(this.props.child); - this.onEdit(); - this.forceUpdate(); - } - }; - } - static contextType = LangContext; } From 883c514240e419a78a4ecade09e63fda0fda0f58 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 10:47:27 -0400 Subject: [PATCH 37/83] fix: check that extras exist before importing Also remove some todos. --- src/back/importGame.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/back/importGame.ts b/src/back/importGame.ts index de050cdea..432ab23b7 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -51,7 +51,6 @@ export const onWillImportCuration: ApiEmitter = new ApiEmit * Import a curation. * @returns A promise that resolves when the import is complete. */ -// Ardil TODO export async function importCuration(opts: ImportCurationOpts): Promise { if (opts.date === undefined) { opts.date = new Date(); } const { @@ -109,8 +108,29 @@ export async function importCuration(opts: ImportCurationOpts): Promise { // Build content list const contentToMove = []; if (curation.meta.extras && curation.meta.extras.length > 0) { - // Add extras folder if meta has an entry - contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', curation.meta.extras)]); + const extrasPath = path.join(getCurationFolder(curation, fpPath), 'Extras'); + // Check that extras exist. + try { + // If this doesn't error out, the extras exist. + await fs.promises.stat(extrasPath); + // Add extras folder if meta has an entry + contentToMove.push([extrasPath, path.join(fpPath, 'Extras', curation.meta.extras)]); + } catch { + // It did error out, we need to tell the user. + const response = await opts.openDialog({ + title: 'Overwriting Game', + message: 'The curation claims to have extras but lacks an Extras folder!\nContinue importing this curation? Warning: this will remove the extras.\n\n' + + `Curation:\n\tTitle: ${curation.meta.title}\n\tLaunch Command: ${curation.meta.launchCommand}\n\tPlatform: ${curation.meta.platform}\n\t` + + `Expected extras path: ${extrasPath}`, + buttons: ['Yes', 'No'] + }); + if (response === 1) { + throw new Error('User Cancelled Import'); + } + curation.meta.extras = undefined; + curation.meta.extrasName = undefined; + } + } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); @@ -300,7 +320,6 @@ function logMessage(text: string, curation: EditCuration): void { * @param curation Curation to get data from. * @param gameId ID to use for Game */ -// Ardil TODO async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, date: Date): Promise { const game: Game = new Game(); Object.assign(game, { From caae80168ad4613110a018ce32463beb7db84301 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:16:04 -0400 Subject: [PATCH 38/83] style: make the linter happy --- src/back/importGame.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 432ab23b7..aaf7d0038 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -130,7 +130,6 @@ export async function importCuration(opts: ImportCurationOpts): Promise { curation.meta.extras = undefined; curation.meta.extrasName = undefined; } - } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); From c59346804e69a245685f3953a467e232e07d2e8b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:58:39 -0400 Subject: [PATCH 39/83] style: remove a bunch of irrelevant todos --- src/back/extensions/ApiImplementation.ts | 1 - src/back/game/GameManager.ts | 3 +-- src/back/importGame.ts | 3 +-- src/back/responses.ts | 15 +++------------ src/renderer/components/CurateBox.tsx | 1 - 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index de6036501..196348af9 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -142,7 +142,6 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, findGamesWithTag: GameManager.findGamesWithTag, updateGame: GameManager.save, updateGames: GameManager.updateGames, - // Ardil TODO removeGameAndChildren: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 3ab0f9a11..f279415c2 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -345,7 +345,7 @@ export async function save(game: Game): Promise { return savedGame; } -// Ardil TODO fix this. +// TODO this needs to re-parent somehow? Idk, wherever you want to put it. export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); const game = await findGame(gameId); @@ -358,7 +358,6 @@ export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: await GameDataManager.remove(gameData.id); } // Delete children - // Ardil TODO do Seirade's suggestion. if (game.children) { for (const child of game.children) { await gameRepository.remove(child); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index aaf7d0038..a1eaffbd3 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -284,7 +284,6 @@ export async function importCuration(opts: ImportCurationOpts): Promise { * Create and launch a game from curation metadata. * @param curation Curation to launch */ -// Ardil TODO export async function launchCuration(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit, onWillEvent:ApiEmitter, onDidEvent: ApiEmitter) { if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } @@ -298,7 +297,7 @@ export async function launchCuration(key: string, meta: EditCurationMeta, symlin onDidEvent.fire(game); } -// Ardil TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. +// TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit) { if (meta.extras) { diff --git a/src/back/responses.ts b/src/back/responses.ts index 62946e1f9..30fcceae5 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -166,7 +166,6 @@ export function registerRequestCallbacks(state: BackState): void { return { done }; }); - // Ardil TODO state.socketServer.register(BackIn.GET_SUGGESTIONS, async (event) => { const startTime = Date.now(); const suggestions: GamePropSuggestions = { @@ -238,7 +237,7 @@ export function registerRequestCallbacks(state: BackState): void { if (game.platform === '') { game.platform = game.parentGame.platform; } - // Ardil TODO any more I should add? + // TODO any more I should add? } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; @@ -349,7 +348,8 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { - // Ardil TODO figure out this thing. + // TODO This needs to somehow re-parent instead of just deleting all the children. It will have to wait + // until the frontend changes are made, I guess. const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); state.queries = {}; // Clear entire cache @@ -640,13 +640,11 @@ export function registerRequestCallbacks(state: BackState): void { return GameDataManager.findSourceDataForHashes(hashes); }); - // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { const games: Game[] = await GameManager.findAllGames(); return games; }); - // Ardil TODO state.socketServer.register(BackIn.RANDOM_GAMES, async (event, data) => { const flatFilters = data.tagFilters ? data.tagFilters.reduce((prev, cur) => prev.concat(cur.tags), []) : []; return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); @@ -661,7 +659,6 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_PAGE, async (event, data) => { data.query.filter = adjustGameFilter(data.query.filter); const startTime = new Date(); @@ -790,7 +787,6 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, @@ -848,7 +844,6 @@ export function registerRequestCallbacks(state: BackState): void { catch (error) { log.error('Launcher', error); } }); - // Ardil TODO add pref to make add-apps searchable? Later? state.socketServer.register(BackIn.UPDATE_PREFERENCES, async (event, data, refresh) => { const dif = difObjects(defaultPreferencesData, state.preferences, data); if (dif) { @@ -1369,7 +1364,6 @@ function adjustGameFilter(filterOpts: FilterGameOpts): FilterGameOpts { return filterOpts; } -// Ardil TODO /** * Creates a function that will run any game launch info given to it and register it as a service */ @@ -1407,7 +1401,6 @@ function runGameFactory(state: BackState) { }; } -// Ardil TODO function createCommand(filename: string, useWine: boolean, execFile: boolean): string { // This whole escaping thing is horribly broken. We probably want to switch // to an array representing the argv instead and not have a shell @@ -1431,7 +1424,6 @@ function createCommand(filename: string, useWine: boolean, execFile: boolean): s * @param command Command to run * @param args Arguments for the command */ -// Ardil TODO what is this? async function runCommand(state: BackState, command: string, args: any[] = []): Promise { const callback = state.registry.commands.get(command); let res = undefined; @@ -1451,7 +1443,6 @@ async function runCommand(state: BackState, command: string, args: any[] = []): /** * Returns a set of AppProviders from all extension registered Applications, complete with callbacks to run them. */ -// Ardil TODO async function getProviders(state: BackState): Promise { return state.extensionsService.getContributions('applications') .then(contributions => { diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 159ea3dc2..f611a048d 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -480,7 +480,6 @@ export function CurateBox(props: CurateBoxProps) { const disabled = props.curation ? props.curation.locked : false; // Whether the platform used by the curation is native locked - // Ardil TODO what is this used for? // eslint-disable-next-line @typescript-eslint/no-unused-vars const native = useMemo(() => { if (props.curation && props.curation.meta.platform) { From 90f5728ddb7d7f8a50ce5d49dbb78c62e6842728 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 16 Mar 2022 10:25:19 -0400 Subject: [PATCH 40/83] chore: add nano swapfiles to gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3f7d0ba99..ac21c4c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ Data/flashpoint.sqlite /.coveralls.yml /coverage /tests/result + +# nano's swap/lock files. +*.swp From 21e24cb536afddd07456201b62712b43f7a4f4db Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 20 Mar 2022 10:18:57 -0400 Subject: [PATCH 41/83] First try, broken. --- src/back/GameLauncher.ts | 99 ++------- src/back/extensions/ApiImplementation.ts | 7 +- src/back/game/GameManager.ts | 67 ++++-- src/back/importGame.ts | 70 +++--- src/back/index.ts | 9 +- src/back/responses.ts | 204 ++++++++++++------ src/back/util/misc.ts | 58 ++++- src/database/entity/AdditionalApp.ts | 32 --- src/database/entity/Game.ts | 25 ++- src/renderer/components/CurateBox.tsx | 121 +++-------- src/renderer/components/CurateBoxAddApp.tsx | 150 ------------- .../components/RightBrowseSidebar.tsx | 76 ++++--- .../components/RightBrowseSidebarAddApp.tsx | 54 ++--- .../components/RightBrowseSidebarExtra.tsx | 125 +++++++++++ src/renderer/components/pages/BrowsePage.tsx | 3 +- src/renderer/components/pages/CuratePage.tsx | 10 +- src/renderer/context/CurationContext.ts | 118 +--------- src/shared/back/types.ts | 18 +- src/shared/curate/metaToMeta.ts | 136 ++---------- src/shared/curate/parse.ts | 52 +---- src/shared/curate/types.ts | 21 +- src/shared/game/interfaces.ts | 7 - src/shared/game/util.ts | 15 +- src/shared/lang.ts | 6 + typings/flashpoint-launcher.d.ts | 11 +- 25 files changed, 578 insertions(+), 916 deletions(-) delete mode 100644 src/database/entity/AdditionalApp.ts delete mode 100644 src/renderer/components/CurateBoxAddApp.tsx create mode 100644 src/renderer/components/RightBrowseSidebarExtra.tsx diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index bcebb81ea..aa4ba4f99 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { AppProvider } from '@shared/extensions/interfaces'; import { ExecMapping, Omit } from '@shared/interfaces'; @@ -16,9 +15,8 @@ import * as GameDataManager from '@back/game/GameDataManager'; const { str } = Coerce; -export type LaunchAddAppOpts = LaunchBaseOpts & { - addApp: AdditionalApp; - native: boolean; +export type LaunchExtrasOpts = LaunchBaseOpts & { + extrasPath: string; } export type LaunchGameOpts = LaunchBaseOpts & { @@ -59,60 +57,20 @@ type LaunchBaseOpts = { export namespace GameLauncher { const logSource = 'Game Launcher'; - export function launchAdditionalApplication(opts: LaunchAddAppOpts): Promise { - // @FIXTHIS It is not possible to open dialog windows from the back process (all electron APIs are undefined). - switch (opts.addApp.applicationPath) { - case ':message:': - return new Promise((resolve, reject) => { - opts.openDialog({ - type: 'info', - title: 'About This Game', - message: opts.addApp.launchCommand, - buttons: ['Ok'], - }).finally(() => resolve()); - }); - case ':extras:': { - const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.addApp.launchCommand))); - return opts.openExternal(folderPath, { activate: true }) - .catch(error => { - if (error) { - opts.openDialog({ - type: 'error', - title: 'Failed to Open Extras', - message: `${error.toString()}\n`+ - `Path: ${folderPath}`, - buttons: ['Ok'], - }); - } - }); - } - default: { - let appPath: string = fixSlashes(path.join(opts.fpPath, getApplicationPath(opts.addApp.applicationPath, opts.execMappings, opts.native))); - const appPathOverride = opts.appPathOverrides.filter(a => a.enabled).find(a => a.path === appPath); - if (appPathOverride) { appPath = appPathOverride.override; } - const appArgs: string = opts.addApp.launchCommand; - const useWine: boolean = process.platform != 'win32' && appPath.endsWith('.exe'); - const launchInfo: LaunchInfo = { - gamePath: appPath, - gameArgs: appArgs, - useWine, - env: getEnvironment(opts.fpPath, opts.proxy), - }; - const proc = exec( - createCommand(launchInfo), - { env: launchInfo.env } - ); - logProcessOutput(proc); - log.info(logSource, `Launch Add-App "${opts.addApp.name}" (PID: ${proc.pid}) [ path: "${opts.addApp.applicationPath}", arg: "${opts.addApp.launchCommand}" ]`); - return new Promise((resolve, reject) => { - if (proc.killed) { resolve(); } - else { - proc.once('exit', () => { resolve(); }); - proc.once('error', error => { reject(error); }); - } + export async function launchExtras(opts: LaunchExtrasOpts): Promise { + const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.extrasPath))); + return opts.openExternal(folderPath, { activate: true }) + .catch(error => { + if (error) { + opts.openDialog({ + type: 'error', + title: 'Failed to Open Extras', + message: `${error.toString()}\n`+ + `Path: ${folderPath}`, + buttons: ['Ok'], }); } - } + }); } /** @@ -122,29 +80,12 @@ export namespace GameLauncher { export async function launchGame(opts: LaunchGameOpts, onWillEvent: ApiEmitter): Promise { // Abort if placeholder (placeholders are not "actual" games) if (opts.game.placeholder) { return; } - // Run all provided additional applications with "AutoRunBefore" enabled - if (opts.game.addApps) { - const addAppOpts: Omit = { - fpPath: opts.fpPath, - htdocsPath: opts.htdocsPath, - native: opts.native, - execMappings: opts.execMappings, - lang: opts.lang, - isDev: opts.isDev, - exePath: opts.exePath, - appPathOverrides: opts.appPathOverrides, - providers: opts.providers, - proxy: opts.proxy, - openDialog: opts.openDialog, - openExternal: opts.openExternal, - runGame: opts.runGame - }; - for (const addApp of opts.game.addApps) { - if (addApp.autoRunBefore) { - const promise = launchAdditionalApplication({ ...addAppOpts, addApp }); - if (addApp.waitForExit) { await promise; } - } - } + if (opts.game.message) { + await opts.openDialog({type: 'info', + title: 'About This Game', + message: opts.game.message, + buttons: ['Ok'], + }); } // Launch game let appPath: string = getApplicationPath(opts.game.applicationPath, opts.execMappings, opts.native); diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index 431f89e2b..aefd5af3c 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -142,7 +142,8 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, findGamesWithTag: GameManager.findGamesWithTag, updateGame: GameManager.save, updateGames: GameManager.updateGames, - removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndAddApps(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), + // Ardil TODO + removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); return game.tagsStr.split(';').findIndex(t => extremeTags.includes(t.trim())) !== -1; @@ -156,24 +157,28 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, get onWillLaunchGame() { return apiEmitters.games.onWillLaunchGame.event; }, + // Ardil TODO remove get onWillLaunchAddApp() { return apiEmitters.games.onWillLaunchAddApp.event; }, get onWillLaunchCurationGame() { return apiEmitters.games.onWillLaunchCurationGame.event; }, + // Ardil TODO remove get onWillLaunchCurationAddApp() { return apiEmitters.games.onWillLaunchCurationAddApp.event; }, get onDidLaunchGame() { return apiEmitters.games.onDidLaunchGame.event; }, + // Ardil TODO remove get onDidLaunchAddApp() { return apiEmitters.games.onDidLaunchAddApp.event; }, get onDidLaunchCurationGame() { return apiEmitters.games.onDidLaunchCurationGame.event; }, + // Ardil TODO remove get onDidLaunchCurationAddApp() { return apiEmitters.games.onDidLaunchCurationAddApp.event; }, diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 571fc484d..771b893e4 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -1,7 +1,6 @@ import { ApiEmitter } from '@back/extensions/ApiEmitter'; import { chunkArray } from '@back/util/misc'; import { validateSqlName, validateSqlOrder } from '@back/util/sql'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Playlist } from '@database/entity/Playlist'; import { PlaylistGame } from '@database/entity/PlaylistGame'; @@ -16,8 +15,9 @@ import { Coerce } from '@shared/utils/Coerce'; import * as fs from 'fs'; import * as path from 'path'; import * as TagManager from './TagManager'; -import { Brackets, FindOneOptions, getManager, SelectQueryBuilder } from 'typeorm'; +import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; +import { isNull } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -36,10 +36,10 @@ export const onDidRemovePlaylistGame = new ApiEmitter(); export async function countGames(): Promise { const gameRepository = getManager().getRepository(Game); - return gameRepository.count(); + return gameRepository.count({ parentGameId: IsNull() }); } -/** Find the game with the specified ID. */ +/** Find the game with the specified ID. Ardil TODO find refs*/ export async function findGame(id?: string, filter?: FindOneOptions): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); @@ -50,7 +50,7 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom return game; } } - +/** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } @@ -58,13 +58,15 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`); + .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) + .where("game.parentGameId is null"); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } - subQ.where(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); + subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); } if (filterOpts) { - applyFlatGameFilters('game', subQ, filterOpts, index ? 1 : 0); + // The "whereCount" param doesn't make much sense now, TODO change it. + applyFlatGameFilters('game', subQ, filterOpts, index ? 2 : 1); } if (orderBy) { subQ.orderBy(`game.${orderBy}`, direction); } @@ -78,11 +80,19 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o // console.log(`${Date.now() - startTime}ms for row`); return raw ? Coerce.num(raw.row_num) : -1; // Coerce it, even though it is probably of type number or undefined } - +/** + * Randomly selects a number of games from the database + * @param count The number of games to find. + * @param broken Whether to include broken games. + * @param excludedLibraries A list of libraries to exclude. + * @param flatFilters A set of filters on tags. + * @returns A ViewGame[] representing the results. + */ export async function findRandomGames(count: number, broken: boolean, excludedLibraries: string[], flatFilters: string[]): Promise { const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); + query.where("game.parentGameId is null"); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -172,7 +182,7 @@ export type FindGamesOpts = { export async function findAllGames(): Promise { const gameRepository = getManager().getRepository(Game); - return gameRepository.find(); + return gameRepository.find({parentGameId: IsNull()}); } /** Search the database for games. */ @@ -188,6 +198,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const range = ranges[i]; query = await getGameQuery('game', opts.filter, opts.orderBy, opts.direction, range.start, range.length, range.index); + query.where("game.parentGameId is null"); // Select games // @TODO Make it infer the type of T from the value of "shallow", and then use that to make "games" get the correct type, somehow? @@ -210,15 +221,15 @@ export async function findGames(opts: FindGamesOpts, shallow: return rangesOut; } -/** Find an add apps with the specified ID. */ -export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { +/** Find an add app with the specified ID. */ +export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { if (id || filter) { if (!filter) { filter = { - relations: ['parentGame'] + relations: ['parentGameId'] }; } - const addAppRepository = getManager().getRepository(AdditionalApp); + const addAppRepository = getManager().getRepository(Game); return addAppRepository.findOne(id, filter); } } @@ -229,6 +240,7 @@ export async function findPlatformAppPaths(platform: string): Promise .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) + .andWhere("game.parentGameId is null") .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -261,6 +273,7 @@ export async function findPlatforms(library: string): Promise { const gameRepository = getManager().getRepository(Game); const libraries = await gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) + .andWhere("game.parentGameId is null") .select('game.platform') .distinct() .getRawMany(); @@ -286,9 +299,10 @@ export async function save(game: Game): Promise { return savedGame; } -export async function removeGameAndAddApps(gameId: string, dataPacksFolderPath: string): Promise { +// Ardil TODO fix this. +export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); - const addAppRepository = getManager().getRepository(AdditionalApp); + //const addAppRepository = getManager().getRepository(AdditionalApp); const game = await findGame(gameId); if (game) { // Delete GameData @@ -298,9 +312,10 @@ export async function removeGameAndAddApps(gameId: string, dataPacksFolderPath: } await GameDataManager.remove(gameData.id); } - // Delete Add Apps - for (const addApp of game.addApps) { - await addAppRepository.remove(addApp); + // Delete children + // Ardil TODO do Seirade's suggestion. + for (const child of game.children) { + await gameRepository.remove(child); } // Delete Game await gameRepository.remove(game); @@ -472,6 +487,10 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi return whereCount; } +/** + * Add a position-independent search term (whitelist or blacklist) in or'd WHERE clauses on title, alternateTitles, + * developer, and publisher. + */ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: string, count: number, whitelist: boolean): void { validateSqlName(alias); @@ -500,6 +519,16 @@ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: str } } +/** + * Add a search term in a WHERE clause on the given field to a selectquerybuilder. + * @param alias The name of the table. + * @param query The query to add to. + * @param field The field (column) to search on. + * @param value The value to search for. If it's a string, it will be interpreted as position-independent + * if the field is not on the exactFields list. + * @param count How many conditions we've already filtered. Determines whether we use .where() or .andWhere(). + * @param whitelist Whether this is a whitelist or a blacklist search. + */ function doWhereField(alias: string, query: SelectQueryBuilder, field: string, value: any, count: number, whitelist: boolean) { // Create comparator const typing = typeof value; diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 0a7287ab9..9068cb822 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -1,12 +1,11 @@ import * as GameDataManager from '@back/game/GameDataManager'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Tag } from '@database/entity/Tag'; import { TagCategory } from '@database/entity/TagCategory'; import { validateSemiUUID } from '@renderer/util/uuid'; import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { convertEditToCurationMetaFile } from '@shared/curate/metaToMeta'; -import { CurationIndexImage, EditAddAppCuration, EditAddAppCurationMeta, EditCuration, EditCurationMeta } from '@shared/curate/types'; +import { CurationIndexImage, EditCuration, EditCurationMeta } from '@shared/curate/types'; import { getCurationFolder } from '@shared/curate/util'; import * as child_process from 'child_process'; import { execFile } from 'child_process'; @@ -17,7 +16,7 @@ import { ApiEmitter } from './extensions/ApiEmitter'; import * as GameManager from './game/GameManager'; import * as TagManager from './game/TagManager'; import { GameManagerState } from './game/types'; -import { GameLauncher, GameLaunchInfo, LaunchAddAppOpts, LaunchGameOpts } from './GameLauncher'; +import { GameLauncher, GameLaunchInfo, LaunchExtrasOpts, LaunchGameOpts } from './GameLauncher'; import { OpenExternalFunc, ShowMessageBoxFunc } from './types'; import { getMklinkBatPath } from './util/elevate'; import { uuid } from './util/uuid'; @@ -52,6 +51,7 @@ export const onWillImportCuration: ApiEmitter = new ApiEmit * Import a curation. * @returns A promise that resolves when the import is complete. */ +// Ardil TODO export async function importCuration(opts: ImportCurationOpts): Promise { if (opts.date === undefined) { opts.date = new Date(); } const { @@ -88,10 +88,9 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Build content list const contentToMove = []; - const extrasAddApp = curation.addApps.find(a => a.meta.applicationPath === ':extras:'); - if (extrasAddApp && extrasAddApp.meta.launchCommand && extrasAddApp.meta.launchCommand.length > 0) { + if (curation.meta.extras && curation.meta.extras.length > 0) { // Add extras folder if meta has an entry - contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', extrasAddApp.meta.launchCommand)]); + contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', curation.meta.extras)]); } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); @@ -110,7 +109,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Add game to database - let game = await createGameFromCurationMeta(gameId, curation.meta, curation.addApps, date); + let game = await createGameFromCurationMeta(gameId, curation.meta, date); game = await GameManager.save(game); // Store curation state for extension use later @@ -156,7 +155,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { if (saveCuration) { // Save working meta const metaPath = path.join(getCurationFolder(curation, fpPath), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, opts.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, opts.tagCategories)); await fs.writeFile(metaPath, meta); // Date in form 'YYYY-MM-DD' for folder sorting const date = new Date(); @@ -237,7 +236,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { console.warn(error.message); if (game.id) { // Clean up half imported entries - GameManager.removeGameAndAddApps(game.id, dataPacksFolderPath); + GameManager.removeGameAndChildren(game.id, dataPacksFolderPath); } }); } @@ -246,11 +245,12 @@ export async function importCuration(opts: ImportCurationOpts): Promise { * Create and launch a game from curation metadata. * @param curation Curation to launch */ -export async function launchCuration(key: string, meta: EditCurationMeta, addAppMetas: EditAddAppCurationMeta[], symlinkCurationContent: boolean, +// Ardil TODO +export async function launchCuration(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit, onWillEvent:ApiEmitter, onDidEvent: ApiEmitter) { if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } curationLog(`Launching Curation ${meta.title}`); - const game = await createGameFromCurationMeta(key, meta, [], new Date()); + const game = await createGameFromCurationMeta(key, meta, new Date()); GameLauncher.launchGame({ ...opts, game: game, @@ -259,22 +259,17 @@ export async function launchCuration(key: string, meta: EditCurationMeta, addApp onDidEvent.fire(game); } -/** - * Create and launch an additional application from curation metadata. - * @param curationKey Key of the parent curation index - * @param appCuration Add App Curation to launch - */ -export async function launchAddAppCuration(curationKey: string, appCuration: EditAddAppCuration, symlinkCurationContent: boolean, - skipLink: boolean, opts: Omit, onWillEvent: ApiEmitter, onDidEvent: ApiEmitter) { - if (!skipLink || !symlinkCurationContent) { await linkContentFolder(curationKey, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } - const addApp = createAddAppFromCurationMeta(appCuration, createPlaceholderGame()); - await onWillEvent.fire(addApp); - GameLauncher.launchAdditionalApplication({ - ...opts, - addApp: addApp, - }); - onDidEvent.fire(addApp); -} +// Ardil TODO this won't work, fix it. +export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, + skipLink: boolean, opts: Omit) { + if (meta.extras) { + if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } + await GameLauncher.launchExtras({ + ...opts, + extrasPath: meta.extras + }); + } + } function logMessage(text: string, curation: EditCuration): void { console.log(`- ${text}\n (id: ${curation.key})`); @@ -285,10 +280,12 @@ function logMessage(text: string, curation: EditCuration): void { * @param curation Curation to get data from. * @param gameId ID to use for Game */ -async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, addApps : EditAddAppCuration[], date: Date): Promise { +// Ardil TODO +async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, date: Date): Promise { const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) + parentGameId: gameMeta.parentGameId, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -312,26 +309,13 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration extreme: gameMeta.extreme || false, library: gameMeta.library || '', orderTitle: '', // This will be set when saved - addApps: [], + children: [], placeholder: false, activeDataOnDisk: false }); - game.addApps = addApps.map(addApp => createAddAppFromCurationMeta(addApp, game)); return game; } -function createAddAppFromCurationMeta(addAppMeta: EditAddAppCuration, game: Game): AdditionalApp { - return { - id: addAppMeta.key, - name: addAppMeta.meta.heading || '', - applicationPath: addAppMeta.meta.applicationPath || '', - launchCommand: addAppMeta.meta.launchCommand || '', - autoRunBefore: false, - waitForExit: false, - parentGame: game - }; -} - async function importGameImage(image: CurationIndexImage, gameId: string, folder: typeof LOGOS | typeof SCREENSHOTS, fullImagePath: string): Promise { if (image.exists) { const last = path.join(gameId.substr(0, 2), gameId.substr(2, 2), gameId+'.png'); @@ -520,7 +504,7 @@ function createPlaceholderGame(): Game { language: '', library: '', orderTitle: '', - addApps: [], + children: [], placeholder: true, activeDataOnDisk: false }); diff --git a/src/back/index.ts b/src/back/index.ts index 7ea3495b9..21061e9f5 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -1,5 +1,4 @@ import * as GameDataManager from '@back/game/GameDataManager'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { GameData } from '@database/entity/GameData'; import { Playlist } from '@database/entity/Playlist'; @@ -128,14 +127,10 @@ const state: BackState = { onLog: new ApiEmitter(), games: { onWillLaunchGame: new ApiEmitter(), - onWillLaunchAddApp: new ApiEmitter(), onWillLaunchCurationGame: new ApiEmitter(), - onWillLaunchCurationAddApp: new ApiEmitter(), onWillUninstallGameData: GameDataManager.onWillUninstallGameData, onDidLaunchGame: new ApiEmitter(), - onDidLaunchAddApp: new ApiEmitter(), onDidLaunchCurationGame: new ApiEmitter(), - onDidLaunchCurationAddApp: new ApiEmitter(), onDidUpdateGame: GameManager.onDidUpdateGame, onDidRemoveGame: GameManager.onDidRemoveGame, onDidUpdatePlaylist: GameManager.onDidUpdatePlaylist, @@ -218,7 +213,7 @@ async function main() { // Curation BackIn.IMPORT_CURATION, BackIn.LAUNCH_CURATION, - BackIn.LAUNCH_CURATION_ADDAPP, + BackIn.LAUNCH_CURATION_EXTRAS, // ? BackIn.SYNC_GAME_METADATA, // Meta Edits @@ -315,7 +310,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { const options: ConnectionOptions = { type: 'sqlite', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), - entities: [Game, AdditionalApp, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], + entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109] }; diff --git a/src/back/responses.ts b/src/back/responses.ts index 9adbc2abe..06ea10b6b 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -34,12 +34,12 @@ import * as GameDataManager from './game/GameDataManager'; import * as GameManager from './game/GameManager'; import * as TagManager from './game/TagManager'; import { escapeArgsForShell, GameLauncher, GameLaunchInfo } from './GameLauncher'; -import { importCuration, launchAddAppCuration, launchCuration } from './importGame'; +import { importCuration, launchCuration, launchCurationExtras } from './importGame'; import { ManagedChildProcess } from './ManagedChildProcess'; import { importAllMetaEdits } from './MetaEdit'; import { BackState, BareTag, TagsFile } from './types'; import { pathToBluezip } from './util/Bluezip'; -import { copyError, createAddAppFromLegacy, createContainer, createGameFromLegacy, createPlaylistFromJson, exit, pathExists, procToService, removeService, runService } from './util/misc'; +import { copyError, createChildFromFromLegacyAddApp, createContainer, createGameFromLegacy, createPlaylistFromJson, exit, pathExists, procToService, removeService, runService } from './util/misc'; import { sanitizeFilename } from './util/sanitizeFilename'; import { uuid } from './util/uuid'; @@ -165,6 +165,7 @@ export function registerRequestCallbacks(state: BackState): void { return { done }; }); + // Ardil TODO state.socketServer.register(BackIn.GET_SUGGESTIONS, async (event) => { const startTime = Date.now(); const suggestions: GamePropSuggestions = { @@ -186,6 +187,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_TOTAL, async (event) => { return await GameManager.countGames(); }); @@ -198,46 +200,18 @@ export function registerRequestCallbacks(state: BackState): void { return data; }); + // Ardil TODO state.socketServer.register(BackIn.GET_EXEC, (event) => { return state.execMappings; }); - state.socketServer.register(BackIn.LAUNCH_ADDAPP, async (event, id) => { - const addApp = await GameManager.findAddApp(id); - if (addApp) { - // If it has GameData, make sure it's present - let gameData: GameData | undefined; - if (addApp.parentGame.activeDataId) { - gameData = await GameDataManager.findOne(addApp.parentGame.activeDataId); - if (gameData && !gameData.presentOnDisk) { - // Download GameData - const onProgress = (percent: number) => { - // Sent to PLACEHOLDER download dialog on client - state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_PERCENT, percent); - }; - state.socketServer.broadcast(BackOut.OPEN_PLACEHOLDER_DOWNLOAD_DIALOG); - try { - await GameDataManager.downloadGameData(gameData.id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath), onProgress) - .finally(() => { - // Close PLACEHOLDER download dialog on client, cosmetic delay to look nice - setTimeout(() => { - state.socketServer.broadcast(BackOut.CLOSE_PLACEHOLDER_DOWNLOAD_DIALOG); - }, 250); - }); - } catch (error: any) { - state.socketServer.broadcast(BackOut.OPEN_ALERT, error); - log.info('Game Launcher', `Game Launch Aborted: ${error}`); - return; - } - } - } - await state.apiEmitters.games.onWillLaunchAddApp.fire(addApp); - const platform = addApp.parentGame ? addApp.parentGame : ''; - GameLauncher.launchAdditionalApplication({ - addApp, + state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { + const game = await GameManager.findGame(id); + if (game && game.extras) { + await GameLauncher.launchExtras({ + extrasPath: game.extras, fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, - native: addApp.parentGame && state.preferences.nativePlatforms.some(p => p === platform) || false, execMappings: state.execMappings, lang: state.languageContainer, isDev: state.isDev, @@ -247,16 +221,20 @@ export function registerRequestCallbacks(state: BackState): void { proxy: state.preferences.browserModeProxy, openDialog: state.socketServer.showMessageBoxBack(event.client), openExternal: state.socketServer.openExternal(event.client), - runGame: runGameFactory(state) + runGame: runGameFactory(state), }); - state.apiEmitters.games.onDidLaunchAddApp.fire(addApp); } }); - + // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id); if (game) { + // Ardil TODO not needed? Temp fix, see if it happens. + if (game.parentGameId && !game.parentGame) { + log.debug("Game Launcher", "Fetching parent game."); + game.parentGame = await GameManager.findGame(game.parentGameId) + } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -296,6 +274,34 @@ export function registerRequestCallbacks(state: BackState): void { } } } + // Make sure the parent's GameData is present too. + if (game.parentGame && game.parentGame.activeDataId) { + gameData = await GameDataManager.findOne(game.parentGame.activeDataId); + if (gameData && !gameData.presentOnDisk) { + // Download GameData + const onDetails = (details: DownloadDetails) => { + state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_DETAILS, details); + }; + const onProgress = (percent: number) => { + // Sent to PLACEHOLDER download dialog on client + state.socketServer.broadcast(BackOut.SET_PLACEHOLDER_DOWNLOAD_PERCENT, percent); + }; + state.socketServer.broadcast(BackOut.OPEN_PLACEHOLDER_DOWNLOAD_DIALOG); + try { + await GameDataManager.downloadGameData(gameData.id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath), onProgress, onDetails) + .finally(() => { + // Close PLACEHOLDER download dialog on client, cosmetic delay to look nice + setTimeout(() => { + state.socketServer.broadcast(BackOut.CLOSE_PLACEHOLDER_DOWNLOAD_DIALOG); + }, 250); + }); + } catch (error) { + state.socketServer.broadcast(BackOut.OPEN_ALERT, error); + log.info('Game Launcher', `Game Launch Aborted: ${error}`); + return; + } + } + } // Launch game await GameLauncher.launchGame({ game, @@ -318,10 +324,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAMES, async (event, data) => { await GameManager.updateGames(data); }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME, async (event, data) => { try { const game = await GameManager.save(data); @@ -337,8 +345,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { - const game = await GameManager.removeGameAndAddApps(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); + // Ardil TODO figure out this thing. + const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); state.queries = {}; // Clear entire cache @@ -349,13 +359,16 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + // Ardil TODO check that this was the right move. + /*state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { // Copy and apply new IDs + const newGame = deepCopy(game); + /* Ardil TODO figure this out. const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); newGame.id = uuid(); for (let j = 0; j < newAddApps.length; j++) { @@ -399,8 +412,9 @@ export function registerRequestCallbacks(state: BackState): void { library: result && result.library, gamesTotal: await GameManager.countGames(), }; - }); + });*/ + // Ardil TODO state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); if (playlist) { @@ -417,6 +431,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_PLAYLIST, async (event, filePath, library) => { try { const rawData = await fs.promises.readFile(filePath, 'utf-8'); @@ -452,6 +467,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_ALL_PLAYLISTS, async (event) => { const playlists = await GameManager.findPlaylists(true); for (const playlist of playlists) { @@ -460,6 +476,7 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.send(event.client, BackOut.PLAYLISTS_CHANGE, await GameManager.findPlaylists(state.preferences.browsePageShowExtreme)); }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_PLAYLIST, async (event, id, location) => { const playlist = await GameManager.findPlaylist(id, true); if (playlist) { @@ -469,6 +486,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { const game = await GameManager.findGame(id); @@ -501,10 +519,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { return GameManager.findGame(id); }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); // Verify it's still on disk @@ -520,10 +540,12 @@ export function registerRequestCallbacks(state: BackState): void { return gameData; }); + // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_GAME_DATA, async (event, id) => { return GameDataManager.findGameData(id); }); + // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME_DATAS, async (event, data) => { // Ignore presentOnDisk, client isn't the most aware await Promise.all(data.map(async (d) => { @@ -536,6 +558,7 @@ export function registerRequestCallbacks(state: BackState): void { })); }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME_DATA, async (event, gameDataId) => { const gameData = await GameDataManager.findOne(gameDataId); if (gameData) { @@ -557,10 +580,12 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_GAME_DATA, async (event, gameId, filePath) => { return GameDataManager.importGameData(gameId, filePath, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); }); + // Ardil TODO state.socketServer.register(BackIn.DOWNLOAD_GAME_DATA, async (event, gameDataId) => { const onProgress = (percent: number) => { // Sent to PLACEHOLDER download dialog on client @@ -579,6 +604,7 @@ export function registerRequestCallbacks(state: BackState): void { }); }); + // Ardil TODO This actually is important, don't ignore! state.socketServer.register(BackIn.UNINSTALL_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); if (gameData && gameData.path && gameData.presentOnDisk) { @@ -605,6 +631,7 @@ export function registerRequestCallbacks(state: BackState): void { } }); + // Ardil TODO should be quick. state.socketServer.register(BackIn.ADD_SOURCE_BY_URL, async (event, url) => { const sourceDir = path.join(state.config.flashpointPath, 'Data/Sources'); await fs.promises.mkdir(sourceDir, { recursive: true }); @@ -613,27 +640,33 @@ export function registerRequestCallbacks(state: BackState): void { }); }); + // Ardil TODO state.socketServer.register(BackIn.DELETE_SOURCE, async (event, id) => { return SourceManager.remove(id); }); + // Ardil TODO state.socketServer.register(BackIn.GET_SOURCES, async (event) => { return SourceManager.find(); }); + // Ardil TODO state.socketServer.register(BackIn.GET_SOURCE_DATA, async (event, hashes) => { return GameDataManager.findSourceDataForHashes(hashes); }); + // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { return GameManager.findAllGames(); }); + // Ardil TODO state.socketServer.register(BackIn.RANDOM_GAMES, async (event, data) => { const flatFilters = data.tagFilters ? data.tagFilters.reduce((prev, cur) => prev.concat(cur.tags), []) : []; return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_KEYSET, async (event, library, query) => { query.filter = adjustGameFilter(query.filter); const result = await GameManager.findGamePageKeyset(query.filter, query.orderBy, query.orderReverse, query.searchLimit); @@ -643,6 +676,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_PAGE, async (event, data) => { data.query.filter = adjustGameFilter(data.query.filter); const startTime = new Date(); @@ -771,6 +805,7 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); + // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, @@ -828,6 +863,7 @@ export function registerRequestCallbacks(state: BackState): void { catch (error: any) { log.error('Launcher', error); } }); + // Ardil TODO add pref to make add-apps searchable? Later? state.socketServer.register(BackIn.UPDATE_PREFERENCES, async (event, data, refresh) => { const dif = difObjects(defaultPreferencesData, state.preferences, data); if (dif) { @@ -937,13 +973,14 @@ export function registerRequestCallbacks(state: BackState): void { return playlistGame; }); + // Ardil done state.socketServer.register(BackIn.SAVE_LEGACY_PLATFORM, async (event, platform) => { const translatedGames = []; const tagCache: Record = {}; for (const game of platform.collection.games) { const addApps = platform.collection.additionalApplications.filter(a => a.gameId === game.id); const translatedGame = await createGameFromLegacy(game, tagCache); - translatedGame.addApps = createAddAppFromLegacy(addApps, translatedGame); + translatedGame.children = createChildFromFromLegacyAddApp(addApps, translatedGame); translatedGames.push(translatedGame); } await GameManager.updateGames(translatedGames); @@ -1012,6 +1049,7 @@ export function registerRequestCallbacks(state: BackState): void { return res; }); + // Ardil TODO state.socketServer.register(BackIn.IMPORT_CURATION, async (event, data) => { let error: any | undefined; try { @@ -1040,7 +1078,7 @@ export function registerRequestCallbacks(state: BackState): void { return { error: error || undefined }; }); - state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { + state.socketServer.register(BackIn.LAUNCH_CURATION_EXTRAS, async (event, data) => { const skipLink = (data.key === state.lastLinkedCurationKey); state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; try { @@ -1065,37 +1103,57 @@ export function registerRequestCallbacks(state: BackState): void { } } } - - await launchCuration(data.key, data.meta, data.addApps, data.symlinkCurationContent, skipLink, { - fpPath: path.resolve(state.config.flashpointPath), - htdocsPath: state.preferences.htdocsFolderPath, - native: state.preferences.nativePlatforms.some(p => p === data.meta.platform), - execMappings: state.execMappings, - lang: state.languageContainer, - isDev: state.isDev, - exePath: state.exePath, - appPathOverrides: state.preferences.appPathOverrides, - providers: await getProviders(state), - proxy: state.preferences.browserModeProxy, - openDialog: state.socketServer.showMessageBoxBack(event.client), - openExternal: state.socketServer.openExternal(event.client), - runGame: runGameFactory(state), - }, - state.apiEmitters.games.onWillLaunchCurationGame, - state.apiEmitters.games.onDidLaunchCurationGame); + if (data.meta.extras) { + await launchCurationExtras(data.key, data.meta, data.symlinkCurationContent, skipLink, { + fpPath: path.resolve(state.config.flashpointPath), + htdocsPath: state.preferences.htdocsFolderPath, + execMappings: state.execMappings, + lang: state.languageContainer, + isDev: state.isDev, + exePath: state.exePath, + appPathOverrides: state.preferences.appPathOverrides, + providers: await getProviders(state), + proxy: state.preferences.browserModeProxy, + openDialog: state.socketServer.showMessageBoxBack(event.client), + openExternal: state.socketServer.openExternal(event.client), + runGame: runGameFactory(state) + }); + } } catch (e) { log.error('Launcher', e + ''); } }); - - state.socketServer.register(BackIn.LAUNCH_CURATION_ADDAPP, async (event, data) => { - const skipLink = (data.curationKey === state.lastLinkedCurationKey); - state.lastLinkedCurationKey = data.curationKey; + // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { + const skipLink = (data.key === state.lastLinkedCurationKey); + state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; try { - await launchAddAppCuration(data.curationKey, data.curation, data.symlinkCurationContent, skipLink, { + if (state.serviceInfo) { + // Make sure all 3 relevant server infos are present before considering MAD4FP opt + const configServer = state.serviceInfo.server.find(s => s.name === state.config.server); + const mad4fpServer = state.serviceInfo.server.find(s => s.mad4fp); + const activeServer = state.services.get('server'); + const activeServerInfo = state.serviceInfo.server.find(s => (activeServer && 'name' in activeServer.info && s.name === activeServer.info?.name)); + if (activeServer && configServer && mad4fpServer) { + if (data.mad4fp && activeServerInfo && !activeServerInfo.mad4fp) { + // Swap to mad4fp server + const mad4fpServerCopy = deepCopy(mad4fpServer); + // Set the content folder path as the final parameter + mad4fpServerCopy.arguments.push(getContentFolderByKey(data.key, state.config.flashpointPath)); + await removeService(state, 'server'); + runService(state, 'server', 'Server', state.config.flashpointPath, {}, mad4fpServerCopy); + } else if (!data.mad4fp && activeServerInfo && activeServerInfo.mad4fp && !configServer.mad4fp) { + // Swap to mad4fp server + await removeService(state, 'server'); + runService(state, 'server', 'Server', state.config.flashpointPath, {}, configServer); + } + } + } + + await launchCuration(data.key, data.meta, data.symlinkCurationContent, skipLink, { fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, - native: state.preferences.nativePlatforms.some(p => p === data.platform) || false, + native: state.preferences.nativePlatforms.some(p => p === data.meta.platform), execMappings: state.execMappings, lang: state.languageContainer, isDev: state.isDev, @@ -1107,8 +1165,8 @@ export function registerRequestCallbacks(state: BackState): void { openExternal: state.socketServer.openExternal(event.client), runGame: runGameFactory(state), }, - state.apiEmitters.games.onWillLaunchCurationAddApp, - state.apiEmitters.games.onDidLaunchCurationAddApp); + state.apiEmitters.games.onWillLaunchCurationGame, + state.apiEmitters.games.onDidLaunchCurationGame); } catch (e) { log.error('Launcher', e + ''); } @@ -1138,6 +1196,7 @@ export function registerRequestCallbacks(state: BackState): void { exit(state); }); + // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { const game = await GameManager.findGame(id); if (game) { @@ -1200,6 +1259,7 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); + // Ardil TODO what is this? state.socketServer.register(BackIn.RUN_COMMAND, async (event, command, args = []) => { // Find command const c = state.registry.commands.get(command); @@ -1310,6 +1370,7 @@ function adjustGameFilter(filterOpts: FilterGameOpts): FilterGameOpts { return filterOpts; } +// Ardil TODO /** * Creates a function that will run any game launch info given to it and register it as a service */ @@ -1347,6 +1408,7 @@ function runGameFactory(state: BackState) { }; } +// Ardil TODO function createCommand(filename: string, useWine: boolean, execFile: boolean): string { // This whole escaping thing is horribly broken. We probably want to switch // to an array representing the argv instead and not have a shell @@ -1370,6 +1432,7 @@ function createCommand(filename: string, useWine: boolean, execFile: boolean): s * @param command Command to run * @param args Arguments for the command */ +// Ardil TODO what is this? async function runCommand(state: BackState, command: string, args: any[] = []): Promise { const callback = state.registry.commands.get(command); let res = undefined; @@ -1389,6 +1452,7 @@ async function runCommand(state: BackState, command: string, args: any[] = []): /** * Returns a set of AppProviders from all extension registered Applications, complete with callbacks to run them. */ +// Ardil TODO async function getProviders(state: BackState): Promise { return state.extensionsService.getContributions('applications') .then(contributions => { diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index fd8041b3a..7f207e204 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -3,7 +3,6 @@ import { createTagsFromLegacy } from '@back/importGame'; import { ManagedChildProcess, ProcessOpts } from '@back/ManagedChildProcess'; import { SocketServer } from '@back/SocketServer'; import { BackState, ShowMessageBoxFunc, ShowOpenDialogFunc, ShowSaveDialogFunc, StatusState } from '@back/types'; -import { AdditionalApp } from '@database/entity/AdditionalApp'; import { Game } from '@database/entity/Game'; import { Playlist } from '@database/entity/Playlist'; import { Tag } from '@database/entity/Tag'; @@ -15,6 +14,7 @@ import { Legacy_IAdditionalApplicationInfo, Legacy_IGameInfo } from '@shared/leg import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import * as child_process from 'child_process'; import * as fs from 'fs'; +import { add } from 'node-7z'; import * as path from 'path'; import { promisify } from 'util'; import { uuid } from './uuid'; @@ -165,8 +165,53 @@ export async function execProcess(state: BackState, proc: IBackProcessInfo, sync } } -export function createAddAppFromLegacy(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): AdditionalApp[] { - return addApps.map(a => { +export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): Game[] { + let retVal: Game[] = []; + for (const addApp of addApps) { + if (addApp.applicationPath === ':message:') { + game.message = addApp.launchCommand; + } else if (addApp.applicationPath === ':extras:') { + game.extras = addApp.launchCommand; + game.extrasName = addApp.name; + } else { + let newGame = new Game(); + Object.assign(newGame, { + id: addApp.id, + title: addApp.name, + applicationPath: addApp.applicationPath, + launchCommand: addApp.launchCommand, + parentGame: game, + library: game.library, + alternateTitles: "", + series: "", + developer: "", + publisher: "", + dateAdded: "0000-00-00 00:00:00.000", + dateModified: "0000-00-00 00:00:00.000", + platform: "", + broken: false, + extreme: game.extreme, + playMode: "", + status: "", + notes: "", + source: "", + releaseDate: "", + version: "", + originalDescription: "", + language: "", + orderTitle: addApp.name.toLowerCase(), + activeDataId: undefined, + activeDataOnDisk: false, + tags: [], + extras: undefined, + extrasName: undefined, + message: undefined, + children: [] + }) + retVal.push(newGame); + } + } + /*return addApps.map(a => { return { id: a.id, name: a.name, @@ -176,14 +221,15 @@ export function createAddAppFromLegacy(addApps: Legacy_IAdditionalApplicationInf waitForExit: a.waitForExit, parentGame: game }; - }); + });*/ + return retVal; } export async function createGameFromLegacy(game: Legacy_IGameInfo, tagCache: Record): Promise { const newGame = new Game(); Object.assign(newGame, { id: game.id, - parentGameId: game.id, + parentGameId: null, title: game.title, alternateTitles: game.alternateTitles, series: game.series, @@ -208,7 +254,7 @@ export async function createGameFromLegacy(game: Legacy_IGameInfo, tagCache: Rec library: game.library, orderTitle: game.orderTitle, placeholder: false, - addApps: [], + children: [], activeDataOnDisk: false }); return newGame; diff --git a/src/database/entity/AdditionalApp.ts b/src/database/entity/AdditionalApp.ts deleted file mode 100644 index ac6f499ab..000000000 --- a/src/database/entity/AdditionalApp.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { Game } from './Game'; - -@Entity() -export class AdditionalApp { - @PrimaryGeneratedColumn('uuid') - /** ID of the additional application (unique identifier) */ - id: string; - @Column() - /** Path to the application that runs the additional application */ - applicationPath: string; - @Column() - /** - * If the additional application should run before the game. - * (If true, this will always run when the game is launched) - * (If false, this will only run when specifically launched) - */ - autoRunBefore: boolean; - @Column() - /** Command line argument(s) passed to the application to launch the game */ - launchCommand: string; - @Column() - /** Name of the additional application */ - @Column({collation: 'NOCASE'}) - name: string; - @Column() - /** Wait for this to exit before the Game will launch (if starting before launch) */ - waitForExit: boolean; - @ManyToOne(type => Game, game => game.addApps) - /** Parent of this add app */ - parentGame: Game; -} diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 6adba1c99..987b04cc3 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,5 +1,4 @@ import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; -import { AdditionalApp } from './AdditionalApp'; import { GameData } from './GameData'; import { Tag } from './Tag'; @@ -17,12 +16,18 @@ export class Game { /** ID of the game (unique identifier) */ id: string; - @ManyToOne(type => Game) + @ManyToOne(type => Game, game => game.children) parentGame?: Game; @Column({ nullable: true }) parentGameId?: string; + @OneToMany(type => Game, game => game.parentGame, { + cascade: true, + eager: true + }) + children: Game[]; + @Column({collation: 'NOCASE'}) @Index('IDX_gameTitle') /** Full title of the game */ @@ -120,13 +125,6 @@ export class Game { /** The title but reconstructed to be suitable for sorting and ordering (and not be shown visually) */ orderTitle: string; - @OneToMany(type => AdditionalApp, addApp => addApp.parentGame, { - cascade: true, - eager: true - }) - /** All attached Additional Apps of a game */ - addApps: AdditionalApp[]; - /** If the game is a placeholder (and can therefore not be saved) */ placeholder: boolean; @@ -141,6 +139,15 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; + @Column({ nullable: true }) + extras?: string; + + @Column({ nullable: true }) + extrasName?: string; + + @Column({ nullable: true }) + message?: string; + // This doesn't run... sometimes. @BeforeUpdate() updateTagsStr() { diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 08d2cdf91..04dde8c4f 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -26,7 +26,6 @@ import { toForcedURL } from '../Util'; import { LangContext } from '../util/lang'; import { pathTo7z } from '../util/SevenZip'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; -import { CurateBoxAddApp } from './CurateBoxAddApp'; import { CurateBoxRow } from './CurateBoxRow'; import { CurateBoxWarnings, CurationWarnings, getWarningCount } from './CurateBoxWarnings'; import { DropdownInputField } from './DropdownInputField'; @@ -87,7 +86,7 @@ export function CurateBox(props: CurateBoxProps) { saveThrottle(() => { if (props.curation) { const metaPath = path.join(getCurationFolder2(props.curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(props.curation.meta, props.tagCategories, props.curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(props.curation.meta, props.tagCategories)); fs.writeFile(metaPath, meta); console.log('Auto-Saved Curation'); } @@ -205,6 +204,10 @@ export function CurateBox(props: CurateBoxProps) { const onOriginalDescriptionChange = useOnInputChange('originalDescription', key, props.dispatch); const onCurationNotesChange = useOnInputChange('curationNotes', key, props.dispatch); const onMountParametersChange = useOnInputChange('mountParameters', key, props.dispatch); + const onParentGameIdChange = useOnInputChange('parentGameId', key, props.dispatch); + const onMessageChange = useOnInputChange('message', key, props.dispatch); + const onExtrasChange = useOnInputChange('extras', key, props.dispatch); + const onExtrasNameChange = useOnInputChange('extrasName', key, props.dispatch); // Callbacks for the fields (onItemSelect) const onPlayModeSelect = useCallback(transformOnItemSelect(onPlayModeChange), [onPlayModeChange]); const onStatusSelect = useCallback(transformOnItemSelect(onStatusChange), [onStatusChange]); @@ -307,7 +310,6 @@ export function CurateBox(props: CurateBoxProps) { await window.Shared.back.request(BackIn.LAUNCH_CURATION, { key: props.curation.key, meta: props.curation.meta, - addApps: props.curation.addApps.map(addApp => addApp.meta), mad4fp: mad4fp, symlinkCurationContent: props.symlinkCurationContent }); @@ -378,42 +380,6 @@ export function CurateBox(props: CurateBoxProps) { }); } }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when the new additional application button is clicked - const onNewAddApp = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'normal' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when adding an Extras add app - const onAddExtras = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'extras' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); - // Callback for when adding a Message add app - const onAddMessage = useCallback(() => { - if (props.curation) { - props.dispatch({ - type: 'new-addapp', - payload: { - key: props.curation.key, - type: 'message' - } - }); - } - }, [props.dispatch, props.curation && props.curation.key]); // Callback for when the export button is clicked const onExportClick = useCallback(async () => { if (props.curation) { @@ -468,7 +434,7 @@ export function CurateBox(props: CurateBoxProps) { .catch((error) => { /* No file is okay, ignore error */ }); // Save working meta const metaPath = path.join(getCurationFolder2(curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories)); const statusProgress = newProgress(props.curation.key, progressDispatch); ProgressDispatch.setText(statusProgress, 'Exporting Curation...'); ProgressDispatch.setUsePercentDone(statusProgress, false); @@ -520,36 +486,6 @@ export function CurateBox(props: CurateBoxProps) { } return false; }, [props.curation]); - // Render additional application elements - const addApps = useMemo(() => ( - <> - { strings.browse.additionalApplications }: - { props.curation && props.curation.addApps.length > 0 ? ( - - - { props.curation.addApps.map(addApp => ( - - )) } - -
- ) : undefined } - - ), [ - props.curation && props.curation.addApps, - props.curation && props.curation.key, - props.symlinkCurationContent, - props.dispatch, - native, - disabled - ]); // Count the number of collisions const collisionCount: number | undefined = useMemo(() => { @@ -848,33 +784,30 @@ export function CurateBox(props: CurateBoxProps) { className={curationNotes.length > 0 ? 'input-field--info' : ''} { ...sharedInputProps } /> + + + + + + + + +
- {/* Additional Application */} -
- {addApps} -
-
- - - -
-
-
-
{/* Content */}
diff --git a/src/renderer/components/CurateBoxAddApp.tsx b/src/renderer/components/CurateBoxAddApp.tsx deleted file mode 100644 index 665ba2bd1..000000000 --- a/src/renderer/components/CurateBoxAddApp.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { BackIn } from '@shared/back/types'; -import { EditAddAppCuration, EditAddAppCurationMeta } from '@shared/curate/types'; -import * as React from 'react'; -import { useCallback } from 'react'; -import { CurationAction } from '../context/CurationContext'; -import { LangContext } from '../util/lang'; -import { CurateBoxRow } from './CurateBoxRow'; -import { InputField } from './InputField'; -import { SimpleButton } from './SimpleButton'; - -export type CurateBoxAddAppProps = { - /** Key of the curation the displayed additional application belongs to. */ - curationKey: string; - /** Meta data for the additional application to display. */ - curation: EditAddAppCuration; - /** If editing any fields of this should be disabled. */ - disabled?: boolean; - /** Dispatcher for the curate page state reducer. */ - dispatch: React.Dispatch; - /** Platform of the game this belongs to. */ - platform?: string; - /** Whether to symlink curation content before running */ - symlinkCurationContent: boolean; - /** Callback for the "onKeyDown" event for all input fields. */ - onInputKeyDown?: (event: React.KeyboardEvent) => void; -}; - -export function CurateBoxAddApp(props: CurateBoxAddAppProps) { - // Callbacks for the fields (onChange) - const curationKey = props.curationKey; - const key = props.curation.key; - const onHeadingChange = useOnInputChange('heading', key, curationKey, props.dispatch); - const onApplicationPathChange = useOnInputChange('applicationPath', key, curationKey, props.dispatch); - const onLaunchCommandChange = useOnInputChange('launchCommand', key, curationKey, props.dispatch); - // Misc. - const editable = true; - const disabled = props.disabled; - // Localized strings - const strings = React.useContext(LangContext); - const specialType = props.curation.meta.applicationPath === ':extras:' || props.curation.meta.applicationPath === ':message:'; - let lcString = strings.browse.launchCommand; - let lcPlaceholderString = strings.browse.noLaunchCommand; - // Change Launch Command strings depending on add app type - switch (props.curation.meta.applicationPath) { - case ':message:': - lcString = strings.curate.message; - lcPlaceholderString = strings.curate.noMessage; - break; - case ':extras:': - lcString = strings.curate.folderName; - lcPlaceholderString = strings.curate.noFolderName; - break; - } - // Callback for the "remove" button - const onRemove = useCallback(() => { - props.dispatch({ - type: 'remove-addapp', - payload: { - curationKey: props.curationKey, - key: props.curation.key - } - }); - }, [props.curationKey, props.curation.key, props.dispatch]); - // Callback for the "run" button - const onRun = useCallback(() => { - return window.Shared.back.request(BackIn.LAUNCH_CURATION_ADDAPP, { - curationKey: props.curationKey, - curation: props.curation, - platform: props.platform, - symlinkCurationContent: props.symlinkCurationContent - }); - }, [props.curation && props.curation.meta && props.curationKey, props.symlinkCurationContent, props.platform]); - // Render - return ( - - - - - { specialType ? undefined : ( - - - - ) } - - - - - - - ); -} - -type InputElement = HTMLInputElement | HTMLTextAreaElement; - -/** Subset of the input elements on change event, with only the properties used by the callbacks. */ -type InputElementOnChangeEvent = { - currentTarget: { - value: React.ChangeEvent['currentTarget']['value'] - } -} - -/** - * Create a callback for InputField's onChange. - * When called, the callback will set the value of a metadata property to the value of the input field. - * @param property Property the input field should change. - * @param curationKey Key of the curation the additional application belongs to. - * @param key Key of the additional application to edit. - * @param dispatch Dispatcher to use. - */ -function useOnInputChange(property: keyof EditAddAppCurationMeta, key: string, curationKey: string, dispatch: React.Dispatch) { - return useCallback((event: InputElementOnChangeEvent) => { - if (key !== undefined) { - dispatch({ - type: 'edit-addapp-meta', - payload: { - curationKey: curationKey, - key: key, - property: property, - value: event.currentTarget.value - } - }); - } - }, [dispatch, key]); -} diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 115f712b7..c035e15f7 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -28,7 +28,8 @@ import { GameImageSplit } from './GameImageSplit'; import { ImagePreview } from './ImagePreview'; import { InputElement, InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; -import { RightBrowseSidebarAddApp } from './RightBrowseSidebarAddApp'; +import { RightBrowseSidebarChild } from './RightBrowseSidebarAddApp'; +import { RightBrowseSidebarExtra } from './RightBrowseSidebarExtra'; import { SimpleButton } from './SimpleButton'; import { TagInputField } from './TagInputField'; @@ -53,6 +54,8 @@ type OwnProps = { onDeselectPlaylist: () => void; /** Called when the playlist notes for the selected game has been changed */ onEditPlaylistNotes: (text: string) => void; + /** Called when a child game needs to be deleted. */ + onDeleteGame: (gameId: string) => void; /** If the "edit mode" is currently enabled */ isEditing: boolean; /** If the selected game is a new game being created */ @@ -109,6 +112,7 @@ export class RightBrowseSidebar extends React.Component this.props.onEditGame({ applicationPath: text })); onNotesChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ notes: text })); onOriginalDescriptionChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ originalDescription: text })); + onMessageChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ message: text })); onBrokenChange = this.wrapOnCheckBoxChange(game => { if (this.props.currentGame) { this.props.onEditGame({ broken: !this.props.currentGame.broken }); @@ -194,7 +198,7 @@ export class RightBrowseSidebar extends React.Component
+ {game.message ? +
+

{strings.message}:

+ +
+ : undefined}

{strings.dateAdded}:

+ { game.broken || editable ? (
) : undefined } {/* -- Additional Applications -- */} - { editable || (currentAddApps && currentAddApps.length > 0) ? ( + { editable || (currentChildren && currentChildren.length > 0) ? (

{strings.additionalApplications}:

- { editable ? ( - - ) : undefined }
- { currentAddApps && currentAddApps.map((addApp) => ( - ( + + onLaunch={this.onChildLaunch} + onDelete={this.onChildDelete} /> )) } + {game.extras && game.extrasName ? + : undefined + }
) : undefined } {/* -- Application Path & Launch Command -- */} @@ -920,28 +938,26 @@ export class RightBrowseSidebar extends React.Component { + onChildDelete = (childId: string): void => { if (this.props.currentGame) { - const newAddApps = deepCopy(this.props.currentGame.addApps); - if (!newAddApps) { throw new Error('editAddApps is missing.'); } - const index = newAddApps.findIndex(addApp => addApp.id === addAppId); + const newChildren = deepCopy(this.props.currentGame.children); + if (!newChildren) { throw new Error('editAddApps is missing.'); } + const index = newChildren.findIndex(addApp => addApp.id === childId); if (index === -1) { throw new Error('Cant remove additional application because it was not found.'); } - newAddApps.splice(index, 1); - this.props.onEditGame({ addApps: newAddApps }); + newChildren.splice(index, 1); + this.props.onEditGame({children: newChildren}); + this.props.onDeleteGame(childId); } } - onNewAddAppClick = (): void => { - if (!this.props.currentGame) { throw new Error('Unable to add a new AddApp. "currentGame" is missing.'); } - const newAddApp = ModelUtils.createAddApp(this.props.currentGame); - newAddApp.id = uuid(); - this.props.onEditGame({ addApps: [...this.props.currentGame.addApps, ...[newAddApp]] }); - } - onScreenshotClick = (): void => { this.setState({ showPreview: true }); } diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 8890b1ed7..8b714a0ab 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -1,4 +1,4 @@ -import { AdditionalApp } from '@database/entity/AdditionalApp'; +import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; @@ -7,41 +7,39 @@ import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; -export type RightBrowseSidebarAddAppProps = { +export type RightBrowseSidebarChildProps = { /** Additional Application to show and edit */ - addApp: AdditionalApp; + child: Game; /** Called when a field is edited */ onEdit?: () => void; /** Called when a field is edited */ - onDelete?: (addAppId: string) => void; + onDelete?: (childId: string) => void; /** Called when the launch button is clicked */ - onLaunch?: (addAppId: string) => void; + onLaunch?: (childId: string) => void; /** If the editing is disabled (it cant go into "edit mode") */ editDisabled?: boolean; }; -export interface RightBrowseSidebarAddApp { +export interface RightBrowseSidebarChild { context: LangContainer; } /** Displays an additional application for a game in the right sidebar of BrowsePage. */ -export class RightBrowseSidebarAddApp extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.name = text; }); +export class RightBrowseSidebarChild extends React.Component { + onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); - onAutoRunBeforeChange = this.wrapOnCheckBoxChange((addApp) => { addApp.autoRunBefore = !addApp.autoRunBefore; }); - onWaitForExitChange = this.wrapOnCheckBoxChange((addApp) => { addApp.waitForExit = !addApp.waitForExit; }); render() { const allStrings = this.context; const strings = allStrings.browse; - const { addApp, editDisabled } = this.props; + const { child: addApp, editDisabled } = this.props; return (
{/* Title & Launch Button */}
@@ -71,27 +69,9 @@ export class RightBrowseSidebarAddApp extends React.Component
- {/* Auto Run Before */} -
-
- -

{strings.autoRunBefore}

-
-
+ {/* Wait for Exit */}
-
- -

{strings.waitForExit}

-
{/* Delete Button */} { !editDisabled ? ( { if (this.props.onLaunch) { - this.props.onLaunch(this.props.addApp.id); + this.props.onLaunch(this.props.child.id); } } onDeleteClick = (): void => { if (this.props.onDelete) { - this.props.onDelete(this.props.addApp.id); + this.props.onDelete(this.props.child.id); } } @@ -138,9 +118,9 @@ export class RightBrowseSidebarAddApp extends React.Component void): (event: React.ChangeEvent) => void { + wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { return (event) => { - const addApp = this.props.addApp; + const addApp = this.props.child; if (addApp) { func(addApp, event.currentTarget.value); this.forceUpdate(); @@ -149,10 +129,10 @@ export class RightBrowseSidebarAddApp extends React.Component void) { + wrapOnCheckBoxChange(func: (addApp: Game) => void) { return () => { if (!this.props.editDisabled) { - func(this.props.addApp); + func(this.props.child); this.onEdit(); this.forceUpdate(); } diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx new file mode 100644 index 000000000..f19c733ff --- /dev/null +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -0,0 +1,125 @@ +import { Game } from '@database/entity/Game'; +import { LangContainer } from '@shared/lang'; +import * as React from 'react'; +import { LangContext } from '../util/lang'; +import { CheckBox } from './CheckBox'; +import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; +import { InputField } from './InputField'; +import { OpenIcon } from './OpenIcon'; + +export type RightBrowseSidebarExtraProps = { + /** Extras to show and edit */ + // These two are xplicitly non-nullable. + extrasPath: string; + extrasName: string; + game: Game; + /** Called when a field is edited */ + onEdit?: () => void; + /** Called when a field is edited */ + onDelete?: (gameId: string) => void; + /** Called when the launch button is clicked */ + onLaunch?: (gameId: string) => void; + /** If the editing is disabled (it cant go into "edit mode") */ + editDisabled?: boolean; +}; + +export interface RightBrowseSidebarExtra { + context: LangContainer; +} + +/** Displays an additional application for a game in the right sidebar of BrowsePage. */ +export class RightBrowseSidebarExtra extends React.Component { + onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); + onExtrasNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); + onExtrasPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); + + render() { + const allStrings = this.context; + const strings = allStrings.browse; + const { extrasPath, extrasName, editDisabled } = this.props; + return ( +
+ {/* Title & Launch Button */} +
+ + +
+ { editDisabled ? undefined : ( + <> + {/* Launch Command */} +
+

{strings.extras}:

+ +
+ + ) } +
+ ); + } + + renderDeleteButton({ confirm, extra }: ConfirmElementArgs): JSX.Element { + const className = 'browse-right-sidebar__additional-application__delete-button'; + return ( +
+ +
+ ); + } + + onLaunchClick = (): void => { + if (this.props.onLaunch) { + this.props.onLaunch(this.props.game.id); + } + } + + onDeleteClick = (): void => { + if (this.props.onDelete) { + this.props.onDelete(this.props.game.id); + } + } + + onEdit(): void { + if (this.props.onEdit) { + this.props.onEdit(); + } + } + + /** Create a wrapper for a EditableTextWrap's onEditDone callback (this is to reduce redundancy). */ + wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { + return (event) => { + const addApp = this.props.game; + if (addApp) { + func(addApp, event.currentTarget.value); + this.forceUpdate(); + } + }; + } + + /** Create a wrapper for a CheckBox's onChange callback (this is to reduce redundancy). */ + wrapOnCheckBoxChange(func: (addApp: Game) => void) { + return () => { + if (!this.props.editDisabled) { + func(this.props.game); + this.onEdit(); + this.forceUpdate(); + } + }; + } + + static contextType = LangContext; +} diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 485b8c654..84e5f19df 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -286,6 +286,7 @@ export class BrowsePage extends React.Component !c.delete)) { const metaPath = path.join(getCurationFolder2(curation), 'meta.yaml'); - const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories, curation.addApps)); + const meta = YAML.stringify(convertEditToCurationMetaFile(curation.meta, props.tagCategories)); try { fs.writeFileSync(metaPath, meta); } catch (error: any) { @@ -743,7 +743,6 @@ async function loadCurationFolder(key: string, fullPath: string, defaultGameMeta const loadedCuration: EditCuration = { key: key, meta: {}, - addApps: [], thumbnail: createCurationIndexImage(), screenshot: createCurationIndexImage(), content: [], @@ -790,13 +789,6 @@ async function loadCurationFolder(key: string, fullPath: string, defaultGameMeta await readCurationMeta(metaYamlPath, defaultGameMetaValues) .then(async (parsedMeta) => { loadedCuration.meta = parsedMeta.game; - for (let i = 0; i < parsedMeta.addApps.length; i++) { - const meta = parsedMeta.addApps[i]; - loadedCuration.addApps.push({ - key: uuid(), - meta: meta, - }); - } }) .catch((error) => { const formedMessage = `Error Parsing Curation Meta at ${metaYamlPath} - ${error.message}`; diff --git a/src/renderer/context/CurationContext.ts b/src/renderer/context/CurationContext.ts index 295e19d7a..ccce946a4 100644 --- a/src/renderer/context/CurationContext.ts +++ b/src/renderer/context/CurationContext.ts @@ -1,6 +1,6 @@ import { GameMetaDefaults } from '@shared/curate/defaultValues'; -import { generateExtrasAddApp, generateMessageAddApp, ParsedCurationMeta } from '@shared/curate/parse'; -import { CurationIndexImage, EditAddAppCurationMeta, EditCuration, EditCurationMeta, IndexedContent } from '@shared/curate/types'; +import { ParsedCurationMeta } from '@shared/curate/parse'; +import { CurationIndexImage, EditCuration, EditCurationMeta, IndexedContent } from '@shared/curate/types'; import { createContextReducer } from '../context-reducer/contextReducer'; import { ReducerAction } from '../context-reducer/interfaces'; import { createCurationIndexImage } from '../curate/importCuration'; @@ -34,7 +34,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const index = nextCurations.findIndex(c => c.key === action.payload.key); if (index !== -1) { const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; // Mark curation for deletion nextCuration.delete = true; nextCurations[index] = nextCuration; @@ -47,7 +47,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const index = nextCurations.findIndex(c => c.key === action.payload.key); if (index !== -1) { const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; // Mark curation for deletion nextCuration.deleted = true; nextCurations[index] = nextCuration; @@ -59,17 +59,9 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; const parsedMeta = action.payload.parsedMeta; nextCuration.meta = parsedMeta.game; - nextCuration.addApps = []; - for (let i = 0; i < parsedMeta.addApps.length; i++) { - const meta = parsedMeta.addApps[i]; - nextCuration.addApps.push({ - key: uuid(), - meta: meta, - }); - } nextCurations[index] = nextCuration; return { ...prevState, curations: nextCurations }; } @@ -78,7 +70,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.thumbnail = action.payload.image; nextCuration.thumbnail.version = prevCuration.thumbnail.version + 1; nextCurations[index] = nextCuration; @@ -89,7 +81,7 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.screenshot = action.payload.image; nextCuration.screenshot.version = prevCuration.screenshot.version + 1; nextCurations[index] = nextCuration; @@ -100,59 +92,11 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur const nextCurations = [ ...prevState.curations ]; const index = ensureCurationIndex(nextCurations, action.payload.key); const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; + const nextCuration = { ...prevCuration }; nextCuration.content = action.payload.content; nextCurations[index] = nextCuration; return { ...prevState, curations: nextCurations }; } - // Add an empty additional application to a curation - case 'new-addapp': { - const nextCurations = [ ...prevState.curations ]; - const index = nextCurations.findIndex(c => c.key === action.payload.key); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - switch (action.payload.type) { - case 'normal': - nextCuration.addApps.push({ - key: uuid(), - meta: {} - }); - break; - case 'extras': - nextCuration.addApps.push({ - key: uuid(), - meta: generateExtrasAddApp('') - }); - break; - case 'message': - nextCuration.addApps.push({ - key: uuid(), - meta: generateMessageAddApp('') - }); - break; - } - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } - // Remove an additional application from a curation - case 'remove-addapp': { - const nextCurations = [ ...prevState.curations ]; - const index = nextCurations.findIndex(c => c.key === action.payload.curationKey); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - const addAppIndex = nextCuration.addApps.findIndex(c => c.key === action.payload.key); - if (addAppIndex >= 0) { - nextCuration.addApps.splice(addAppIndex, 1); - } - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } // Edit curation's meta case 'edit-curation-meta': { // Find the curation @@ -169,30 +113,6 @@ function curationReducer(prevState: CurationsState, action: CurationAction): Cur } return { ...prevState, curations: nextCurations }; } - // Edit additional application's meta - case 'edit-addapp-meta': { - // Find the curation - const nextCurations = [ ...prevState.curations ]; // (New curations array to replace the current) - const index = nextCurations.findIndex(c => c.key === action.payload.curationKey); - if (index >= 0) { - // Copy the previous curation (and the nested addApps array) - const prevCuration = nextCurations[index]; - const nextCuration = { ...prevCuration, addApps: [ ...prevCuration.addApps ] }; - // Find the additional application - const addAppIndex = prevCuration.addApps.findIndex(c => c.key === action.payload.key); - if (addAppIndex >= 0) { - const prevAddApp = prevCuration.addApps[addAppIndex]; - const nextAddApp = { ...prevAddApp }; - // Replace the value (in the copied meta) - nextAddApp.meta[action.payload.property] = action.payload.value; - // Replace the previous additional application with the new (in the copied array) - nextCuration.addApps[addAppIndex] = nextAddApp; - } - // Replace the previous curation with the new (in the copied array) - nextCurations[index] = nextCuration; - } - return { ...prevState, curations: nextCurations }; - } // Sorts all curations A-Z case 'sort-curations': { const newCurations = [...prevState.curations].sort((a, b) => { @@ -263,7 +183,6 @@ export function createEditCuration(key: string): EditCuration { key: key, meta: {}, content: [], - addApps: [], thumbnail: createCurationIndexImage(), screenshot: createCurationIndexImage(), locked: false, @@ -307,16 +226,6 @@ export type CurationAction = ( key: string; content: IndexedContent[]; }> | - /** Add an empty additional application to curation */ - ReducerAction<'new-addapp', { - key: string; - type: 'normal' | 'extras' | 'message'; - }> | - /** Remove an additional application (by key) from a curation */ - ReducerAction<'remove-addapp', { - curationKey: string; - key: string; - }> | /** Edit the value of a curation's meta's property. */ ReducerAction<'edit-curation-meta', { /** Key of the curation to change. */ @@ -326,17 +235,6 @@ export type CurationAction = ( /** Value to set the property to. */ value: EditCurationMeta[keyof EditCurationMeta]; }> | - /** Edit the value of an additional application's meta's property. */ - ReducerAction<'edit-addapp-meta', { - /** Key of the curation the additional application belongs to. */ - curationKey: string; - /** Key of the additional application to change. */ - key: string; - /** Name of the property to change. */ - property: keyof EditAddAppCurationMeta; - /** Value to set the property to. */ - value: EditAddAppCurationMeta[keyof EditAddAppCurationMeta]; - }> | /** Sort Curations A-Z */ ReducerAction<'sort-curations', {}> | /** Change the lock status of a curation. */ diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 122f1ed90..946350b79 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -14,7 +14,7 @@ import { SocketTemplate } from '@shared/socket/types'; import { MessageBoxOptions, OpenDialogOptions, OpenExternalOptions, SaveDialogOptions } from 'electron'; import { GameData, TagAlias, TagFilterGroup } from 'flashpoint-launcher'; import { AppConfigData, AppExtConfigData } from '../config/interfaces'; -import { EditAddAppCuration, EditAddAppCurationMeta, EditCuration, EditCurationMeta } from '../curate/types'; +import { EditCuration, EditCurationMeta } from '../curate/types'; import { ExecMapping, GamePropSuggestions, IService, ProcessAction } from '../interfaces'; import { LangContainer, LangFile } from '../lang'; import { ILogEntry, ILogPreEntry, LogLevel } from '../Log/interface'; @@ -47,7 +47,7 @@ export enum BackIn { DELETE_GAME, DUPLICATE_GAME, EXPORT_GAME, - LAUNCH_ADDAPP, + LAUNCH_EXTRAS, SAVE_IMAGE, DELETE_IMAGE, ADD_LOG, @@ -67,7 +67,7 @@ export enum BackIn { SAVE_LEGACY_PLATFORM, IMPORT_CURATION, LAUNCH_CURATION, - LAUNCH_CURATION_ADDAPP, + LAUNCH_CURATION_EXTRAS, QUIT, // Sources @@ -199,7 +199,7 @@ export type BackInTemplate = SocketTemplate BrowseChangeData; [BackIn.DUPLICATE_GAME]: (id: string, dupeImages: boolean) => BrowseChangeData; [BackIn.EXPORT_GAME]: (id: string, location: string, metaOnly: boolean) => void; - [BackIn.LAUNCH_ADDAPP]: (id: string) => void; + [BackIn.LAUNCH_EXTRAS]: (id: string) => void; [BackIn.SAVE_IMAGE]: (folder: string, id: string, content: string) => void; [BackIn.DELETE_IMAGE]: (folder: string, id: string) => void; [BackIn.ADD_LOG]: (data: ILogPreEntry & { logLevel: LogLevel }) => void; @@ -219,7 +219,7 @@ export type BackInTemplate = SocketTemplate void; [BackIn.IMPORT_CURATION]: (data: ImportCurationData) => ImportCurationResponseData; [BackIn.LAUNCH_CURATION]: (data: LaunchCurationData) => void; - [BackIn.LAUNCH_CURATION_ADDAPP]: (data: LaunchCurationAddAppData) => void; + [BackIn.LAUNCH_CURATION_EXTRAS]: (data: LaunchCurationData) => void; [BackIn.QUIT]: () => void; // Tag funcs @@ -493,18 +493,10 @@ export type ImportCurationResponseData = { export type LaunchCurationData = { key: string; meta: EditCurationMeta; - addApps: EditAddAppCurationMeta[]; mad4fp: boolean; symlinkCurationContent: boolean; } -export type LaunchCurationAddAppData = { - curationKey: string; - curation: EditAddAppCuration; - platform?: string; - symlinkCurationContent: boolean; -} - export type TagSuggestion = { alias?: string; primaryAlias: string; diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index 61fc21321..f0dac9d71 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -1,7 +1,7 @@ import { Game } from '@database/entity/Game'; import { TagCategory } from '@database/entity/TagCategory'; import { ParsedCurationMeta } from './parse'; -import { EditAddAppCuration, EditCurationMeta } from './types'; +import { EditCurationMeta } from './types'; /** * Convert game and its additional applications into a raw object representation in the curation format. @@ -34,38 +34,10 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor parsed['Launch Command'] = game.launchCommand; parsed['Game Notes'] = game.notes; parsed['Original Description'] = game.originalDescription; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - for (let i = 0; i < game.addApps.length; i++) { - const addApp = game.addApps[i]; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.name; - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.name, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Parent Game ID'] = game.parentGameId; + parsed['Extras'] = game.extras; + parsed['Extras Name'] = game.extrasName; + parsed['Message'] = game.message; // Return return parsed; } @@ -75,7 +47,7 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor * @param curation Curation to convert. * @param addApps Additional applications of the curation. */ -export function convertEditToCurationMetaFile(curation: EditCurationMeta, categories: TagCategory[], addApps?: EditAddAppCuration[]): CurationMetaFile { +export function convertEditToCurationMetaFile(curation: EditCurationMeta, categories: TagCategory[]): CurationMetaFile { const parsed: CurationMetaFile = {}; const tagCategories = curation.tags ? curation.tags.map(t => { const cat = categories.find(c => c.id === t.categoryId); @@ -104,42 +76,10 @@ export function convertEditToCurationMetaFile(curation: EditCurationMeta, catego parsed['Original Description'] = curation.originalDescription; parsed['Curation Notes'] = curation.curationNotes; parsed['Mount Parameters'] = curation.mountParameters; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - if (addApps) { - for (let i = 0; i < addApps.length; i++) { - const addApp = addApps[i].meta; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.heading; - if (heading) { - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.heading, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Parent Game ID'] = curation.parentGameId; + parsed['Extras'] = curation.extras; + parsed['Extras Name'] = curation.extrasName; + parsed['Message'] = curation.message; // Return return parsed; } @@ -178,42 +118,10 @@ export function convertParsedToCurationMeta(curation: ParsedCurationMeta, catego parsed['Original Description'] = curation.game.originalDescription; parsed['Curation Notes'] = curation.game.curationNotes; parsed['Mount Parameters'] = curation.game.mountParameters; - // Add-apps meta - const parsedAddApps: CurationFormatAddApps = {}; - if (curation.addApps) { - for (let i = 0; i < curation.addApps.length; i++) { - const addApp = curation.addApps[i]; - if (addApp.applicationPath === ':extras:') { - parsedAddApps['Extras'] = addApp.launchCommand; - } else if (addApp.applicationPath === ':message:') { - parsedAddApps['Message'] = addApp.launchCommand; - } else { - let heading = addApp.heading; - if (heading) { - // Check if the property name is already in use - if (parsedAddApps[heading] !== undefined) { - // Look for an available name (by appending a number after it) - let index = 2; - while (true) { - const testHeading = `${heading} (${index})`; - if (parsedAddApps[testHeading] === undefined) { - heading = testHeading; - break; - } - index += 1; - } - } - // Add add-app - parsedAddApps[heading] = { - 'Heading': addApp.heading, - 'Application Path': addApp.applicationPath, - 'Launch Command': addApp.launchCommand, - }; - } - } - } - } - parsed['Additional Applications'] = parsedAddApps; + parsed['Extras'] = curation.game.extras; + parsed['Extras Name'] = curation.game.extrasName; + parsed['Message'] = curation.game.message; + parsed['Parent Game ID'] = curation.game.parentGameId; // Return return parsed; } @@ -239,20 +147,10 @@ type CurationMetaFile = { 'Alternate Titles'?: string; 'Library'?: string; 'Version'?: string; - 'Additional Applications'?: CurationFormatAddApps; 'Curation Notes'?: string; 'Mount Parameters'?: string; -}; - -type CurationFormatAddApps = { - [key: string]: CurationFormatAddApp; -} & { 'Extras'?: string; + 'Extras Name'?: string; 'Message'?: string; -}; - -type CurationFormatAddApp = { - 'Application Path'?: string; - 'Heading'?: string; - 'Launch Command'?: string; -}; + 'Parent Game ID'?: string; +}; \ No newline at end of file diff --git a/src/shared/curate/parse.ts b/src/shared/curate/parse.ts index bd067ca0a..b9a1574ba 100644 --- a/src/shared/curate/parse.ts +++ b/src/shared/curate/parse.ts @@ -3,7 +3,7 @@ import { Coerce } from '@shared/utils/Coerce'; import { IObjectParserProp, ObjectParser } from '../utils/ObjectParser'; import { CurationFormatObject, parseCurationFormat } from './format/parser'; import { CFTokenizer, tokenizeCurationFormat } from './format/tokenizer'; -import { EditAddAppCurationMeta, EditCurationMeta } from './types'; +import { EditCurationMeta } from './types'; import { getTagsFromStr } from './util'; const { str } = Coerce; @@ -12,8 +12,6 @@ const { str } = Coerce; export type ParsedCurationMeta = { /** Meta data of the game. */ game: EditCurationMeta; - /** Meta data of the additional applications. */ - addApps: EditAddAppCurationMeta[]; }; /** @@ -49,7 +47,6 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) // Default parsed data const parsed: ParsedCurationMeta = { game: {}, - addApps: [], }; // Make sure it exists before calling Object.keys if (!data) { @@ -93,6 +90,10 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) parser.prop('version', v => parsed.game.version = str(v)); parser.prop('library', v => parsed.game.library = str(v).toLowerCase()); // must be lower case parser.prop('mount parameters', v => parsed.game.mountParameters = str(v)); + parser.prop('extras', v => parsed.game.extras = str(v)); + parser.prop('extras name', v => parsed.game.extrasName = str(v)); + parser.prop('message', v => parsed.game.message = str(v)); + parser.prop('parent game id', v => parsed.game.parentGameId = str(v)); if (lowerCaseData.genre) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.genre), str(lowerCaseData['tag categories'])); } if (lowerCaseData.genres) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.genres), str(lowerCaseData['tag categories'])); } if (lowerCaseData.tags) { parsed.game.tags = await getTagsFromStr(arrayStr(lowerCaseData.tags), str(lowerCaseData['tag categories'])); } @@ -106,37 +107,10 @@ export async function parseCurationMetaFile(data: any, onError?: (error: string) } // property aliases parser.prop('animation notes', v => parsed.game.notes = str(v)); - // Add-apps - parser.prop('additional applications').map((item, label, map) => { - parsed.addApps.push(convertAddApp(item, label, map[label])); - }); // Return return parsed; } -/** - * Convert a "raw" curation additional application meta object into a more programmer friendly object. - * @param item Object parser, wrapped around the "raw" add-app meta object to convert. - * @param label Label of the object. - */ -function convertAddApp(item: IObjectParserProp, label: string | number | symbol, rawValue: any): EditAddAppCurationMeta { - const addApp: EditAddAppCurationMeta = {}; - const labelStr = str(label); - switch (labelStr.toLowerCase()) { - case 'extras': // (Extras add-app) - return generateExtrasAddApp(str(rawValue)); - case 'message': // (Message add-app) - return generateMessageAddApp(str(rawValue)); - default: // (Normal add-app) - addApp.heading = labelStr; - item.prop('Heading', v => addApp.heading = str(v), true); - item.prop('Application Path', v => addApp.applicationPath = str(v)); - item.prop('Launch Command', v => addApp.launchCommand = str(v)); - break; - } - return addApp; -} - // Coerce an object into a sensible string function arrayStr(rawStr: any): string { if (Array.isArray(rawStr)) { @@ -145,19 +119,3 @@ function arrayStr(rawStr: any): string { } return str(rawStr); } - -export function generateExtrasAddApp(folderName: string) : EditAddAppCurationMeta { - return { - heading: 'Extras', - applicationPath: ':extras:', - launchCommand: folderName - }; -} - -export function generateMessageAddApp(message: string) : EditAddAppCurationMeta { - return { - heading: 'Message', - applicationPath: ':message:', - launchCommand: message - }; -} diff --git a/src/shared/curate/types.ts b/src/shared/curate/types.ts index 10c7c1886..247f8bcfc 100644 --- a/src/shared/curate/types.ts +++ b/src/shared/curate/types.ts @@ -7,8 +7,6 @@ export type EditCuration = { key: string; /** Meta data of the curation. */ meta: EditCurationMeta; - /** Keys of additional applications that belong to this game. */ - addApps: EditAddAppCuration[]; /** Data of each file in the content folder (and sub-folders). */ content: IndexedContent[]; /** Screenshot. */ @@ -23,18 +21,11 @@ export type EditCuration = { deleted: boolean; } -/** Data of an additional application curation in the curation importer. */ -export type EditAddAppCuration = { - /** Unique key of the curation (UUIDv4). Generated when loaded. */ - key: string; - /** Meta data of the curation. */ - meta: EditAddAppCurationMeta; -} - /** Meta data of a curation. */ export type EditCurationMeta = Partial<{ // Game fields title: string; + parentGameId?: string; alternateTitles: string; series: string; developer: string; @@ -55,13 +46,9 @@ export type EditCurationMeta = Partial<{ originalDescription: string; language: string; mountParameters: string; -}> - -/** Meta data of an additional application curation. */ -export type EditAddAppCurationMeta = Partial<{ - heading: string; - applicationPath: string; - launchCommand: string; + extras?: string; + extrasName?: string; + message?: string; }> export type CurationIndex = { diff --git a/src/shared/game/interfaces.ts b/src/shared/game/interfaces.ts index 7f10ffa14..add83bd5e 100644 --- a/src/shared/game/interfaces.ts +++ b/src/shared/game/interfaces.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '../../database/entity/AdditionalApp'; import { Game } from '../../database/entity/Game'; import { Playlist } from '../../database/entity/Playlist'; import { OrderGamesOpts } from './GameFilter'; @@ -39,12 +38,6 @@ export type GameAddRequest = { game: Game; } -/** Client Request - Add an additional application */ -export type AppAddRequest = { - /** Add App to add */ - addApp: AdditionalApp; -} - /** Client Request - Information needed to make a search */ export type SearchRequest = { /** String to use as a search query */ diff --git a/src/shared/game/util.ts b/src/shared/game/util.ts index 3344e912e..2c1096c0e 100644 --- a/src/shared/game/util.ts +++ b/src/shared/game/util.ts @@ -1,4 +1,3 @@ -import { AdditionalApp } from '../../database/entity/AdditionalApp'; import { Game } from '../../database/entity/Game'; export namespace ModelUtils { @@ -30,22 +29,10 @@ export namespace ModelUtils { language: '', library: '', orderTitle: '', - addApps: [], + children: [], placeholder: false, activeDataOnDisk: false }); return game; } - - export function createAddApp(game: Game): AdditionalApp { - return { - id: '', - parentGame: game, - applicationPath: '', - autoRunBefore: false, - launchCommand: '', - name: '', - waitForExit: false, - }; - } } diff --git a/src/shared/lang.ts b/src/shared/lang.ts index dc0354671..ff515109c 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -314,6 +314,12 @@ const langTemplate = { 'mountParameters', 'noMountParameters', 'showExtremeScreenshot', + 'extras', + 'noExtras', + 'message', + 'noMessage', + 'extrasName', + 'noExtrasName', ] as const, tags: [ 'name', diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index ec8c2c11c..7d8230705 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -457,6 +457,8 @@ declare module 'flashpoint-launcher' { id: string; /** ID of the game which owns this game */ parentGameId?: string; + /** A list of child games. */ + children: Game[]; /** Full title of the game */ title: string; /** Any alternate titles to match against search */ @@ -503,8 +505,6 @@ declare module 'flashpoint-launcher' { language: string; /** Library this game belongs to */ library: string; - /** All attached Additional Apps of a game */ - addApps: AdditionalApp[]; /** Unused */ orderTitle: string; /** If the game is a placeholder (and can therefore not be saved) */ @@ -513,6 +513,13 @@ declare module 'flashpoint-launcher' { activeDataId?: number; /** Whether the data is present on disk */ activeDataOnDisk: boolean; + /** The path to any extras. */ + extras?: string; + /** The name to be displayed for those extras. */ + extrasName?: string; + /** The message to display when the game starts. */ + message?: string; + data?: GameData[]; updateTagsStr: () => void; }; From 96d7327a975ed2391057dd8ecd1aa9387976f0cb Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 20 Mar 2022 22:26:18 -0400 Subject: [PATCH 42/83] Fix silly database mistake. --- src/back/game/GameManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 771b893e4..d51fb1d61 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -124,13 +124,14 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // console.log('FindGamePageKeyset:'); const subQ = await getGameQuery('sub', filterOpts, orderBy, direction); - subQ.select(`sub.${orderBy}, sub.title, sub.id, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); + subQ.select(`sub.${orderBy}, sub.title, sub.id, sub.parentGameId, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); subQ.orderBy(`sub.${orderBy} ${direction}, sub.title`, direction); let query = getManager().createQueryBuilder() .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') + .andWhere('g.parentGameId is null') .setParameters(subQ.getParameters()); if (searchLimit) { From a7856f90e00929b19a05eac16157a6623c7c5819 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Fri, 25 Mar 2022 07:07:03 -0400 Subject: [PATCH 43/83] Searching and launching broken, but browsing works now. --- src/back/game/GameManager.ts | 55 +++++++++++++++++++++--------------- src/back/responses.ts | 3 ++ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index d51fb1d61..e6c10dd3c 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -17,7 +17,7 @@ import * as path from 'path'; import * as TagManager from './TagManager'; import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; -import { isNull } from 'util'; +import { isNull, isNullOrUndefined } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -53,13 +53,14 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom /** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } + log.debug('GameManager', 'findGameRow'); // const startTime = Date.now(); const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) - .where("game.parentGameId is null"); + .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) + .where("game.parentGameId IS NULL"); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); @@ -74,7 +75,9 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o .setParameters(subQ.getParameters()) .select('row_num') .from('(' + subQ.getQuery() + ')', 'g') - .where('g.id = :gameId', { gameId: gameId }); + .where('g.id = :gameId', { gameId: gameId }) + // Shouldn't be needed, but doing it anyway. + .andWhere('g.parentGameId IS NULL'); const raw = await query.getRawOne(); // console.log(`${Date.now() - startTime}ms for row`); @@ -92,7 +95,7 @@ export async function findRandomGames(count: number, broken: boolean, excludedLi const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); - query.where("game.parentGameId is null"); + query.where("game.parentGameId IS NULL"); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -165,6 +168,15 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga } // console.log(` Count: ${Date.now() - startTime}ms`); + let i = 0; + while (i < total) { + if (keyset[i]) { + // @ts-ignore + log.debug('GameManager', keyset[i].title); + i = total; + } + i++; + } return { keyset, @@ -191,7 +203,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const ranges = opts.ranges || [{ start: 0, length: undefined }]; const rangesOut: ResponseGameRange[] = []; - // console.log('FindGames:'); + console.log('FindGames:'); let query: SelectQueryBuilder | undefined; for (let i = 0; i < ranges.length; i++) { @@ -199,7 +211,6 @@ export async function findGames(opts: FindGamesOpts, shallow: const range = ranges[i]; query = await getGameQuery('game', opts.filter, opts.orderBy, opts.direction, range.start, range.length, range.index); - query.where("game.parentGameId is null"); // Select games // @TODO Make it infer the type of T from the value of "shallow", and then use that to make "games" get the correct type, somehow? @@ -207,6 +218,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const games = (shallow) ? (await query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.extreme, game.tagsStr').getRawMany()) as ViewGame[] : await query.getMany(); + rangesOut.push({ start: range.start, length: range.length, @@ -222,26 +234,13 @@ export async function findGames(opts: FindGamesOpts, shallow: return rangesOut; } -/** Find an add app with the specified ID. */ -export async function findAddApp(id?: string, filter?: FindOneOptions): Promise { - if (id || filter) { - if (!filter) { - filter = { - relations: ['parentGameId'] - }; - } - const addAppRepository = getManager().getRepository(Game); - return addAppRepository.findOne(id, filter); - } -} - export async function findPlatformAppPaths(platform: string): Promise { const gameRepository = getManager().getRepository(Game); const values = await gameRepository.createQueryBuilder('game') .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) - .andWhere("game.parentGameId is null") + .andWhere("game.parentGameId IS NULL") .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -274,7 +273,6 @@ export async function findPlatforms(library: string): Promise { const gameRepository = getManager().getRepository(Game); const libraries = await gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) - .andWhere("game.parentGameId is null") .select('game.platform') .distinct() .getRawMany(); @@ -450,7 +448,7 @@ async function chunkedFindByIds(gameIds: string[]): Promise { const chunks = chunkArray(gameIds, 100); let gamesFound: Game[] = []; for (const chunk of chunks) { - gamesFound = gamesFound.concat(await gameRepository.findByIds(chunk)); + gamesFound = gamesFound.concat(await gameRepository.findByIds(chunk, {parentGameId: IsNull()})); } return gamesFound; @@ -477,10 +475,12 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi } for (const phrase of searchQuery.genericWhitelist) { doWhereTitle(alias, query, phrase, whereCount, true); + log.debug("GameManager", `Whitelist title string: ${phrase}`); whereCount++; } for (const phrase of searchQuery.genericBlacklist) { doWhereTitle(alias, query, phrase, whereCount, false); + log.debug("GameManager", `Blacklist title string: ${phrase}`); whereCount++; } } @@ -586,6 +586,7 @@ async function getGameQuery( alias: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, offset?: number, limit?: number, index?: PageTuple ): Promise> { validateSqlName(alias); + log.debug('GameManager', 'getGameQuery'); if (orderBy) { validateSqlName(orderBy); } if (direction) { validateSqlOrder(direction); } @@ -600,6 +601,12 @@ async function getGameQuery( query.where(`(${alias}.${orderBy}, ${alias}.title, ${alias}.id) ${comparator} (:orderVal, :title, :id)`, { orderVal: index.orderVal, title: index.title, id: index.id }); whereCount++; } + if (whereCount === 0) { + query.where('parentGameId IS NULL'); + } else { + query.andWhere('parentGameId IS NULL'); + } + whereCount++; // Apply all flat game filters if (filterOpts) { whereCount = applyFlatGameFilters(alias, query, filterOpts, whereCount); @@ -615,8 +622,10 @@ async function getGameQuery( query.orderBy('pg.order', 'ASC'); if (whereCount === 0) { query.where('pg.playlistId = :playlistId', { playlistId: filterOpts.playlistId }); } else { query.andWhere('pg.playlistId = :playlistId', { playlistId: filterOpts.playlistId }); } + whereCount++; query.skip(offset); // TODO: Why doesn't offset work here? } + // Tag filtering if (filterOpts && filterOpts.searchQuery) { const aliasWhitelist = filterOpts.searchQuery.whitelist.filter(f => f.field === 'tag').map(f => f.value); diff --git a/src/back/responses.ts b/src/back/responses.ts index 06ea10b6b..4e953abc4 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -225,6 +225,9 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); + state.socketServer.registerAny((event, type, args) => { + log.debug('Responses', BackIn[type]); + }); // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id); From 0937ac809ffe689bbf8d9794c24c8d5ea9d1cebb Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 29 Mar 2022 19:11:53 -0400 Subject: [PATCH 44/83] Fix the extensions API. --- src/back/extensions/ApiImplementation.ts | 18 +-------------- src/back/types.ts | 4 ---- typings/flashpoint-launcher.d.ts | 29 ++---------------------- 3 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index aefd5af3c..de6036501 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -143,7 +143,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, updateGame: GameManager.save, updateGames: GameManager.updateGames, // Ardil TODO - removeGameAndAddApps: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), + removeGameAndChildren: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); return game.tagsStr.split(';').findIndex(t => extremeTags.includes(t.trim())) !== -1; @@ -157,31 +157,15 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, get onWillLaunchGame() { return apiEmitters.games.onWillLaunchGame.event; }, - // Ardil TODO remove - get onWillLaunchAddApp() { - return apiEmitters.games.onWillLaunchAddApp.event; - }, get onWillLaunchCurationGame() { return apiEmitters.games.onWillLaunchCurationGame.event; }, - // Ardil TODO remove - get onWillLaunchCurationAddApp() { - return apiEmitters.games.onWillLaunchCurationAddApp.event; - }, get onDidLaunchGame() { return apiEmitters.games.onDidLaunchGame.event; }, - // Ardil TODO remove - get onDidLaunchAddApp() { - return apiEmitters.games.onDidLaunchAddApp.event; - }, get onDidLaunchCurationGame() { return apiEmitters.games.onDidLaunchCurationGame.event; }, - // Ardil TODO remove - get onDidLaunchCurationAddApp() { - return apiEmitters.games.onDidLaunchCurationAddApp.event; - }, get onDidUpdateGame() { return apiEmitters.games.onDidUpdateGame.event; }, diff --git a/src/back/types.ts b/src/back/types.ts index 76c71494f..3705f5766 100644 --- a/src/back/types.ts +++ b/src/back/types.ts @@ -163,14 +163,10 @@ export type ApiEmittersState = Readonly<{ onLog: ApiEmitter; games: Readonly<{ onWillLaunchGame: ApiEmitter; - onWillLaunchAddApp: ApiEmitter; onWillLaunchCurationGame: ApiEmitter; - onWillLaunchCurationAddApp: ApiEmitter; onWillUninstallGameData: ApiEmitter; onDidLaunchGame: ApiEmitter; - onDidLaunchAddApp: ApiEmitter; onDidLaunchCurationGame: ApiEmitter; - onDidLaunchCurationAddApp: ApiEmitter; onDidUpdateGame: ApiEmitter<{oldGame: flashpoint.Game, newGame: flashpoint.Game}>; onDidRemoveGame: ApiEmitter; onDidUpdatePlaylist: ApiEmitter<{oldPlaylist: flashpoint.Playlist, newPlaylist: flashpoint.Playlist}>; diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 7d8230705..1220aed10 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -188,7 +188,7 @@ declare module 'flashpoint-launcher' { * Removes a Game and all its AddApps * @param gameId ID of Game to remove */ - function removeGameAndAddApps(gameId: string): Promise; + function removeGameAndChildren(gameId: string): Promise; // Misc /** @@ -210,14 +210,10 @@ declare module 'flashpoint-launcher' { // Events const onWillLaunchGame: Event; - const onWillLaunchAddApp: Event; const onWillLaunchCurationGame: Event; - const onWillLaunchCurationAddApp: Event; const onWillUninstallGameData: Event; const onDidLaunchGame: Event; - const onDidLaunchAddApp: Event; const onDidLaunchCurationGame: Event; - const onDidLaunchCurationAddApp: Event; const onDidInstallGameData: Event; const onDidUninstallGameData: Event; @@ -573,28 +569,7 @@ declare module 'flashpoint-launcher' { lastUpdated: Date; /** Any data provided by this Source */ data?: SourceData[]; - } - - type AdditionalApp = { - /** ID of the additional application (unique identifier) */ - id: string; - /** Path to the application that runs the additional application */ - applicationPath: string; - /** - * If the additional application should run before the game. - * (If true, this will always run when the game is launched) - * (If false, this will only run when specifically launched) - */ - autoRunBefore: boolean; - /** Command line argument(s) passed to the application to launch the game */ - launchCommand: string; - /** Name of the additional application */ - name: string; - /** Wait for this to exit before the Game will launch (if starting before launch) */ - waitForExit: boolean; - /** Parent of this add app */ - parentGame: Game; - }; + } type Tag = { /** ID of the tag (unique identifier) */ From 3f6edb9648972e2944d3656d90c72433720ef853 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:24:17 -0400 Subject: [PATCH 45/83] Update deps, add migration. --- package.json | 2 +- src/back/game/GameManager.ts | 16 +--------------- src/database/entity/Game.ts | 1 - .../migration/1648251821422-ChildCurations.ts | 19 +++++++++++++++++++ 4 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 src/database/migration/1648251821422-ChildCurations.ts diff --git a/package.json b/package.json index bcb4b809c..92bc9c25a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", - "sqlite3": "4.2.0", + "sqlite3": "^5.0.2", "tail": "2.0.3", "typeorm": "0.2.37", "typesafe-actions": "4.4.2", diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index e6c10dd3c..b4255bdd5 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -53,7 +53,6 @@ export async function findGame(id?: string, filter?: FindOneOptions): Prom /** Get the row number of an entry, specified by its gameId. */ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } - log.debug('GameManager', 'findGameRow'); // const startTime = Date.now(); const gameRepository = getManager().getRepository(Game); @@ -134,7 +133,6 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') - .andWhere('g.parentGameId is null') .setParameters(subQ.getParameters()); if (searchLimit) { @@ -168,15 +166,6 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga } // console.log(` Count: ${Date.now() - startTime}ms`); - let i = 0; - while (i < total) { - if (keyset[i]) { - // @ts-ignore - log.debug('GameManager', keyset[i].title); - i = total; - } - i++; - } return { keyset, @@ -203,7 +192,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const ranges = opts.ranges || [{ start: 0, length: undefined }]; const rangesOut: ResponseGameRange[] = []; - console.log('FindGames:'); + // console.log('FindGames:'); let query: SelectQueryBuilder | undefined; for (let i = 0; i < ranges.length; i++) { @@ -475,12 +464,10 @@ function applyFlatGameFilters(alias: string, query: SelectQueryBuilder, fi } for (const phrase of searchQuery.genericWhitelist) { doWhereTitle(alias, query, phrase, whereCount, true); - log.debug("GameManager", `Whitelist title string: ${phrase}`); whereCount++; } for (const phrase of searchQuery.genericBlacklist) { doWhereTitle(alias, query, phrase, whereCount, false); - log.debug("GameManager", `Blacklist title string: ${phrase}`); whereCount++; } } @@ -586,7 +573,6 @@ async function getGameQuery( alias: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, offset?: number, limit?: number, index?: PageTuple ): Promise> { validateSqlName(alias); - log.debug('GameManager', 'getGameQuery'); if (orderBy) { validateSqlName(orderBy); } if (direction) { validateSqlOrder(direction); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 987b04cc3..bb4be1dad 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -23,7 +23,6 @@ export class Game { parentGameId?: string; @OneToMany(type => Game, game => game.parentGame, { - cascade: true, eager: true }) children: Game[]; diff --git a/src/database/migration/1648251821422-ChildCurations.ts b/src/database/migration/1648251821422-ChildCurations.ts new file mode 100644 index 000000000..865d02d92 --- /dev/null +++ b/src/database/migration/1648251821422-ChildCurations.ts @@ -0,0 +1,19 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class ChildCurations1648251821422 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE game ADD extras varchar`, undefined); + await queryRunner.query(`ALTER TABLE game ADD extrasName varchar`, undefined); + await queryRunner.query(`ALTER TABLE game ADD message varchar`, undefined); + await queryRunner.query(`UPDATE game SET message = a.launchCommand FROM additional_app a WHERE game.id=a.parentGameId AND a.applicationPath=':message:'`); + await queryRunner.query(`UPDATE game SET extras = a.launchCommand, extrasName = a.name FROM additional_app a WHERE game.id = a.parentGameId AND a.applicationPath = ':extras:'`, undefined); + await queryRunner.query(`UPDATE game SET parentGameId = NULL WHERE id IS parentGameId`, undefined); + await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"0000-00-00 00:00:00.000" AS dateAdded,"0000-00-00 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); + await queryRunner.query(`DROP TABLE additional_app`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} From ea091b73faa8de638ef81103da776dc36a11eee8 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:25:24 -0400 Subject: [PATCH 46/83] Add migration at database init. --- src/back/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/back/index.ts b/src/back/index.ts index 21061e9f5..622f84ebc 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -16,6 +16,7 @@ import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-So import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; +import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; import { BackIn, BackInit, BackInitArgs, BackOut } from '@shared/back/types'; import { ILogoSet, LogoSet } from '@shared/extensions/interfaces'; import { IBackProcessInfo, RecursivePartial } from '@shared/interfaces'; @@ -312,7 +313,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, - SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109] + SourceFileCount1612436426353, GameTagsStr1613571078561, GameDataParams1619885915109, ChildCurations1648251821422] }; state.connection = await createConnection(options); // TypeORM forces on but breaks Playlist Game links to unimported games From 72dfdc77e3bf8d9cbaea7226f3361c975c8942c7 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:55:37 -0400 Subject: [PATCH 47/83] Fix child-loading, make children[] nullable, add option not to fetch children, fix a few typos, and a few small bugfixes. --- src/back/game/GameManager.ts | 25 +++++++++++++--- src/back/importGame.ts | 4 +-- src/back/responses.ts | 30 +++++++++++-------- src/back/util/misc.ts | 4 +-- src/database/entity/Game.ts | 11 ++++--- .../components/RightBrowseSidebar.tsx | 3 +- .../components/RightBrowseSidebarExtra.tsx | 2 +- typings/flashpoint-launcher.d.ts | 4 ++- 8 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index b4255bdd5..e096fe4c3 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -40,12 +40,27 @@ export async function countGames(): Promise { } /** Find the game with the specified ID. Ardil TODO find refs*/ -export async function findGame(id?: string, filter?: FindOneOptions): Promise { +export async function findGame(id?: string, filter?: FindOneOptions, noChildren?: boolean): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); const game = await gameRepository.findOne(id, filter); + // Only fetch the children if the game exists, the caller didn't ask us not to, and it's not a child itself. + // This enforces the no-multiple-generations rule. + if (game && !noChildren && !game.parentGameId) { + game.children = await gameRepository.createQueryBuilder() + .relation("children") + .of(game) + .loadMany(); + } if (game) { - game.tags.sort(tagSort); + if (game.tags) { + game.tags.sort(tagSort); + } + // Not sure why the standard "if (game.children)" wasn't working here, but it wasn't. + // Sort the child games. It's probably a good idea. + if (game.children !== undefined && game.children !== null && game.children.length > 1) { + game.children.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + } } return game; } @@ -302,8 +317,10 @@ export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: } // Delete children // Ardil TODO do Seirade's suggestion. - for (const child of game.children) { - await gameRepository.remove(child); + if (game.children) { + for (const child of game.children) { + await gameRepository.remove(child); + } } // Delete Game await gameRepository.remove(game); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 9068cb822..6a28f9e40 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -71,7 +71,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { where: { launchCommand: curation.meta.launchCommand } - }); + }, true); if (existingGame) { // Warn user of possible duplicate const response = await opts.openDialog({ @@ -94,7 +94,7 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); - const oldGame = await GameManager.findGame(gameId); + const oldGame = await GameManager.findGame(gameId, undefined, true); if (oldGame) { const response = await opts.openDialog({ title: 'Overwriting Game', diff --git a/src/back/responses.ts b/src/back/responses.ts index 4e953abc4..06edcd5ab 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -206,7 +206,7 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game && game.extras) { await GameLauncher.launchExtras({ extrasPath: game.extras, @@ -225,19 +225,18 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); - state.socketServer.registerAny((event, type, args) => { - log.debug('Responses', BackIn[type]); - }); // Ardil TODO state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { - // Ardil TODO not needed? Temp fix, see if it happens. if (game.parentGameId && !game.parentGame) { log.debug("Game Launcher", "Fetching parent game."); - game.parentGame = await GameManager.findGame(game.parentGameId) + // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. + game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } + // Ensure that the children is an array. Also enforce the no-multiple-generations rule. + //game.children = game.parentGameId ? [] : await game.children; // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -489,10 +488,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO + // Ardil TODO ensure that we really don't need children for this. state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { // Save to file try { @@ -524,7 +523,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { - return GameManager.findGame(id); + return await GameManager.findGame(id); }); // Ardil TODO @@ -660,7 +659,8 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { - return GameManager.findAllGames(); + let games: Game[] = await GameManager.findAllGames(); + return games; }); // Ardil TODO @@ -957,6 +957,10 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.register(BackIn.GET_PLAYLIST_GAME, async (event, playlistId, gameId) => { const playlistGame = await GameManager.findPlaylistGame(playlistId, gameId); + if (playlistGame && playlistGame.game) { + // Ensure that the children is an array. Also enforce the no-multiple-generations rule. + //playlistGame.game.children = playlistGame.game.parentGameId ? [] : await playlistGame.game.children; + } return playlistGame; }); @@ -983,7 +987,7 @@ export function registerRequestCallbacks(state: BackState): void { for (const game of platform.collection.games) { const addApps = platform.collection.additionalApplications.filter(a => a.gameId === game.id); const translatedGame = await createGameFromLegacy(game, tagCache); - translatedGame.children = createChildFromFromLegacyAddApp(addApps, translatedGame); + translatedGames.push(...createChildFromFromLegacyAddApp(addApps, translatedGame)); translatedGames.push(translatedGame); } await GameManager.updateGames(translatedGames); @@ -1201,7 +1205,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { - const game = await GameManager.findGame(id); + const game = await GameManager.findGame(id, undefined, true); if (game) { const meta: MetaEditMeta = { id: game.id, diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index 7f207e204..dd1b20421 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -181,6 +181,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli applicationPath: addApp.applicationPath, launchCommand: addApp.launchCommand, parentGame: game, + parentGameId: game.id, library: game.library, alternateTitles: "", series: "", @@ -205,8 +206,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli tags: [], extras: undefined, extrasName: undefined, - message: undefined, - children: [] + message: undefined }) retVal.push(newGame); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index bb4be1dad..0e863c126 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,4 +1,4 @@ -import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Tree, UpdateDateColumn } from 'typeorm'; import { GameData } from './GameData'; import { Tag } from './Tag'; @@ -16,16 +16,15 @@ export class Game { /** ID of the game (unique identifier) */ id: string; - @ManyToOne(type => Game, game => game.children) + @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; @Column({ nullable: true }) parentGameId?: string; - @OneToMany(type => Game, game => game.parentGame, { - eager: true - }) - children: Game[]; + // Careful: potential infinite loop here. DO NOT eager-load this. + @OneToMany((type) => Game, (game) => game.parentGame) + children?: Game[]; @Column({collation: 'NOCASE'}) @Index('IDX_gameTitle') diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index c035e15f7..2dcddf72b 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -953,8 +953,9 @@ export class RightBrowseSidebar extends React.Component addApp.id === childId); if (index === -1) { throw new Error('Cant remove additional application because it was not found.'); } newChildren.splice(index, 1); - this.props.onEditGame({children: newChildren}); this.props.onDeleteGame(childId); + // @TODO make this better. + this.props.onEditGame({children:newChildren}); } } diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index f19c733ff..6b7626797 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -9,7 +9,7 @@ import { OpenIcon } from './OpenIcon'; export type RightBrowseSidebarExtraProps = { /** Extras to show and edit */ - // These two are xplicitly non-nullable. + // These two are explicitly non-nullable. extrasPath: string; extrasName: string; game: Game; diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 1220aed10..a3fac5e01 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -451,10 +451,12 @@ declare module 'flashpoint-launcher' { type Game = { /** ID of the game (unique identifier) */ id: string; + /** This game's parent game. */ + parentGame?: Game; /** ID of the game which owns this game */ parentGameId?: string; /** A list of child games. */ - children: Game[]; + children?: Game[]; /** Full title of the game */ title: string; /** Any alternate titles to match against search */ From 63ffabad659bb524f04bdaf767bd5512539d6519 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:57:28 -0400 Subject: [PATCH 48/83] perf: don't fetch the game twice. --- src/back/MetaEdit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/back/MetaEdit.ts b/src/back/MetaEdit.ts index f07e4440f..5060822af 100644 --- a/src/back/MetaEdit.ts +++ b/src/back/MetaEdit.ts @@ -172,7 +172,7 @@ export async function importAllMetaEdits(fullMetaEditsFolderPath: string, openDi const game = await GameManager.findGame(id); if (game) { - games[id] = await GameManager.findGame(id); + games[id] = game; } else { // Game not found const combined = combinedMetas[id]; if (!combined) { throw new Error(`Failed to check for collisions. "combined meta" is missing (id: "${id}") (bug)`); } From a26b06f860fee1a344c096e48795ffd1b4ac4651 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 19:59:16 -0400 Subject: [PATCH 49/83] build: switch to better-sqlite3 I was getting meh performance with sqlite3, and I heard better-sqlite3 was faster. It seems to be so. --- ormconfig.json | 4 ++-- package.json | 2 +- src/back/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ormconfig.json b/ormconfig.json index 45a4124b7..beef81b3d 100644 --- a/ormconfig.json +++ b/ormconfig.json @@ -1,5 +1,5 @@ { - "type": "sqlite", + "type": "better-sqlite3", "host": "localhost", "port": 3306, "username": "flashpoint", @@ -22,4 +22,4 @@ "migrationsDir": "src/database/migration", "subscribersDir": "src/database/subscriber" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 92bc9c25a..58ef7342d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "axios": "0.21.4", + "better-sqlite3": "^7.5.0", "connected-react-router": "6.8.0", "electron-updater": "4.3.1", "electron-util": "0.14.2", @@ -57,7 +58,6 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", - "sqlite3": "^5.0.2", "tail": "2.0.3", "typeorm": "0.2.37", "typesafe-actions": "4.4.2", diff --git a/src/back/index.ts b/src/back/index.ts index 622f84ebc..02c007fbc 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -35,7 +35,7 @@ import * as mime from 'mime'; import * as path from 'path'; import 'reflect-metadata'; // Required for the DB Models to function -import 'sqlite3'; +import 'better-sqlite3'; import { Tail } from 'tail'; import { ConnectionOptions, createConnection } from 'typeorm'; import { ConfigFile } from './ConfigFile'; @@ -309,7 +309,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { // Setup DB if (!state.connection) { const options: ConnectionOptions = { - type: 'sqlite', + type: 'better-sqlite3', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, From a831a3dbc66ed9c853a20cdb745ba115b87980a4 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 31 Mar 2022 20:16:59 -0400 Subject: [PATCH 50/83] Fix comment for removeGameAndChildren() --- typings/flashpoint-launcher.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index a3fac5e01..80e9ccd55 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -185,7 +185,7 @@ declare module 'flashpoint-launcher' { */ function updateGames(games: Game[]): Promise; /** - * Removes a Game and all its AddApps + * Removes a Game and all its children * @param gameId ID of Game to remove */ function removeGameAndChildren(gameId: string): Promise; From b9ba19343e885506f4a82ddc0a23d47eeee920a8 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Fri, 1 Apr 2022 12:26:39 -0400 Subject: [PATCH 51/83] Fix extras. --- src/back/GameLauncher.ts | 2 +- src/back/responses.ts | 1 - .../components/RightBrowseSidebar.tsx | 26 +++++++++++++++++-- .../components/RightBrowseSidebarExtra.tsx | 19 +------------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index aa4ba4f99..6a8137a5a 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -59,7 +59,7 @@ export namespace GameLauncher { export async function launchExtras(opts: LaunchExtrasOpts): Promise { const folderPath = fixSlashes(path.join(opts.fpPath, path.posix.join('Extras', opts.extrasPath))); - return opts.openExternal(folderPath, { activate: true }) + return opts.openExternal(`file://${folderPath}`, { activate: true }) .catch(error => { if (error) { opts.openDialog({ diff --git a/src/back/responses.ts b/src/back/responses.ts index 06edcd5ab..5e76e6a8f 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -521,7 +521,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAME, async (event, id) => { return await GameManager.findGame(id); }); diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 2dcddf72b..80bc7b516 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -113,6 +113,8 @@ export class RightBrowseSidebar extends React.Component this.props.onEditGame({ notes: text })); onOriginalDescriptionChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ originalDescription: text })); onMessageChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ message: text })); + onExtrasChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ extras: text})); + onExtrasNameChange = this.wrapOnTextChange((game, text) => this.props.onEditGame({ extrasName: text})); onBrokenChange = this.wrapOnCheckBoxChange(game => { if (this.props.currentGame) { this.props.onEditGame({ broken: !this.props.currentGame.broken }); @@ -516,8 +518,28 @@ export class RightBrowseSidebar extends React.Component -
+
: undefined} +
+

{strings.extrasName}:

+ +
+
+

{strings.extras}:

+ +

{strings.dateAdded}:

) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) ? ( + { editable || (currentChildren && currentChildren.length > 0) || game.extras ? (

{strings.additionalApplications}:

diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index 6b7626797..9c0527509 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -29,9 +29,6 @@ export interface RightBrowseSidebarExtra { /** Displays an additional application for a game in the right sidebar of BrowsePage. */ export class RightBrowseSidebarExtra extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); - onExtrasNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); - onExtrasPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); render() { const allStrings = this.context; @@ -44,27 +41,13 @@ export class RightBrowseSidebarExtra extends React.Component + editable={false} />
- { editDisabled ? undefined : ( - <> - {/* Launch Command */} -
-

{strings.extras}:

- -
- - ) }
); } From 2d8ffb7ef45148813e165d6766ad21079ea1688d Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 10:25:48 -0400 Subject: [PATCH 52/83] Make the linter happy, also fix curation. That is: I hope so. I haven't tested curation yet, but I *think* this will fix some portions of it. --- src/back/GameLauncher.ts | 40 ++---------- src/back/game/GameManager.ts | 42 +++++++++--- src/back/importGame.ts | 34 ++++++++-- src/back/responses.ts | 26 +++----- src/back/util/misc.ts | 39 ++++++----- src/database/entity/Game.ts | 2 +- src/renderer/components/CurateBox.tsx | 50 ++++++++++++--- src/renderer/components/CurateBoxWarnings.tsx | 2 + .../components/RightBrowseSidebar.tsx | 43 ++++++------- .../components/RightBrowseSidebarAddApp.tsx | 2 - .../components/RightBrowseSidebarExtra.tsx | 64 +------------------ src/renderer/components/pages/BrowsePage.tsx | 2 +- .../components/pages/DeveloperPage.tsx | 2 +- src/renderer/context/CurationContext.ts | 1 - src/shared/back/types.ts | 2 +- src/shared/curate/metaToMeta.ts | 2 +- src/shared/curate/parse.ts | 2 +- src/shared/lang.ts | 3 + 18 files changed, 168 insertions(+), 190 deletions(-) diff --git a/src/back/GameLauncher.ts b/src/back/GameLauncher.ts index 6a8137a5a..32e84ade8 100644 --- a/src/back/GameLauncher.ts +++ b/src/back/GameLauncher.ts @@ -1,11 +1,9 @@ import { Game } from '@database/entity/Game'; import { AppProvider } from '@shared/extensions/interfaces'; -import { ExecMapping, Omit } from '@shared/interfaces'; +import { ExecMapping } from '@shared/interfaces'; import { LangContainer } from '@shared/lang'; -import { fixSlashes, padStart, stringifyArray } from '@shared/Util'; +import { fixSlashes } from '@shared/Util'; import { Coerce } from '@shared/utils/Coerce'; -import { ChildProcess, exec } from 'child_process'; -import { EventEmitter } from 'events'; import { AppPathOverride, GameData, ManagedChildProcess } from 'flashpoint-launcher'; import * as path from 'path'; import { ApiEmitter } from './extensions/ApiEmitter'; @@ -81,9 +79,9 @@ export namespace GameLauncher { // Abort if placeholder (placeholders are not "actual" games) if (opts.game.placeholder) { return; } if (opts.game.message) { - await opts.openDialog({type: 'info', - title: 'About This Game', - message: opts.game.message, + await opts.openDialog({type: 'info', + title: 'About This Game', + message: opts.game.message, buttons: ['Ok'], }); } @@ -303,34 +301,6 @@ export namespace GameLauncher { throw Error('Unsupported platform'); } } - - function logProcessOutput(proc: ChildProcess): void { - // Log for debugging purposes - // (might be a bad idea to fill the console with junk?) - const logInfo = (event: string, args: any[]): void => { - log.info(logSource, `${event} (PID: ${padStart(proc.pid, 5)}) ${stringifyArray(args, stringifyArrayOpts)}`); - }; - const logErr = (event: string, args: any[]): void => { - log.error(logSource, `${event} (PID: ${padStart(proc.pid, 5)}) ${stringifyArray(args, stringifyArrayOpts)}`); - }; - registerEventListeners(proc, [/* 'close', */ 'disconnect', 'exit', 'message'], logInfo); - registerEventListeners(proc, ['error'], logErr); - if (proc.stdout) { proc.stdout.on('data', (data) => { logInfo('stdout', [data.toString('utf8')]); }); } - if (proc.stderr) { proc.stderr.on('data', (data) => { logErr('stderr', [data.toString('utf8')]); }); } - } -} - -const stringifyArrayOpts = { - trimStrings: true, -}; - -function registerEventListeners(emitter: EventEmitter, events: string[], callback: (event: string, args: any[]) => void): void { - for (let i = 0; i < events.length; i++) { - const e: string = events[i]; - emitter.on(e, (...args: any[]) => { - callback(e, args); - }); - } } /** diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index e096fe4c3..39475b659 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -17,7 +17,6 @@ import * as path from 'path'; import * as TagManager from './TagManager'; import { Brackets, FindOneOptions, getManager, SelectQueryBuilder, IsNull } from 'typeorm'; import * as GameDataManager from './GameDataManager'; -import { isNull, isNullOrUndefined } from 'util'; const exactFields = [ 'broken', 'library', 'activeDataOnDisk' ]; enum flatGameFields { @@ -48,7 +47,7 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi // This enforces the no-multiple-generations rule. if (game && !noChildren && !game.parentGameId) { game.children = await gameRepository.createQueryBuilder() - .relation("children") + .relation('children') .of(game) .loadMany(); } @@ -74,7 +73,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const subQ = gameRepository.createQueryBuilder('game') .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) - .where("game.parentGameId IS NULL"); + .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); @@ -109,7 +108,7 @@ export async function findRandomGames(count: number, broken: boolean, excludedLi const gameRepository = getManager().getRepository(Game); const query = gameRepository.createQueryBuilder('game'); query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.tagsStr'); - query.where("game.parentGameId IS NULL"); + query.where('game.parentGameId IS NULL'); if (!broken) { query.andWhere('broken = false'); } if (excludedLibraries.length > 0) { query.andWhere('library NOT IN (:...libs)', { libs: excludedLibraries }); @@ -222,7 +221,7 @@ export async function findGames(opts: FindGamesOpts, shallow: const games = (shallow) ? (await query.select('game.id, game.title, game.platform, game.developer, game.publisher, game.extreme, game.tagsStr').getRawMany()) as ViewGame[] : await query.getMany(); - + rangesOut.push({ start: range.start, length: range.length, @@ -244,7 +243,7 @@ export async function findPlatformAppPaths(platform: string): Promise .select('game.applicationPath') .distinct() .where('game.platform = :platform', {platform: platform}) - .andWhere("game.parentGameId IS NULL") + .andWhere('game.parentGameId IS NULL') .groupBy('game.applicationPath') .orderBy('COUNT(*)', 'DESC') .getRawMany(); @@ -288,6 +287,19 @@ export async function updateGames(games: Game[]): Promise { for (const chunk of chunks) { await getManager().transaction(async transEntityManager => { for (const game of chunk) { + // Set nullable properties to null if they're empty. + if (game.parentGameId === '') { + game.parentGameId = undefined; + } + if (game.extras === '') { + game.extras = undefined; + } + if (game.extrasName === '') { + game.extrasName = undefined; + } + if (game.message === '') { + game.message = undefined; + } await transEntityManager.save(Game, game); } }); @@ -296,6 +308,19 @@ export async function updateGames(games: Game[]): Promise { export async function save(game: Game): Promise { const gameRepository = getManager().getRepository(Game); + // Set nullable properties to null if they're empty. + if (game.parentGameId === '') { + game.parentGameId = undefined; + } + if (game.extras === '') { + game.extras = undefined; + } + if (game.extrasName === '') { + game.extrasName = undefined; + } + if (game.message === '') { + game.message = undefined; + } log.debug('Launcher', 'Saving game...'); const savedGame = await gameRepository.save(game); if (savedGame) { onDidUpdateGame.fire({oldGame: game, newGame: savedGame}); } @@ -305,7 +330,6 @@ export async function save(game: Game): Promise { // Ardil TODO fix this. export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); - //const addAppRepository = getManager().getRepository(AdditionalApp); const game = await findGame(gameId); if (game) { // Delete GameData @@ -529,7 +553,7 @@ function doWhereTitle(alias: string, query: SelectQueryBuilder, value: str * @param alias The name of the table. * @param query The query to add to. * @param field The field (column) to search on. - * @param value The value to search for. If it's a string, it will be interpreted as position-independent + * @param value The value to search for. If it's a string, it will be interpreted as position-independent * if the field is not on the exactFields list. * @param count How many conditions we've already filtered. Determines whether we use .where() or .andWhere(). * @param whitelist Whether this is a whitelist or a blacklist search. @@ -628,7 +652,7 @@ async function getGameQuery( whereCount++; query.skip(offset); // TODO: Why doesn't offset work here? } - + // Tag filtering if (filterOpts && filterOpts.searchQuery) { const aliasWhitelist = filterOpts.searchQuery.whitelist.filter(f => f.field === 'tag').map(f => f.value); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 6a28f9e40..38ed863de 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -86,6 +86,26 @@ export async function importCuration(opts: ImportCurationOpts): Promise { } } } + // @TODO ditto as above. + if (curation.meta.parentGameId && curation.meta.parentGameId != '') { + const existingGame = await GameManager.findGame(curation.meta.parentGameId, undefined, true); + if (existingGame == undefined) { + // Warn user of invalid parent + const response = await opts.openDialog({ + title: 'Invalid Parent', + message: 'This curation has an invalid parent game id.\nContinue importing this curation? Warning: this will make the game be parentless!\n\n' + + `Curation:\n\tTitle: ${curation.meta.title}\n\tLaunch Command: ${curation.meta.launchCommand}\n\tPlatform: ${curation.meta.platform}\n\n`, + buttons: ['Yes', 'No'] + }); + if (response === 1) { + throw new Error('User Cancelled Import'); + } else { + curation.meta.parentGameId = undefined; + } + } + } else if (curation.meta.parentGameId == '') { + curation.meta.parentGameId = undefined; + } // Build content list const contentToMove = []; if (curation.meta.extras && curation.meta.extras.length > 0) { @@ -259,17 +279,17 @@ export async function launchCuration(key: string, meta: EditCurationMeta, symlin onDidEvent.fire(game); } -// Ardil TODO this won't work, fix it. +// Ardil TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit) { - if (meta.extras) { - if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } + if (meta.extras) { + if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } await GameLauncher.launchExtras({ ...opts, extrasPath: meta.extras }); - } } +} function logMessage(text: string, curation: EditCuration): void { console.log(`- ${text}\n (id: ${curation.key})`); @@ -285,7 +305,7 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) - parentGameId: gameMeta.parentGameId, + parentGameId: gameMeta.parentGameId === '' ? undefined : gameMeta.parentGameId, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -475,7 +495,7 @@ function curationLog(content: string): void { // return buffer.equals(secondBuffer); // } -function createPlaceholderGame(): Game { +/* function createPlaceholderGame(): Game { const id = uuid(); const game = new Game(); Object.assign(game, { @@ -509,7 +529,7 @@ function createPlaceholderGame(): Game { activeDataOnDisk: false }); return game; -} +}*/ export async function createTagsFromLegacy(tags: string, tagCache: Record): Promise { const allTags: Tag[] = []; diff --git a/src/back/responses.ts b/src/back/responses.ts index 5e76e6a8f..17afc0c98 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -205,11 +205,10 @@ export function registerRequestCallbacks(state: BackState): void { return state.execMappings; }); - state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, id) => { - const game = await GameManager.findGame(id, undefined, true); - if (game && game.extras) { + state.socketServer.register(BackIn.LAUNCH_EXTRAS, async (event, extrasPath) => { + if (extrasPath) { await GameLauncher.launchExtras({ - extrasPath: game.extras, + extrasPath: extrasPath, fpPath: path.resolve(state.config.flashpointPath), htdocsPath: state.preferences.htdocsFolderPath, execMappings: state.execMappings, @@ -231,12 +230,10 @@ export function registerRequestCallbacks(state: BackState): void { if (game) { if (game.parentGameId && !game.parentGame) { - log.debug("Game Launcher", "Fetching parent game."); + log.debug('Game Launcher', 'Fetching parent game.'); // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } - // Ensure that the children is an array. Also enforce the no-multiple-generations rule. - //game.children = game.parentGameId ? [] : await game.children; // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { @@ -362,13 +359,13 @@ export function registerRequestCallbacks(state: BackState): void { }); // Ardil TODO check that this was the right move. - /*state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + /* state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { // Copy and apply new IDs - + const newGame = deepCopy(game); /* Ardil TODO figure this out. const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); @@ -658,7 +655,7 @@ export function registerRequestCallbacks(state: BackState): void { // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { - let games: Game[] = await GameManager.findAllGames(); + const games: Game[] = await GameManager.findAllGames(); return games; }); @@ -955,12 +952,7 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.GET_PLAYLIST_GAME, async (event, playlistId, gameId) => { - const playlistGame = await GameManager.findPlaylistGame(playlistId, gameId); - if (playlistGame && playlistGame.game) { - // Ensure that the children is an array. Also enforce the no-multiple-generations rule. - //playlistGame.game.children = playlistGame.game.parentGameId ? [] : await playlistGame.game.children; - } - return playlistGame; + return await GameManager.findPlaylistGame(playlistId, gameId); }); state.socketServer.register(BackIn.ADD_PLAYLIST_GAME, async (event, playlistId, gameId) => { @@ -1119,7 +1111,7 @@ export function registerRequestCallbacks(state: BackState): void { exePath: state.exePath, appPathOverrides: state.preferences.appPathOverrides, providers: await getProviders(state), - proxy: state.preferences.browserModeProxy, + proxy: state.preferences.browserModeProxy, openDialog: state.socketServer.showMessageBoxBack(event.client), openExternal: state.socketServer.openExternal(event.client), runGame: runGameFactory(state) diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index dd1b20421..69a572ca2 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -14,7 +14,6 @@ import { Legacy_IAdditionalApplicationInfo, Legacy_IGameInfo } from '@shared/leg import { deepCopy, recursiveReplace, stringifyArray } from '@shared/Util'; import * as child_process from 'child_process'; import * as fs from 'fs'; -import { add } from 'node-7z'; import * as path from 'path'; import { promisify } from 'util'; import { uuid } from './uuid'; @@ -166,7 +165,7 @@ export async function execProcess(state: BackState, proc: IBackProcessInfo, sync } export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalApplicationInfo[], game: Game): Game[] { - let retVal: Game[] = []; + const retVal: Game[] = []; for (const addApp of addApps) { if (addApp.applicationPath === ':message:') { game.message = addApp.launchCommand; @@ -174,7 +173,7 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli game.extras = addApp.launchCommand; game.extrasName = addApp.name; } else { - let newGame = new Game(); + const newGame = new Game(); Object.assign(newGame, { id: addApp.id, title: addApp.name, @@ -183,23 +182,23 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli parentGame: game, parentGameId: game.id, library: game.library, - alternateTitles: "", - series: "", - developer: "", - publisher: "", - dateAdded: "0000-00-00 00:00:00.000", - dateModified: "0000-00-00 00:00:00.000", - platform: "", + alternateTitles: '', + series: '', + developer: '', + publisher: '', + dateAdded: '0000-00-00 00:00:00.000', + dateModified: '0000-00-00 00:00:00.000', + platform: '', broken: false, extreme: game.extreme, - playMode: "", - status: "", - notes: "", - source: "", - releaseDate: "", - version: "", - originalDescription: "", - language: "", + playMode: '', + status: '', + notes: '', + source: '', + releaseDate: '', + version: '', + originalDescription: '', + language: '', orderTitle: addApp.name.toLowerCase(), activeDataId: undefined, activeDataOnDisk: false, @@ -207,11 +206,11 @@ export function createChildFromFromLegacyAddApp(addApps: Legacy_IAdditionalAppli extras: undefined, extrasName: undefined, message: undefined - }) + }); retVal.push(newGame); } } - /*return addApps.map(a => { + /* return addApps.map(a => { return { id: a.id, name: a.name, diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 0e863c126..aaba46526 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -1,4 +1,4 @@ -import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Tree, UpdateDateColumn } from 'typeorm'; +import { BeforeUpdate, Column, Entity, Index, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { GameData } from './GameData'; import { Tag } from './Tag'; diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 04dde8c4f..e58b29225 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -480,6 +480,8 @@ export function CurateBox(props: CurateBoxProps) { const disabled = props.curation ? props.curation.locked : false; // Whether the platform used by the curation is native locked + // Ardil TODO what is this used for? + // eslint-disable-next-line @typescript-eslint/no-unused-vars const native = useMemo(() => { if (props.curation && props.curation.meta.platform) { isPlatformNativeLocked(props.curation.meta.platform); @@ -622,6 +624,14 @@ export function CurateBox(props: CurateBoxProps) { onChange={onTitleChange} { ...sharedInputProps } /> + + + v function useOnInputChange(property: keyof EditCurationMeta, key: string | undefined, dispatch: React.Dispatch) { return useCallback((event: InputElementOnChangeEvent) => { if (key !== undefined) { - dispatch({ - type: 'edit-curation-meta', - payload: { - key: key, - property: property, - value: event.currentTarget.value - } - }); + // If it's one of the nullable types, treat '' as undefined. + if (property == 'parentGameId' || property == 'extras' || property == 'extrasName' || property == 'message') { + dispatch({ + type: 'edit-curation-meta', + payload: { + key: key, + property: property, + value: event.currentTarget.value == '' ? undefined : event.currentTarget.value + } + }); + } else { + dispatch({ + type: 'edit-curation-meta', + payload: { + key: key, + property: property, + value: event.currentTarget.value + } + }); + } } }, [dispatch, key]); } @@ -1135,6 +1157,18 @@ export function getCurationWarnings(curation: EditCuration, suggestions: Partial if (!warns.noLaunchCommand) { warns.invalidLaunchCommand = invalidLaunchCommandWarnings(getContentFolderByKey2(curation.key), launchCommand, strings); } + // @TODO check that the parentGameId is valid in some synchronous manner. + /* const parentId = curation.meta.parentGameId || ''; + log.debug("getCurationWarnings", "parentId: "+parentId); + if (parentId !== '') { + warns.invalidParentGameId = true; + window.Shared.back.request(BackIn.GET_GAME, parentId).then((result) => { + warns.invalidParentGameId = result == undefined; + }); + } else { + // If the parentGameId is undefined/empty, it's just a non-child game. That's fine. + warns.invalidParentGameId = false; + }*/ warns.noLogo = !curation.thumbnail.exists; warns.noScreenshot = !curation.screenshot.exists; warns.noTags = (!curation.meta.tags || curation.meta.tags.length === 0); diff --git a/src/renderer/components/CurateBoxWarnings.tsx b/src/renderer/components/CurateBoxWarnings.tsx index ec99a68c4..ccc0775f7 100644 --- a/src/renderer/components/CurateBoxWarnings.tsx +++ b/src/renderer/components/CurateBoxWarnings.tsx @@ -13,6 +13,8 @@ export type CurationWarnings = { noLaunchCommand?: boolean; /** If the launch command is not a url with the "http" protocol and doesn't point to a file in 'content' */ invalidLaunchCommand?: string[]; + /** If the parentGameId is not valid. */ + invalidParentGameId?: boolean; /** If the release date is invalid (incorrectly formatted). */ releaseDateInvalid?: boolean; /** If the application path value isn't used by any other game. */ diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 80bc7b516..ad5ff64eb 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -7,7 +7,6 @@ import { WithConfirmDialogProps } from '@renderer/containers/withConfirmDialog'; import { BackIn, BackOut, BackOutTemplate, TagSuggestion } from '@shared/back/types'; import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { wrapSearchTerm } from '@shared/game/GameFilter'; -import { ModelUtils } from '@shared/game/util'; import { GamePropSuggestions, PickType, ProcessAction } from '@shared/interfaces'; import { LangContainer } from '@shared/lang'; import { deepCopy, generateTagFilterGroup, sizeToString } from '@shared/Util'; @@ -19,7 +18,6 @@ import { WithPreferencesProps } from '../containers/withPreferences'; import { WithSearchProps } from '../containers/withSearch'; import { getGameImagePath, getGameImageURL } from '../Util'; import { LangContext } from '../util/lang'; -import { uuid } from '../util/uuid'; import { CheckBox } from './CheckBox'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { DropdownInputField } from './DropdownInputField'; @@ -510,20 +508,20 @@ export class RightBrowseSidebar extends React.Component {game.message ?
-

{strings.message}:

- -
- : undefined} +

{strings.message}:

+ +
+ : undefined}

{strings.extrasName}:

{strings.extras}:

- { game.broken || editable ? (
)) } - {game.extras && game.extrasName ? - : undefined - } + onLaunch={this.onExtrasLaunch} /> + : undefined}
) : undefined } {/* -- Application Path & Launch Command -- */} @@ -963,9 +958,9 @@ export class RightBrowseSidebar extends React.Component { diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 8b714a0ab..9062d0b06 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -2,7 +2,6 @@ import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; -import { CheckBox } from './CheckBox'; import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; import { OpenIcon } from './OpenIcon'; @@ -69,7 +68,6 @@ export class RightBrowseSidebarChild extends React.Component
- {/* Wait for Exit */}
{/* Delete Button */} diff --git a/src/renderer/components/RightBrowseSidebarExtra.tsx b/src/renderer/components/RightBrowseSidebarExtra.tsx index 9c0527509..bd03aec6a 100644 --- a/src/renderer/components/RightBrowseSidebarExtra.tsx +++ b/src/renderer/components/RightBrowseSidebarExtra.tsx @@ -1,26 +1,15 @@ -import { Game } from '@database/entity/Game'; import { LangContainer } from '@shared/lang'; import * as React from 'react'; import { LangContext } from '../util/lang'; -import { CheckBox } from './CheckBox'; -import { ConfirmElement, ConfirmElementArgs } from './ConfirmElement'; import { InputField } from './InputField'; -import { OpenIcon } from './OpenIcon'; export type RightBrowseSidebarExtraProps = { /** Extras to show and edit */ // These two are explicitly non-nullable. extrasPath: string; extrasName: string; - game: Game; - /** Called when a field is edited */ - onEdit?: () => void; - /** Called when a field is edited */ - onDelete?: (gameId: string) => void; /** Called when the launch button is clicked */ - onLaunch?: (gameId: string) => void; - /** If the editing is disabled (it cant go into "edit mode") */ - editDisabled?: boolean; + onLaunch?: (extrasPath: string) => void; }; export interface RightBrowseSidebarExtra { @@ -33,13 +22,12 @@ export class RightBrowseSidebarExtra extends React.Component {/* Title & Launch Button */}
): JSX.Element { - const className = 'browse-right-sidebar__additional-application__delete-button'; - return ( -
- -
- ); - } - onLaunchClick = (): void => { if (this.props.onLaunch) { - this.props.onLaunch(this.props.game.id); - } - } - - onDeleteClick = (): void => { - if (this.props.onDelete) { - this.props.onDelete(this.props.game.id); + this.props.onLaunch(this.props.extrasPath); } } - onEdit(): void { - if (this.props.onEdit) { - this.props.onEdit(); - } - } - - /** Create a wrapper for a EditableTextWrap's onEditDone callback (this is to reduce redundancy). */ - wrapOnTextChange(func: (addApp: Game, text: string) => void): (event: React.ChangeEvent) => void { - return (event) => { - const addApp = this.props.game; - if (addApp) { - func(addApp, event.currentTarget.value); - this.forceUpdate(); - } - }; - } - - /** Create a wrapper for a CheckBox's onChange callback (this is to reduce redundancy). */ - wrapOnCheckBoxChange(func: (addApp: Game) => void) { - return () => { - if (!this.props.editDisabled) { - func(this.props.game); - this.onEdit(); - this.forceUpdate(); - } - }; - } - static contextType = LangContext; } diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 84e5f19df..5b6a28884 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -721,7 +721,7 @@ export class BrowsePage extends React.Component { - this.setState({ text: text + filePath + '\n' + createTextBarProgress(current, files.length) }) + this.setState({ text: text + filePath + '\n' + createTextBarProgress(current, files.length) }); }) .catch((error) => { text = text + `Failure - ${fileName} - ERROR: ${error}\n`; diff --git a/src/renderer/context/CurationContext.ts b/src/renderer/context/CurationContext.ts index ccce946a4..52947d36a 100644 --- a/src/renderer/context/CurationContext.ts +++ b/src/renderer/context/CurationContext.ts @@ -4,7 +4,6 @@ import { CurationIndexImage, EditCuration, EditCurationMeta, IndexedContent } fr import { createContextReducer } from '../context-reducer/contextReducer'; import { ReducerAction } from '../context-reducer/interfaces'; import { createCurationIndexImage } from '../curate/importCuration'; -import { uuid } from '../util/uuid'; const curationDefaultState: CurationsState = { defaultMetaData: undefined, diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 946350b79..be492e070 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -199,7 +199,7 @@ export type BackInTemplate = SocketTemplate BrowseChangeData; [BackIn.DUPLICATE_GAME]: (id: string, dupeImages: boolean) => BrowseChangeData; [BackIn.EXPORT_GAME]: (id: string, location: string, metaOnly: boolean) => void; - [BackIn.LAUNCH_EXTRAS]: (id: string) => void; + [BackIn.LAUNCH_EXTRAS]: (extrasPath: string) => void; [BackIn.SAVE_IMAGE]: (folder: string, id: string, content: string) => void; [BackIn.DELETE_IMAGE]: (folder: string, id: string) => void; [BackIn.ADD_LOG]: (data: ILogPreEntry & { logLevel: LogLevel }) => void; diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index f0dac9d71..1ac00414c 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -153,4 +153,4 @@ type CurationMetaFile = { 'Extras Name'?: string; 'Message'?: string; 'Parent Game ID'?: string; -}; \ No newline at end of file +}; diff --git a/src/shared/curate/parse.ts b/src/shared/curate/parse.ts index b9a1574ba..3b141288c 100644 --- a/src/shared/curate/parse.ts +++ b/src/shared/curate/parse.ts @@ -1,6 +1,6 @@ import { BackIn } from '@shared/back/types'; import { Coerce } from '@shared/utils/Coerce'; -import { IObjectParserProp, ObjectParser } from '../utils/ObjectParser'; +import { ObjectParser } from '../utils/ObjectParser'; import { CurationFormatObject, parseCurationFormat } from './format/parser'; import { CFTokenizer, tokenizeCurationFormat } from './format/tokenizer'; import { EditCurationMeta } from './types'; diff --git a/src/shared/lang.ts b/src/shared/lang.ts index ff515109c..b25992fb2 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -410,6 +410,9 @@ const langTemplate = { 'ilc_notHttp', 'ilc_nonExistant', 'sort', + 'parentGameId', + 'noParentGameId', + 'invalidParentGameId', ] as const, playlist: [ 'enterDescriptionHere', From 8cb9bb8a8f11e8e9273597bca319b3d4bcf3b92a Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 11:23:50 -0400 Subject: [PATCH 53/83] fix: child launching - children inherit on empty platform. --- src/back/responses.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/back/responses.ts b/src/back/responses.ts index 17afc0c98..86d17d8fd 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -234,6 +234,13 @@ export function registerRequestCallbacks(state: BackState): void { // Note: we explicitly don't fetch the parent's children. We already have the only child we're interested in. game.parentGame = await GameManager.findGame(game.parentGameId, undefined, true); } + // Inherit empty fields. + if (game.parentGame) { + if (game.platform === '') { + game.platform = game.parentGame.platform; + } + // Ardil TODO any more I should add? + } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; if (configServer) { From 4f519508083ec2c73db6df6c9e999b5173b791ac Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 17:47:32 -0400 Subject: [PATCH 54/83] Update the download button on child launch. --- src/renderer/components/RightBrowseSidebar.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index ad5ff64eb..dc4da6b88 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -626,7 +626,10 @@ export class RightBrowseSidebar extends React.Component { + addApp && this.props.onGameLaunch(addApp.id) + .then(this.onForceUpdateGameData); + }} onDelete={this.onChildDelete} /> )) } {game.extras && game.extrasName ? @@ -955,10 +958,6 @@ export class RightBrowseSidebar extends React.Component Date: Sat, 2 Apr 2022 18:06:14 -0400 Subject: [PATCH 55/83] Add option in findPlatforms to exclude children. Previously, the blank platform strings of children were included in the results of findPlatforms(). --- src/back/game/GameManager.ts | 11 +++++++---- src/back/responses.ts | 3 ++- typings/flashpoint-launcher.d.ts | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 39475b659..5311979d6 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -272,13 +272,16 @@ export async function findUniqueValuesInOrder(entity: any, column: string): Prom return Coerce.strArray(values.map(v => v[`entity_${column}`])); } -export async function findPlatforms(library: string): Promise { +export async function findPlatforms(library: string, includeChildren?: boolean): Promise { const gameRepository = getManager().getRepository(Game); - const libraries = await gameRepository.createQueryBuilder('game') + const query = gameRepository.createQueryBuilder('game') .where('game.library = :library', {library: library}) .select('game.platform') - .distinct() - .getRawMany(); + .distinct(); + if (!includeChildren) { + query.andWhere('game.parentGameId IS NULL'); + } + const libraries = await query.getRawMany(); return Coerce.strArray(libraries.map(l => l.game_platform)); } diff --git a/src/back/responses.ts b/src/back/responses.ts index 86d17d8fd..502536d0a 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -95,7 +95,8 @@ export function registerRequestCallbacks(state: BackState): void { const mad4fpEnabled = state.serviceInfo ? (state.serviceInfo.server.findIndex(s => s.mad4fp === true) !== -1) : false; const platforms: Record = {}; for (const library of libraries) { - platforms[library] = (await GameManager.findPlatforms(library)).sort(); + // Explicitly exclude the platforms of child curations - they're mostly blank. + platforms[library] = (await GameManager.findPlatforms(library, false)).sort(); } // Fire after return has sent diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index 80e9ccd55..eeabc487d 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -194,8 +194,9 @@ declare module 'flashpoint-launcher' { /** * Returns all unique Platform strings in a library * @param library Library to search + * @param includeChildren Whether to include child curations in the platform search. Default: false. */ - function findPlatforms(library: string): Promise; + function findPlatforms(library: string, includeChildren?: boolean): Promise; /** * Parses a Playlist JSON file and returns an object you can save later. * @param jsonData Raw JSON data of the Playlist file From 9c96d2f622c07ab9102c89f454605095bc8f3f39 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:00:36 -0400 Subject: [PATCH 56/83] Make the nullable properties actually nullable. Also make some == into ===, and make the message bar appear in when browsing. --- src/back/game/GameManager.ts | 16 ++++++------- src/back/importGame.ts | 5 +++- src/back/responses.ts | 2 +- src/database/entity/Game.ts | 20 ++++++++-------- src/renderer/components/CurateBox.tsx | 6 ++--- .../components/RightBrowseSidebar.tsx | 24 +++++++++---------- src/shared/curate/metaToMeta.ts | 9 +++---- typings/flashpoint-launcher.d.ts | 10 ++++---- 8 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 5311979d6..d632aa53c 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -292,16 +292,16 @@ export async function updateGames(games: Game[]): Promise { for (const game of chunk) { // Set nullable properties to null if they're empty. if (game.parentGameId === '') { - game.parentGameId = undefined; + game.parentGameId = null; } if (game.extras === '') { - game.extras = undefined; + game.extras = null; } if (game.extrasName === '') { - game.extrasName = undefined; + game.extrasName = null; } if (game.message === '') { - game.message = undefined; + game.message = null; } await transEntityManager.save(Game, game); } @@ -313,16 +313,16 @@ export async function save(game: Game): Promise { const gameRepository = getManager().getRepository(Game); // Set nullable properties to null if they're empty. if (game.parentGameId === '') { - game.parentGameId = undefined; + game.parentGameId = null; } if (game.extras === '') { - game.extras = undefined; + game.extras = null; } if (game.extrasName === '') { - game.extrasName = undefined; + game.extrasName = null; } if (game.message === '') { - game.message = undefined; + game.message = null; } log.debug('Launcher', 'Saving game...'); const savedGame = await gameRepository.save(game); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 38ed863de..de050cdea 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -305,7 +305,7 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration const game: Game = new Game(); Object.assign(game, { id: gameId, // (Re-use the id of the curation) - parentGameId: gameMeta.parentGameId === '' ? undefined : gameMeta.parentGameId, + parentGameId: gameMeta.parentGameId === '' ? null : gameMeta.parentGameId ? gameMeta.parentGameId : null, title: gameMeta.title || '', alternateTitles: gameMeta.alternateTitles || '', series: gameMeta.series || '', @@ -323,6 +323,9 @@ async function createGameFromCurationMeta(gameId: string, gameMeta: EditCuration version: gameMeta.version || '', originalDescription: gameMeta.originalDescription || '', language: gameMeta.language || '', + message: gameMeta.message || null, + extrasName: gameMeta.extrasName || null, + extras: gameMeta.extras || null, dateAdded: date.toISOString(), dateModified: date.toISOString(), broken: false, diff --git a/src/back/responses.ts b/src/back/responses.ts index 502536d0a..6676a64ed 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -578,7 +578,7 @@ export function registerRequestCallbacks(state: BackState): void { } const game = await GameManager.findGame(gameData.gameId); if (game) { - game.activeDataId = undefined; + game.activeDataId = null; game.activeDataOnDisk = false; await GameManager.save(game); } diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index aaba46526..68dfe9cef 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -19,8 +19,8 @@ export class Game { @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; - @Column({ nullable: true }) - parentGameId?: string; + @Column({ type: "varchar", nullable: true }) + parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. @OneToMany((type) => Game, (game) => game.parentGame) @@ -127,8 +127,8 @@ export class Game { placeholder: boolean; /** ID of the active data */ - @Column({ nullable: true }) - activeDataId?: number; + @Column({ type: "integer", nullable: true }) + activeDataId: number | null; /** Whether the data is present on disk */ @Column({ default: false }) @@ -137,14 +137,14 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; - @Column({ nullable: true }) - extras?: string; + @Column({ type: "varchar", nullable: true }) + extras: string | null; - @Column({ nullable: true }) - extrasName?: string; + @Column({ type: "varchar", nullable: true }) + extrasName: string | null; - @Column({ nullable: true }) - message?: string; + @Column({ type: "varchar", nullable: true }) + message: string | null; // This doesn't run... sometimes. @BeforeUpdate() diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index e58b29225..570a86aa6 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -945,13 +945,13 @@ function useOnInputChange(property: keyof EditCurationMeta, key: string | undefi return useCallback((event: InputElementOnChangeEvent) => { if (key !== undefined) { // If it's one of the nullable types, treat '' as undefined. - if (property == 'parentGameId' || property == 'extras' || property == 'extrasName' || property == 'message') { + if (property === 'parentGameId' || property === 'extras' || property === 'extrasName' || property === 'message') { dispatch({ type: 'edit-curation-meta', payload: { key: key, property: property, - value: event.currentTarget.value == '' ? undefined : event.currentTarget.value + value: event.currentTarget.value === '' ? undefined : event.currentTarget.value } }); } else { @@ -1163,7 +1163,7 @@ export function getCurationWarnings(curation: EditCuration, suggestions: Partial if (parentId !== '') { warns.invalidParentGameId = true; window.Shared.back.request(BackIn.GET_GAME, parentId).then((result) => { - warns.invalidParentGameId = result == undefined; + warns.invalidParentGameId = result === undefined; }); } else { // If the parentGameId is undefined/empty, it's just a non-child game. That's fine. diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index dc4da6b88..7481d8170 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -506,18 +506,16 @@ export class RightBrowseSidebar extends React.Component
- {game.message ? -
-

{strings.message}:

- -
- : undefined} +
+

{strings.message}:

+ +

{strings.extrasName}:

) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) || game.extras ? ( + { editable || (currentChildren && currentChildren.length > 0) || (game.extras && game.extrasName) ? (

{strings.additionalApplications}:

diff --git a/src/shared/curate/metaToMeta.ts b/src/shared/curate/metaToMeta.ts index 1ac00414c..8900829e8 100644 --- a/src/shared/curate/metaToMeta.ts +++ b/src/shared/curate/metaToMeta.ts @@ -34,10 +34,11 @@ export function convertGameToCurationMetaFile(game: Game, categories: TagCategor parsed['Launch Command'] = game.launchCommand; parsed['Game Notes'] = game.notes; parsed['Original Description'] = game.originalDescription; - parsed['Parent Game ID'] = game.parentGameId; - parsed['Extras'] = game.extras; - parsed['Extras Name'] = game.extrasName; - parsed['Message'] = game.message; + // The meta files use undefined, the DB uses null. + parsed['Parent Game ID'] = game.parentGameId ? game.parentGameId : undefined; + parsed['Extras'] = game.extras ? game.extras : undefined; + parsed['Extras Name'] = game.extrasName ? game.extrasName : undefined; + parsed['Message'] = game.message ? game.message : undefined; // Return return parsed; } diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index eeabc487d..be326db68 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -455,7 +455,7 @@ declare module 'flashpoint-launcher' { /** This game's parent game. */ parentGame?: Game; /** ID of the game which owns this game */ - parentGameId?: string; + parentGameId: string | null; /** A list of child games. */ children?: Game[]; /** Full title of the game */ @@ -509,15 +509,15 @@ declare module 'flashpoint-launcher' { /** If the game is a placeholder (and can therefore not be saved) */ placeholder: boolean; /** ID of the active data */ - activeDataId?: number; + activeDataId: number | null; /** Whether the data is present on disk */ activeDataOnDisk: boolean; /** The path to any extras. */ - extras?: string; + extras: string | null; /** The name to be displayed for those extras. */ - extrasName?: string; + extrasName: string | null; /** The message to display when the game starts. */ - message?: string; + message: string | null; data?: GameData[]; updateTagsStr: () => void; From 0f78d9f68bf2380d3acc2f048289393d884da00b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:10:13 -0400 Subject: [PATCH 57/83] Make the linter happy about b49d9c1. --- src/database/entity/Game.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 68dfe9cef..0d8b984d0 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -19,7 +19,7 @@ export class Game { @ManyToOne((type) => Game, (game) => game.children) parentGame?: Game; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. @@ -127,7 +127,7 @@ export class Game { placeholder: boolean; /** ID of the active data */ - @Column({ type: "integer", nullable: true }) + @Column({ type: 'integer', nullable: true }) activeDataId: number | null; /** Whether the data is present on disk */ @@ -137,13 +137,13 @@ export class Game { @OneToMany(type => GameData, datas => datas.game) data?: GameData[]; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) extras: string | null; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) extrasName: string | null; - @Column({ type: "varchar", nullable: true }) + @Column({ type: 'varchar', nullable: true }) message: string | null; // This doesn't run... sometimes. From 5c4f3b906348a7facd129d9f05250e784c53968e Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:17:40 -0400 Subject: [PATCH 58/83] Add new lang strings for English. --- lang/en.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lang/en.json b/lang/en.json index 30d6fd331..2dad855cd 100644 --- a/lang/en.json +++ b/lang/en.json @@ -301,7 +301,13 @@ "noGameMatchedSearch": "Try searching for something less restrictive.", "mountParameters": "Mount Parameters", "noMountParameters": "No Mount Parameters", - "showExtremeScreenshot": "Show Extreme Screenshot" + "showExtremeScreenshot": "Show Extreme Screenshot", + "extras": "Extras", + "noExtras": "No Extras", + "message": "Launch Message", + "noMessage": "No Message", + "extrasName": "Extras Name", + "noExtrasName": "No Extras Name" }, "tags": { "name": "Name", @@ -390,7 +396,10 @@ "noScreenshot": "There is no screenshot on this curation.", "ilc_notHttp": "Use HTTP.", "ilc_nonExistant": "Point to an existing file in your curation's 'content' folder.", - "sort": "Sort Curations (A-Z)" + "sort": "Sort Curations (A-Z)", + "parentGameId": "Parent Game ID", + "noParentGameId": "No Parent Game ID", + "invalidParentGameId": "Invalid Parent Game ID!" }, "playlist": { "enterDescriptionHere": "Enter a description here...", From a636b114daf26f464629abbb5f78dc7959f8977b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 22:40:15 -0400 Subject: [PATCH 59/83] Give extras launch button its own section in sidebar. --- .../components/RightBrowseSidebar.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 7481d8170..5494f1ec5 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -613,8 +613,19 @@ export class RightBrowseSidebar extends React.Component
) : undefined } + {(game.extras && game.extrasName) ? ( +
+
+

{strings.extras}:

+
+ +
+ ) : undefined } {/* -- Additional Applications -- */} - { editable || (currentChildren && currentChildren.length > 0) || (game.extras && game.extrasName) ? ( + { editable || (currentChildren && currentChildren.length > 0) ? (

{strings.additionalApplications}:

@@ -630,12 +641,6 @@ export class RightBrowseSidebar extends React.Component )) } - {game.extras && game.extrasName ? - - : undefined}
) : undefined } {/* -- Application Path & Launch Command -- */} From 4e1dffc9df7daf8cc54c440e69eacc6786555a84 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sat, 2 Apr 2022 23:55:18 -0400 Subject: [PATCH 60/83] Fix game count update on delete, remove TODOs. --- src/back/responses.ts | 16 +--------------- src/renderer/app.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index 6676a64ed..738a4e40a 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -188,7 +188,6 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_TOTAL, async (event) => { return await GameManager.countGames(); }); @@ -201,7 +200,6 @@ export function registerRequestCallbacks(state: BackState): void { return data; }); - // Ardil TODO state.socketServer.register(BackIn.GET_EXEC, (event) => { return state.execMappings; }); @@ -225,7 +223,7 @@ export function registerRequestCallbacks(state: BackState): void { }); } }); - // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_GAME, async (event, id) => { const game = await GameManager.findGame(id, undefined, true); @@ -331,12 +329,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAMES, async (event, data) => { await GameManager.updateGames(data); }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME, async (event, data) => { try { const game = await GameManager.save(data); @@ -352,7 +348,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { // Ardil TODO figure out this thing. const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); @@ -421,7 +416,6 @@ export function registerRequestCallbacks(state: BackState): void { }; });*/ - // Ardil TODO state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); if (playlist) { @@ -438,7 +432,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_PLAYLIST, async (event, filePath, library) => { try { const rawData = await fs.promises.readFile(filePath, 'utf-8'); @@ -474,7 +467,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_ALL_PLAYLISTS, async (event) => { const playlists = await GameManager.findPlaylists(true); for (const playlist of playlists) { @@ -483,7 +475,6 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.send(event.client, BackOut.PLAYLISTS_CHANGE, await GameManager.findPlaylists(state.preferences.browsePageShowExtreme)); }); - // Ardil TODO state.socketServer.register(BackIn.EXPORT_PLAYLIST, async (event, id, location) => { const playlist = await GameManager.findPlaylist(id, true); if (playlist) { @@ -493,7 +484,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO ensure that we really don't need children for this. state.socketServer.register(BackIn.EXPORT_GAME, async (event, id, location, metaOnly) => { if (await pathExists(metaOnly ? path.dirname(location) : location)) { const game = await GameManager.findGame(id, undefined, true); @@ -530,7 +520,6 @@ export function registerRequestCallbacks(state: BackState): void { return await GameManager.findGame(id); }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); // Verify it's still on disk @@ -546,12 +535,10 @@ export function registerRequestCallbacks(state: BackState): void { return gameData; }); - // Ardil TODO state.socketServer.register(BackIn.GET_GAMES_GAME_DATA, async (event, id) => { return GameDataManager.findGameData(id); }); - // Ardil TODO state.socketServer.register(BackIn.SAVE_GAME_DATAS, async (event, data) => { // Ignore presentOnDisk, client isn't the most aware await Promise.all(data.map(async (d) => { @@ -564,7 +551,6 @@ export function registerRequestCallbacks(state: BackState): void { })); }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_GAME_DATA, async (event, gameDataId) => { const gameData = await GameDataManager.findOne(gameDataId); if (gameData) { diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index a120563d5..c8c8a72ce 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -888,8 +888,13 @@ export class App extends React.Component { const strings = this.props.main.lang; const library = getBrowseSubPath(this.props.location.pathname); window.Shared.back.request(BackIn.DELETE_GAME, gameId) - .then(() => { this.setViewQuery(library); }) - .catch((error) => { + .then((deleteResults) => { + this.props.dispatchMain({ + type: MainActionType.SET_GAMES_TOTAL, + total: deleteResults.gamesTotal, + }); + this.setViewQuery(library); + }).catch((error) => { log.error('Launcher', `Error deleting game: ${error}`); alert(strings.dialog.unableToDeleteGame + '\n\n' + error); }); From e881b5b46741aa8e3c53e8ba674201aaa3d010da Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Sun, 3 Apr 2022 15:11:26 -0400 Subject: [PATCH 61/83] Fix game data browser for single-pack games. --- src/renderer/components/GameDataBrowser.tsx | 6 +++--- src/renderer/components/RightBrowseSidebar.tsx | 2 +- src/renderer/components/pages/BrowsePage.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/GameDataBrowser.tsx b/src/renderer/components/GameDataBrowser.tsx index 2686e4b18..f222285ad 100644 --- a/src/renderer/components/GameDataBrowser.tsx +++ b/src/renderer/components/GameDataBrowser.tsx @@ -31,7 +31,7 @@ export type GameDataBrowserProps = { game: Game; onClose: () => void; onEditGame: (game: Partial) => void; - onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId?: number) => void; + onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId: number | null) => void; onForceUpdateGameData: () => void; } @@ -122,7 +122,7 @@ export class GameDataBrowser extends React.Component { await window.Shared.back.request(BackIn.DELETE_GAME_DATA, id); if (this.props.game.activeDataId === id) { - this.props.onUpdateActiveGameData(false); + this.props.onUpdateActiveGameData(false, null); } const newPairedData = [...this.state.pairedData]; const idx = newPairedData.findIndex(pr => pr.id === id); @@ -146,7 +146,7 @@ export class GameDataBrowser extends React.Component this.onUpdateTitle(index, title)} onUpdateParameters={(parameters) => this.onUpdateParameters(index, parameters)} onActiveToggle={() => { - this.props.onUpdateActiveGameData(data.presentOnDisk, data.id); + this.props.onUpdateActiveGameData(data.presentOnDisk, data.id === this.props.game.activeDataId ? null : data.id); }} onUninstall={() => { window.Shared.back.request(BackIn.UNINSTALL_GAME_DATA, data.id) diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index 5494f1ec5..de3d297ac 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -69,7 +69,7 @@ type OwnProps = { onOpenExportMetaEdit: (gameId: string) => void; onEditGame: (game: Partial) => void; - onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId?: number) => void; + onUpdateActiveGameData: (activeDataOnDisk: boolean, activeDataId: number | null) => void; }; export type RightBrowseSidebarProps = OwnProps & WithPreferencesProps & WithSearchProps & WithConfirmDialogProps; diff --git a/src/renderer/components/pages/BrowsePage.tsx b/src/renderer/components/pages/BrowsePage.tsx index 5b6a28884..7cab74bfa 100644 --- a/src/renderer/components/pages/BrowsePage.tsx +++ b/src/renderer/components/pages/BrowsePage.tsx @@ -697,7 +697,7 @@ export class BrowsePage extends React.Component { + onUpdateActiveGameData = (activeDataOnDisk: boolean, activeDataId: number | null): void => { if (this.state.currentGame) { const newGame = new Game(); Object.assign(newGame, {...this.state.currentGame, activeDataOnDisk, activeDataId }); From f7695887887b6f1c7e74af69b0880ac44d94cf41 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 4 Apr 2022 08:09:48 -0400 Subject: [PATCH 62/83] Fix getRootPath() --- src/renderer/curate/importCuration.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/curate/importCuration.ts b/src/renderer/curate/importCuration.ts index ebc796da2..134bec5cf 100644 --- a/src/renderer/curate/importCuration.ts +++ b/src/renderer/curate/importCuration.ts @@ -168,11 +168,14 @@ async function getRootPath(dir: string): Promise { // Convert it to lower-case, because the extensions we're matching against // are lower-case. if (endsWithList(fullpath.toLowerCase(), validMetaNames)) { - return fullpath; + return path.dirname(fullpath); } } else if (stats.isDirectory()) { + const contents: string[] = await fs.readdir(fullpath); // We have a directory. Push all of the directory's contents onto the end of the queue. - queue.push(...(await fs.readdir(fullpath))); + for (const k of contents) { + queue.push(path.join(entry, k)); + } } } } From db0bbc74750ba0fc0681e31a1976cd457711b380 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 6 Apr 2022 20:53:38 -0400 Subject: [PATCH 63/83] Revert "build: switch to better-sqlite3" This reverts commit c64d1f71c6458f6e5a730e1c8e2b54a62346b462. --- ormconfig.json | 4 ++-- package.json | 2 +- src/back/index.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ormconfig.json b/ormconfig.json index beef81b3d..45a4124b7 100644 --- a/ormconfig.json +++ b/ormconfig.json @@ -1,5 +1,5 @@ { - "type": "better-sqlite3", + "type": "sqlite", "host": "localhost", "port": 3306, "username": "flashpoint", @@ -22,4 +22,4 @@ "migrationsDir": "src/database/migration", "subscribersDir": "src/database/subscriber" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 58ef7342d..92bc9c25a 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.15", "axios": "0.21.4", - "better-sqlite3": "^7.5.0", "connected-react-router": "6.8.0", "electron-updater": "4.3.1", "electron-util": "0.14.2", @@ -58,6 +57,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", + "sqlite3": "^5.0.2", "tail": "2.0.3", "typeorm": "0.2.37", "typesafe-actions": "4.4.2", diff --git a/src/back/index.ts b/src/back/index.ts index 02c007fbc..622f84ebc 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -35,7 +35,7 @@ import * as mime from 'mime'; import * as path from 'path'; import 'reflect-metadata'; // Required for the DB Models to function -import 'better-sqlite3'; +import 'sqlite3'; import { Tail } from 'tail'; import { ConnectionOptions, createConnection } from 'typeorm'; import { ConfigFile } from './ConfigFile'; @@ -309,7 +309,7 @@ async function onProcessMessage(message: any, sendHandle: any): Promise { // Setup DB if (!state.connection) { const options: ConnectionOptions = { - type: 'better-sqlite3', + type: 'sqlite', database: path.join(state.config.flashpointPath, 'Data', 'flashpoint.sqlite'), entities: [Game, Playlist, PlaylistGame, Tag, TagAlias, TagCategory, GameData, Source, SourceData], migrations: [Initial1593172736527, AddExtremeToPlaylist1599706152407, GameData1611753257950, SourceDataUrlPath1612434225789, SourceFileURL1612435692266, From 6bf31389ee5a39faa52d12fccb2e925d0a6ef297 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:18:28 -0400 Subject: [PATCH 64/83] fix: load tags for children. Also: slight cleanup. Remove some temporary statements, and add a relation-loading statement for the tags of child games. --- src/back/game/GameManager.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index d632aa53c..ca4f1325b 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -38,7 +38,7 @@ export async function countGames(): Promise { return gameRepository.count({ parentGameId: IsNull() }); } -/** Find the game with the specified ID. Ardil TODO find refs*/ +/** Find the game with the specified ID. */ export async function findGame(id?: string, filter?: FindOneOptions, noChildren?: boolean): Promise { if (id || filter) { const gameRepository = getManager().getRepository(Game); @@ -50,6 +50,13 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi .relation('children') .of(game) .loadMany(); + // Load tags for the children too. + for (const child of game.children) { + child.tags = await gameRepository.createQueryBuilder() + .relation('tags') + .of(child) + .loadMany(); + } } if (game) { if (game.tags) { @@ -72,7 +79,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num, game.parentGameId`) + .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } @@ -88,9 +95,7 @@ export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, o .setParameters(subQ.getParameters()) .select('row_num') .from('(' + subQ.getQuery() + ')', 'g') - .where('g.id = :gameId', { gameId: gameId }) - // Shouldn't be needed, but doing it anyway. - .andWhere('g.parentGameId IS NULL'); + .where('g.id = :gameId', { gameId: gameId }); const raw = await query.getRawOne(); // console.log(`${Date.now() - startTime}ms for row`); From 5b0a8faf9b83b765be8adfbf95d3f6a8cfdae7c7 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:19:24 -0400 Subject: [PATCH 65/83] test: GameManager: findGame() and countGames(). --- jest.config.ts | 1 + tests/src/back/game/GameManager.test.ts | 167 ++++++++++++++++++ tests/src/back/game/exampleDB.ts | 224 ++++++++++++++++++++++++ 3 files changed, 392 insertions(+) create mode 100644 tests/src/back/game/GameManager.test.ts create mode 100644 tests/src/back/game/exampleDB.ts diff --git a/jest.config.ts b/jest.config.ts index 03ca46b9d..f185eb72a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,6 +12,7 @@ const config: Config.InitialOptions = { '^@main(.*)$': '/src/main/$1', '^@renderer(.*)$': '/src/renderer/$1', '^@back(.*)$': '/src/back/$1', + '^@database(.*)$': '/src/database/$1', '^@tests(.*)$': '/tests/$1' }, testPathIgnorePatterns: [ diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts new file mode 100644 index 000000000..677b909bd --- /dev/null +++ b/tests/src/back/game/GameManager.test.ts @@ -0,0 +1,167 @@ +import * as GameManager from '@back/game/GameManager'; +import { uuid } from '@back/util/uuid'; +import { Game } from '@database/entity/Game'; +import { GameData } from '@database/entity/GameData'; +import { Playlist } from '@database/entity/Playlist'; +import { PlaylistGame } from '@database/entity/PlaylistGame'; +import { Source } from '@database/entity/Source'; +import { SourceData } from '@database/entity/SourceData'; +import { Tag } from '@database/entity/Tag'; +import { TagAlias } from '@database/entity/TagAlias'; +import { TagCategory } from '@database/entity/TagCategory'; +import { Initial1593172736527 } from '@database/migration/1593172736527-Initial'; +import { AddExtremeToPlaylist1599706152407 } from '@database/migration/1599706152407-AddExtremeToPlaylist'; +import { GameData1611753257950 } from '@database/migration/1611753257950-GameData'; +import { SourceDataUrlPath1612434225789 } from '@database/migration/1612434225789-SourceData_UrlPath'; +import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-Source_FileURL'; +import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; +import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; +import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; +import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; +import { + ConnectionOptions, + createConnection, + getConnection, + getManager +} from 'typeorm'; +import { gameArray } from './exampleDB'; + +const formatLocal = (input: Game): Partial => { + const partial: Partial = input; + delete partial.placeholder; + delete partial.updateTagsStr; + return partial; +}; +const formatDB = (input?: Game): Game | undefined => { + if (input) { + input.dateAdded = new Date(input.dateAdded).toISOString(); + } + return input; +}; +const formatDBMany = (input?: Game[]): Partial[] | undefined => { + if (input) { + input.forEach((game) => { + // TODO It seems the types aren't quite right? This conversion *should* be unnecessary, but here we are? + game.dateAdded = new Date(game.dateAdded).toISOString(); + }); + } + return input; +}; + +beforeAll(async () => { + const options: ConnectionOptions = { + type: 'sqlite', + database: ':memory:', + entities: [ + Game, + Playlist, + PlaylistGame, + Tag, + TagAlias, + TagCategory, + GameData, + Source, + SourceData, + ], + migrations: [ + Initial1593172736527, + AddExtremeToPlaylist1599706152407, + GameData1611753257950, + SourceDataUrlPath1612434225789, + SourceFileURL1612435692266, + SourceFileCount1612436426353, + GameTagsStr1613571078561, + GameDataParams1619885915109, + ChildCurations1648251821422, + ], + }; + const connection = await createConnection(options); + // TypeORM forces on but breaks Playlist Game links to unimported games + await connection.query('PRAGMA foreign_keys=off;'); + await connection.runMigrations(); +}); + +afterAll(async () => { + await getConnection().close(); +}); + +/* ASSUMPTIONS MADE: + * Each testing block will receive a clean database. Ensure that each testing block leaves a clean DB. + */ + +describe('GameManager.findGame()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); + }); + test('Find game by UUID', async () => { + expect( + formatDB(await GameManager.findGame(gameArray[0].id, undefined, true)) + ).toEqual(formatLocal(gameArray[0])); + }); + test('Dont find game by UUID', async () => { + // Generate a new UUID and try to fetch it. Should fail. + expect(formatDB(await GameManager.findGame(uuid()))).toBeUndefined(); + }); + test('Find game by property', async () => { + expect( + formatDB( + await GameManager.findGame( + undefined, + { where: { title: gameArray[0].title } }, + true + ) + ) + ).toEqual(formatLocal(gameArray[0])); + }); + test('Dont find game by property', async () => { + // At this point, I'm just using uuid() as a random string generator. + expect( + formatDB( + await GameManager.findGame(undefined, { where: { title: uuid() } }) + ) + ).toBeUndefined(); + }); + test('Find game including children', async () => { + expect( + formatDBMany((await GameManager.findGame(gameArray[0].id))?.children) + ).toEqual([formatLocal(gameArray[1])]); + }); + test('Find game excluding children', async () => { + expect( + formatDBMany( + (await GameManager.findGame(gameArray[0].id, undefined, true))?.children + ) + ).toBeUndefined(); + }); + test('Find game lacking children', async () => { + expect( + formatDBMany((await GameManager.findGame(gameArray[1].id))?.children) + ).toBeUndefined(); + }); +}); + +describe('GameManager.countGames()', () => { + beforeEach(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterEach(async () => { + await getManager().getRepository(Game).clear(); + }); + test('Count games', async () => { + // Count the number of games that have a null parentGameId. + let count = 0; + gameArray.forEach((game) => { + if (!game.parentGameId) { + count++; + } + }); + expect(await GameManager.countGames()).toBe(count); + }); + test('Count zero games', async () => { + getManager().getRepository(Game).clear(); + expect(await GameManager.countGames()).toBe(0); + }); +}); diff --git a/tests/src/back/game/exampleDB.ts b/tests/src/back/game/exampleDB.ts new file mode 100644 index 000000000..f5f330ebf --- /dev/null +++ b/tests/src/back/game/exampleDB.ts @@ -0,0 +1,224 @@ +import { Game } from '@database/entity/Game'; + +export const gameArray: Game[] = [ + { + id: 'c6ca5ded-42f4-4251-9423-55700140b096', + parentGameId: null, + title: '"Alone"', + alternateTitles: '', + series: '', + developer: 'Natpat', + publisher: 'MoFunZone; Sketchy', + dateAdded: '2019-11-24T23:39:57.629Z', + dateModified: '2021-03-07T02:08:12.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Platformer; Puzzle; Pixel', + source: 'https://www.newgrounds.com/portal/view/578326', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: 'http://uploads.ungrounded.net/578000/578326_Preloader.swf', + releaseDate: '2011-08-28', + version: '', + originalDescription: + 'Play as a penguin and a tortoise solving puzzles using a jetpack and a gun in this challenging, funky, colourful platformer!\nPlay alongside a beautiful soundtrack with 3 different songs, and funky graphics. Can you beat all 20 levels?\nI\'m so glad I\'m finally getting this out. Finally! :D Enjoy :)', + language: 'en', + library: 'arcade', + orderTitle: '"alone"', + activeDataId: null, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: '6fabbb7a-f614-455c-a239-360b6b69ea24', + // This is not the real parent game id. It doesn't matter, deal with it. + parentGameId: 'c6ca5ded-42f4-4251-9423-55700140b096', + title: '"Game feel" demo', + alternateTitles: '', + series: '', + developer: 'Sebastien Benard', + publisher: 'Deepnight.net', + dateAdded: '2021-01-25T23:30:49.267Z', + dateModified: '2022-04-03T17:37:21.000Z', + platform: 'HTML5', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: + 'Demonstration; Action; Platformer; Shooter; Pixel; Side-Scrolling', + source: 'http://deepnight.net/games/game-feel/', + applicationPath: 'FPSoftware\\Basilisk-Portable\\Basilisk-Portable.exe', + launchCommand: 'http://deepnight.net/games/game-feel/', + releaseDate: '2019-12-19', + version: '', + originalDescription: + 'This prototype is not exactly an actual game. It was developed to serve as a demonstration for a “Game feel” talk in 2019 at the ENJMIN school.\n\nIt shows the impact of small details on the overall quality of a game.\n\nYou will need a GAMEPAD to test it. You can enable or disable game features in this demo by pressing the START button.\n\nGAMEPAD is required to play\nA\njump\nB\ndash\nX\nshoot\nSTART\nenable/disable features\nSELECT\nrestart', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 8656, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: '4b1c582f-c953-48d4-b839-22897adc8406', + parentGameId: null, + title: '"Eight Planets and a Dwarf" Sudoku', + alternateTitles: '', + series: '', + developer: 'Julia Genyuk; Dave Fisher', + publisher: 'Windows to the Universe', + dateAdded: '2022-02-16T03:34:35.697Z', + dateModified: '2022-02-16T04:08:14.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Space; Sudoku', + source: 'https://www.windows2universe.org/games/sudoku/sudoku.html', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://www.windows2universe.org/games/sudoku/planets_sudoku.swf', + releaseDate: '', + version: '', + originalDescription: '', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 96874, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'f82c01e0-a30e-49e2-84b2-9b45c437eda6', + parentGameId: null, + title: '"Mind Realm"', + alternateTitles: '', + series: '', + developer: 'Wisdomchild', + publisher: 'WET GAMIN', + dateAdded: '2021-09-03T13:42:06.533Z', + dateModified: '2021-12-13T02:09:25.000Z', + platform: 'HTML5', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Puzzle; Score-Attack; Pixel; Arcade', + source: 'http://wetgamin.com/mindrealm.php', + applicationPath: 'FPSoftware\\Basilisk-Portable\\Basilisk-Portable.exe', + launchCommand: 'http://wetgamin.com/html5/mindrealm/index.html', + releaseDate: '', + version: '', + originalDescription: '', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 44546, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'b7dbd71b-d099-4fdf-9127-6e0a341b7f2d', + parentGameId: null, + title: '"Build a Tree" Dendrochronology Activity', + alternateTitles: '', + series: '', + developer: '', + publisher: 'Windows to the Universe', + dateAdded: '2022-02-18T04:20:19.301Z', + dateModified: '2022-02-18T04:31:29.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Creative; Educational; Object Creator; Toy', + source: + 'https://www.windows2universe.org/earth/climate/dendrochronology_build_tree.html', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://www.windows2universe.org/earth/climate/images/dendrochronology_build_tree.swf', + releaseDate: '', + version: '', + originalDescription: + 'The interactive diagram below demonstrates a very simple model of tree ring growth.\n\nSelect a temperature range (Normal, Cool, or Warm) and a precipitation amount (Normal, Dry, or Wet) for the coming year. Click the "Add Yearly Growth" button. The tree (which you are viewing a cross-section of the trunk of) grows one year\'s worth, adding a new ring.\n\nAdd some rings while varying the temperature and precipitation. Which of these factors has a stronger influence on the growth of the type of tree being modeled here?\n\nUse the "Reset" button to start over.\n\nThe "Show Specimen Tree" button displays a section of an "actual" tree specimen. Can you model the annual climate during each year of the specimen tree\'s life, matching your diagram with the specimen, to determine the climate history "written" in the rings of the specimen tree? (The "answer" is listed below, lower down on this page).', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 97171, + activeDataOnDisk: false, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, + { + id: 'a95d0ff7-3ee9-460f-a4f9-0e0c77764d13', + parentGameId: null, + title: '!BETA! little bullet hell', + alternateTitles: '', + series: '', + developer: 'leonidoss341', + publisher: 'Newgrounds', + dateAdded: '2021-08-04T06:09:06.114Z', + dateModified: '2021-08-04T06:09:44.000Z', + platform: 'Flash', + broken: false, + extreme: false, + playMode: 'Single Player', + status: 'Playable', + notes: '', + tagsStr: 'Action; Shooter', + source: 'https://www.newgrounds.com/portal/view/624363', + applicationPath: 'FPSoftware\\Flash\\flashplayer_32_sa.exe', + launchCommand: + 'http://uploads.ungrounded.net/624000/624363_touhou_project_tutorial.swf', + releaseDate: '2013-08-29', + version: 'Beta', + originalDescription: + 'Testing bullets. Just some fun\n\nAuthor Comments:\n\nWARNING! It\'s just beta, no need to say me that you wanna game, please tell me: everything work good or not? Also i want to know about graphic.\nMovement: keys or WASD, Shift-focus, Z-shooting.', + language: 'en', + library: 'arcade', + orderTitle: '', + activeDataId: 32195, + activeDataOnDisk: true, + extras: null, + extrasName: null, + message: null, + tags: [], + placeholder: false, + updateTagsStr: new Game().updateTagsStr, + }, +]; From 2614e615ff9f7d6f5a604b9448264434d5472a4f Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:24:33 -0400 Subject: [PATCH 66/83] fix: make orderBy required in findGameRow() Previously, the param was marked as optional. However, failure to supply it resulted in an error (column not found). --- src/back/game/GameManager.ts | 2 +- src/back/responses.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index ca4f1325b..2215b6b69 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -72,7 +72,7 @@ export async function findGame(id?: string, filter?: FindOneOptions, noChi } } /** Get the row number of an entry, specified by its gameId. */ -export async function findGameRow(gameId: string, filterOpts?: FilterGameOpts, orderBy?: GameOrderBy, direction?: GameOrderReverse, index?: PageTuple): Promise { +export async function findGameRow(gameId: string, orderBy: GameOrderBy, direction?: GameOrderReverse, filterOpts?: FilterGameOpts, index?: PageTuple): Promise { if (orderBy) { validateSqlName(orderBy); } // const startTime = Date.now(); diff --git a/src/back/responses.ts b/src/back/responses.ts index 738a4e40a..cd091e23b 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -802,9 +802,9 @@ export function registerRequestCallbacks(state: BackState): void { state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, - query.filter, query.orderBy, query.orderReverse, + query.filter, undefined); return position - 1; // ("position" starts at 1, while "index" starts at 0) From 0cf811f5a564634f469419e8711790d190691018 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:56:50 -0400 Subject: [PATCH 67/83] fix: findGameRow() keyset pagination directions Switch the comparison for keyset pagination when orderBy is different. --- src/back/game/GameManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 2215b6b69..9b97f0931 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -79,11 +79,11 @@ export async function findGameRow(gameId: string, orderBy: GameOrderBy, directio const gameRepository = getManager().getRepository(Game); const subQ = gameRepository.createQueryBuilder('game') - .select(`game.id, row_number() over (order by game.${orderBy}) row_num`) + .select(`game.id, row_number() over (order by game.${orderBy} ${direction ? direction : ''}) row_num`) .where('game.parentGameId IS NULL'); if (index) { if (!orderBy) { throw new Error('Failed to get game row. "index" is set but "orderBy" is missing.'); } - subQ.andWhere(`(game.${orderBy}, game.id) > (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); + subQ.andWhere(`(game.${orderBy}, game.id) ${direction === 'DESC' ? '<' : '>'} (:orderVal, :id)`, { orderVal: index.orderVal, id: index.id }); } if (filterOpts) { // The "whereCount" param doesn't make much sense now, TODO change it. From d6ab6aa1635f7ea097cec07a2dcbaee0d2a83867 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:57:40 -0400 Subject: [PATCH 68/83] test: GameManager.findGameRow() --- tests/src/back/game/GameManager.test.ts | 312 +++++++++++++++++++++++- 1 file changed, 309 insertions(+), 3 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 677b909bd..f3e22bdf7 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -22,16 +22,37 @@ import { ConnectionOptions, createConnection, getConnection, - getManager + getManager, } from 'typeorm'; import { gameArray } from './exampleDB'; +import * as v8 from 'v8'; + +// Only the keys of T that can't be null or undefined. +type DefinedKeysOf = { + [k in keyof T]-?: null extends T[k] + ? never + : undefined extends T[k] + ? never + : k; +}[keyof T]; + +// This will be a copy of the array that I can feel comfortable mutating. I want to leave gameArray clean. +let arrayCopy: Game[]; const formatLocal = (input: Game): Partial => { - const partial: Partial = input; + const partial = input as Partial; delete partial.placeholder; delete partial.updateTagsStr; return partial; }; +const formatLocalMany = (input: Game[]): Partial[] => { + const partial = input as Partial[]; + partial.forEach((game) => { + delete game.placeholder; + delete game.updateTagsStr; + }); + return partial; +}; const formatDB = (input?: Game): Game | undefined => { if (input) { input.dateAdded = new Date(input.dateAdded).toISOString(); @@ -47,6 +68,33 @@ const formatDBMany = (input?: Game[]): Partial[] | undefined => { } return input; }; +/** + * Filters and then sorts an array of Game objects. + * @param array The array to filter and sort. + * @param filterFunc The function that determines if an element should be left in by the filter. + * @param sortColumn The column to sort the array on. + * @param reverse Whether or not to sort the array backwards. + * @returns The filtered and sorted array. + */ +const filterAndSort = ( + array: Game[], + filterFunc: (game: Game) => boolean, + sortColumn: DefinedKeysOf, + reverse?: boolean +): Game[] => { + const filtered = array.filter(filterFunc); + const flip = reverse ? -1 : 1; + filtered.sort((a: Game, b: Game) => { + if (a[sortColumn] > b[sortColumn]) { + return flip * 1; + } + if (a[sortColumn] < b[sortColumn]) { + return flip * -1; + } + return 0; + }); + return filtered; +}; beforeAll(async () => { const options: ConnectionOptions = { @@ -161,7 +209,265 @@ describe('GameManager.countGames()', () => { expect(await GameManager.countGames()).toBe(count); }); test('Count zero games', async () => { - getManager().getRepository(Game).clear(); + await getManager().getRepository(Game).clear(); expect(await GameManager.countGames()).toBe(0); }); }); + +describe('GameManager.findGameRow()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); + }); + test('Valid game ID, orderBy title', async () => { + // People on the internet say that this will be suboptimal. I don't care too much. + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + expect(await GameManager.findGameRow(gameArray[3].id, 'title', 'ASC')).toBe( + 1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id) + ); + }); + test('Invalid game ID, orderBy title', async () => { + expect(await GameManager.findGameRow(uuid(), 'title', 'ASC')).toBe(-1); + }); + test('Reasonable game filter, orderBy title', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => + game.originalDescription.includes('t') && game.parentGameId == null, + 'title' + ); + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: 'originalDescription', + value: 't', + }, + ], + }, + }) + ) + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Exclusive game filter, orderBy title', async () => { + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: 'originalDescription', + // Again, just a random string generator, essentially. + value: uuid(), + }, + ], + }, + }) + ).toBe(-1); + }); + test('Valid game ID, orderBy developer', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'developer' + ); + //console.log(JSON.stringify(arrayCopy)); + expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Invalid game filter', async () => { + // Invalid game filters should be ignored. + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => + game.originalDescription.includes('t') && game.parentGameId == null, + 'title' + ); + expect( + await GameManager.findGameRow(gameArray[0].id, 'title', 'ASC', { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [], + whitelist: [ + { + field: uuid(), + value: 't', + }, + { + field: 'originalDescription', + value: 't', + }, + ], + }, + }) + ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + }); + test('Valid game ID, orderBy title reverse', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + expect( + await GameManager.findGameRow(gameArray[3].id, 'title', 'DESC') + ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id)); + }); + test('Child game ID, orderBy title', async () => { + expect(await GameManager.findGameRow(gameArray[1].id, 'title', 'ASC')).toBe( + -1 + ); + }); + test('Valid game ID, orderBy title, with index before', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[5].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'ASC', + undefined, + { + orderVal: gameArray[5].title, + title: gameArray[5].title, + id: gameArray[5].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title, with index after', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title' + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[4].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'ASC', + undefined, + { + orderVal: gameArray[4].title, + title: gameArray[4].title, + id: gameArray[4].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title reverse, with index before', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[4].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'DESC', + undefined, + { + orderVal: gameArray[4].title, + title: gameArray[4].title, + id: gameArray[4].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); + test('Valid game ID, orderBy title reverse, with index after', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId == null, + 'title', + true + ); + const indexPos = filtered.findIndex( + (game: Game) => game.id == gameArray[5].id + ); + const resultPos = filtered.findIndex( + (game: Game) => game.id == gameArray[0].id + ); + const diff = resultPos - indexPos; + expect( + await GameManager.findGameRow( + gameArray[0].id, + 'title', + 'DESC', + undefined, + { + orderVal: gameArray[5].title, + title: gameArray[5].title, + id: gameArray[5].id, + } + ) + ).toBe(diff > 0 ? diff : -1); + }); +}); From ee5878e86e3de462e174e8a6c8404e11b1d05fd8 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Mon, 18 Apr 2022 22:07:14 -0400 Subject: [PATCH 69/83] style: make the linter happy --- tests/src/back/game/GameManager.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index f3e22bdf7..915a34129 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -32,8 +32,8 @@ type DefinedKeysOf = { [k in keyof T]-?: null extends T[k] ? never : undefined extends T[k] - ? never - : k; + ? never + : k; }[keyof T]; // This will be a copy of the array that I can feel comfortable mutating. I want to leave gameArray clean. @@ -263,8 +263,8 @@ describe('GameManager.findGameRow()', () => { }, }) ) - // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); }); test('Exclusive game filter, orderBy title', async () => { expect( @@ -293,10 +293,9 @@ describe('GameManager.findGameRow()', () => { (game: Game) => game.parentGameId == null, 'developer' ); - //console.log(JSON.stringify(arrayCopy)); expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) - // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + // Add one because row_number() is one-based, and JS arrays are zero-based. + .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); }); test('Invalid game filter', async () => { // Invalid game filters should be ignored. From 0bd6f4b45280bccdc7ab025df5626f84798efe1a Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:48:04 -0400 Subject: [PATCH 70/83] test: allow connection to be managed outside Don't open a new database connection if one already exists, and don't close the connection at the end of testing. --- tests/src/back/game/GameManager.test.ts | 69 ++++++++++++------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 915a34129..825fe1293 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -21,7 +21,7 @@ import { ChildCurations1648251821422 } from '@database/migration/1648251821422-C import { ConnectionOptions, createConnection, - getConnection, + getConnectionManager, getManager, } from 'typeorm'; import { gameArray } from './exampleDB'; @@ -97,40 +97,38 @@ const filterAndSort = ( }; beforeAll(async () => { - const options: ConnectionOptions = { - type: 'sqlite', - database: ':memory:', - entities: [ - Game, - Playlist, - PlaylistGame, - Tag, - TagAlias, - TagCategory, - GameData, - Source, - SourceData, - ], - migrations: [ - Initial1593172736527, - AddExtremeToPlaylist1599706152407, - GameData1611753257950, - SourceDataUrlPath1612434225789, - SourceFileURL1612435692266, - SourceFileCount1612436426353, - GameTagsStr1613571078561, - GameDataParams1619885915109, - ChildCurations1648251821422, - ], - }; - const connection = await createConnection(options); - // TypeORM forces on but breaks Playlist Game links to unimported games - await connection.query('PRAGMA foreign_keys=off;'); - await connection.runMigrations(); -}); - -afterAll(async () => { - await getConnection().close(); + if (!getConnectionManager().has('default')) { + const options: ConnectionOptions = { + type: 'sqlite', + database: ':memory:', + entities: [ + Game, + Playlist, + PlaylistGame, + Tag, + TagAlias, + TagCategory, + GameData, + Source, + SourceData, + ], + migrations: [ + Initial1593172736527, + AddExtremeToPlaylist1599706152407, + GameData1611753257950, + SourceDataUrlPath1612434225789, + SourceFileURL1612435692266, + SourceFileCount1612436426353, + GameTagsStr1613571078561, + GameDataParams1619885915109, + ChildCurations1648251821422, + ], + }; + const connection = await createConnection(options); + // TypeORM forces on but breaks Playlist Game links to unimported games + await connection.query('PRAGMA foreign_keys=off;'); + await connection.runMigrations(); + } }); /* ASSUMPTIONS MADE: @@ -470,3 +468,4 @@ describe('GameManager.findGameRow()', () => { ).toBe(diff > 0 ? diff : -1); }); }); + From 1d05994fd599adc2da2a08b7bbe6a18d75ff7dd1 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:05:46 -0400 Subject: [PATCH 71/83] fix: sorting for findGamePageKeyset() Also: add some jsdoc and a possible todo, and remove an unneeded selected colum. --- src/back/game/GameManager.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 9b97f0931..3ab0f9a11 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -136,7 +136,16 @@ export type GetPageKeysetResult = { total: number; } -export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: GameOrderBy, direction: GameOrderReverse, searchLimit?: number): Promise { +/** + * Gets the elements that occur before each page, and the total number of games that satisfy the filter. + * @param filterOpts The options that should be used to filter the results. + * @param orderBy The column to sort results by. + * @param direction The direction to sort results. + * @param searchLimit A limit on the number of returned results + * @param viewPageSize The size of a page. Mostly used for testing. + * @returns The elements that occur before each page, and the total number of games that satisfy the filter. + */ +export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: GameOrderBy, direction: GameOrderReverse, searchLimit?: number, viewPageSize = VIEW_PAGE_SIZE): Promise { // let startTime = Date.now(); validateSqlName(orderBy); @@ -145,11 +154,11 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // console.log('FindGamePageKeyset:'); const subQ = await getGameQuery('sub', filterOpts, orderBy, direction); - subQ.select(`sub.${orderBy}, sub.title, sub.id, sub.parentGameId, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id) % ${VIEW_PAGE_SIZE} when 0 then 1 else 0 end page_boundary`); - subQ.orderBy(`sub.${orderBy} ${direction}, sub.title`, direction); + subQ.select(`sub.${orderBy}, sub.title, sub.id, case row_number() over(order by sub.${orderBy} ${direction}, sub.title ${direction}, sub.id ${direction}) % ${viewPageSize} when 0 then 1 else 0 end page_boundary`); + subQ.orderBy(`sub.${orderBy} ${direction}, sub.title ${direction}, sub.id`, direction); let query = getManager().createQueryBuilder() - .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}) + 1 page_number`) + .select(`g.${orderBy}, g.title, g.id, row_number() over(order by g.${orderBy} ${direction}, g.title ${direction}, g.id ${direction}) + 1 page_number`) .from('(' + subQ.getQuery() + ')', 'g') .where('g.page_boundary = 1') .setParameters(subQ.getParameters()); @@ -169,6 +178,7 @@ export async function findGamePageKeyset(filterOpts: FilterGameOpts, orderBy: Ga // Count games let total = -1; // startTime = Date.now(); + // TODO reuse subQ? const subGameQuery = await getGameQuery('sub', filterOpts, orderBy, direction, 0, searchLimit ? searchLimit : undefined, undefined); query = getManager().createQueryBuilder() .select('COUNT(*)') From 3d629f2bbccfe33e52374befeada3c46567cc612 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:07:19 -0400 Subject: [PATCH 72/83] test: GameManager.findGamePageKeyset() Also switch filtering funcs to === from ==. --- tests/src/back/game/GameManager.test.ts | 203 +++++++++++++++++++++--- 1 file changed, 181 insertions(+), 22 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 825fe1293..68a6a1c24 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -226,11 +226,11 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); expect(await GameManager.findGameRow(gameArray[3].id, 'title', 'ASC')).toBe( - 1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id) + 1 + filtered.findIndex((game: Game) => game.id === gameArray[3].id) ); }); test('Invalid game ID, orderBy title', async () => { @@ -243,7 +243,7 @@ describe('GameManager.findGameRow()', () => { const filtered = filterAndSort( arrayCopy, (game: Game) => - game.originalDescription.includes('t') && game.parentGameId == null, + game.originalDescription.includes('t') && game.parentGameId === null, 'title' ); expect( @@ -262,7 +262,7 @@ describe('GameManager.findGameRow()', () => { }) ) // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + .toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Exclusive game filter, orderBy title', async () => { expect( @@ -288,12 +288,12 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'developer' ); expect(await GameManager.findGameRow(gameArray[0].id, 'developer', 'ASC')) // Add one because row_number() is one-based, and JS arrays are zero-based. - .toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + .toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Invalid game filter', async () => { // Invalid game filters should be ignored. @@ -303,7 +303,7 @@ describe('GameManager.findGameRow()', () => { const filtered = filterAndSort( arrayCopy, (game: Game) => - game.originalDescription.includes('t') && game.parentGameId == null, + game.originalDescription.includes('t') && game.parentGameId === null, 'title' ); expect( @@ -324,7 +324,7 @@ describe('GameManager.findGameRow()', () => { ], }, }) - ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[0].id)); + ).toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[0].id)); }); test('Valid game ID, orderBy title reverse', async () => { arrayCopy = v8.deserialize( @@ -332,13 +332,13 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); expect( await GameManager.findGameRow(gameArray[3].id, 'title', 'DESC') - ).toBe(1 + filtered.findIndex((game: Game) => game.id == gameArray[3].id)); + ).toBe(1 + filtered.findIndex((game: Game) => game.id === gameArray[3].id)); }); test('Child game ID, orderBy title', async () => { expect(await GameManager.findGameRow(gameArray[1].id, 'title', 'ASC')).toBe( @@ -351,14 +351,14 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[5].id + (game: Game) => game.id === gameArray[5].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -381,14 +381,14 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title' ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[4].id + (game: Game) => game.id === gameArray[4].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -411,15 +411,15 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[4].id + (game: Game) => game.id === gameArray[4].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -442,15 +442,15 @@ describe('GameManager.findGameRow()', () => { ) as Game[]; const filtered = filterAndSort( arrayCopy, - (game: Game) => game.parentGameId == null, + (game: Game) => game.parentGameId === null, 'title', true ); const indexPos = filtered.findIndex( - (game: Game) => game.id == gameArray[5].id + (game: Game) => game.id === gameArray[5].id ); const resultPos = filtered.findIndex( - (game: Game) => game.id == gameArray[0].id + (game: Game) => game.id === gameArray[0].id ); const diff = resultPos - indexPos; expect( @@ -469,3 +469,162 @@ describe('GameManager.findGameRow()', () => { }); }); +describe('GameManager.findGamePageKeyset()', () => { + beforeAll(async () => { + await getManager().getRepository(Game).save(gameArray); + }); + afterAll(async () => { + await getManager().getRepository(Game).clear(); + }); + test('No filters, orderby title, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby title reverse, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title', + true + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'DESC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby developer, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'developer' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'developer', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('Filter out UUID, orderby title, pagesize 1', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null && game.id !== gameArray[0].id, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + { + searchQuery: { + genericBlacklist: [], + genericWhitelist: [], + blacklist: [ + { + field: 'id', + value: gameArray[0].id, + }, + ], + whitelist: [], + }, + }, + 'title', + 'ASC', + undefined, + 1 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); + test('No filters, orderby title, pagesize 1, limit 3', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[Number(key) - 2].id, + filtered[Number(key) - 2].title, + ]); + } + expect(result.total).toBe(3); + }); + test('No filters, orderby title, pagesize 2', async () => { + arrayCopy = v8.deserialize( + v8.serialize(formatLocalMany(gameArray)) + ) as Game[]; + const filtered = filterAndSort( + arrayCopy, + (game: Game) => game.parentGameId === null, + 'title' + ); + let result = await GameManager.findGamePageKeyset( + {}, + 'title', + 'ASC', + undefined, + 2 + ); + for (const key in result.keyset) { + expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ + filtered[2 * (Number(key) - 2) + 1].id, + filtered[2 * (Number(key) - 2) + 1].title, + ]); + } + expect(result.total).toBe(filtered.length); + }); +}); From 87efa4a136aa8588bb10003c9d145433828b9fb1 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:32:53 -0400 Subject: [PATCH 73/83] feat: restore DUPLICATE_GAME. Also clean up some TODOs. --- src/back/responses.ts | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index cd091e23b..328f8cb18 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -361,8 +361,7 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO check that this was the right move. - /* state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { + state.socketServer.register(BackIn.DUPLICATE_GAME, async (event, id, dupeImages) => { const game = await GameManager.findGame(id); let result: Game | undefined; if (game) { @@ -370,14 +369,15 @@ export function registerRequestCallbacks(state: BackState): void { // Copy and apply new IDs const newGame = deepCopy(game); - /* Ardil TODO figure this out. - const newAddApps = game.addApps.map(addApp => deepCopy(addApp)); + const newChildren = game.children?.map(addApp => deepCopy(addApp)); newGame.id = uuid(); - for (let j = 0; j < newAddApps.length; j++) { - newAddApps[j].id = uuid(); - newAddApps[j].parentGame = newGame; + if (newChildren) { + for (let j = 0; j < newChildren.length; j++) { + newChildren[j].id = uuid(); + newChildren[j].parentGameId = newGame.id; + } } - newGame.addApps = newAddApps; + newGame.children = newChildren; // Add copies result = await GameManager.save(newGame); @@ -414,7 +414,7 @@ export function registerRequestCallbacks(state: BackState): void { library: result && result.library, gamesTotal: await GameManager.countGames(), }; - });*/ + }); state.socketServer.register(BackIn.DUPLICATE_PLAYLIST, async (event, data) => { const playlist = await GameManager.findPlaylist(data, true); @@ -572,12 +572,10 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_GAME_DATA, async (event, gameId, filePath) => { return GameDataManager.importGameData(gameId, filePath, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); }); - // Ardil TODO state.socketServer.register(BackIn.DOWNLOAD_GAME_DATA, async (event, gameDataId) => { const onProgress = (percent: number) => { // Sent to PLACEHOLDER download dialog on client @@ -596,7 +594,6 @@ export function registerRequestCallbacks(state: BackState): void { }); }); - // Ardil TODO This actually is important, don't ignore! state.socketServer.register(BackIn.UNINSTALL_GAME_DATA, async (event, id) => { const gameData = await GameDataManager.findOne(id); if (gameData && gameData.path && gameData.presentOnDisk) { @@ -623,7 +620,6 @@ export function registerRequestCallbacks(state: BackState): void { } }); - // Ardil TODO should be quick. state.socketServer.register(BackIn.ADD_SOURCE_BY_URL, async (event, url) => { const sourceDir = path.join(state.config.flashpointPath, 'Data/Sources'); await fs.promises.mkdir(sourceDir, { recursive: true }); @@ -632,17 +628,14 @@ export function registerRequestCallbacks(state: BackState): void { }); }); - // Ardil TODO state.socketServer.register(BackIn.DELETE_SOURCE, async (event, id) => { return SourceManager.remove(id); }); - // Ardil TODO state.socketServer.register(BackIn.GET_SOURCES, async (event) => { return SourceManager.find(); }); - // Ardil TODO state.socketServer.register(BackIn.GET_SOURCE_DATA, async (event, hashes) => { return GameDataManager.findSourceDataForHashes(hashes); }); @@ -659,7 +652,6 @@ export function registerRequestCallbacks(state: BackState): void { return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_KEYSET, async (event, library, query) => { query.filter = adjustGameFilter(query.filter); const result = await GameManager.findGamePageKeyset(query.filter, query.orderBy, query.orderReverse, query.searchLimit); @@ -965,7 +957,6 @@ export function registerRequestCallbacks(state: BackState): void { return playlistGame; }); - // Ardil done state.socketServer.register(BackIn.SAVE_LEGACY_PLATFORM, async (event, platform) => { const translatedGames = []; const tagCache: Record = {}; @@ -1041,7 +1032,6 @@ export function registerRequestCallbacks(state: BackState): void { return res; }); - // Ardil TODO state.socketServer.register(BackIn.IMPORT_CURATION, async (event, data) => { let error: any | undefined; try { @@ -1115,7 +1105,7 @@ export function registerRequestCallbacks(state: BackState): void { log.error('Launcher', e + ''); } }); - // Ardil TODO + state.socketServer.register(BackIn.LAUNCH_CURATION, async (event, data) => { const skipLink = (data.key === state.lastLinkedCurationKey); state.lastLinkedCurationKey = data.symlinkCurationContent ? data.key : ''; @@ -1188,7 +1178,6 @@ export function registerRequestCallbacks(state: BackState): void { exit(state); }); - // Ardil TODO state.socketServer.register(BackIn.EXPORT_META_EDIT, async (event, id, properties) => { const game = await GameManager.findGame(id, undefined, true); if (game) { @@ -1251,7 +1240,6 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); - // Ardil TODO what is this? state.socketServer.register(BackIn.RUN_COMMAND, async (event, command, args = []) => { // Find command const c = state.registry.commands.get(command); From 46fa961a05fafbd391e754edff597e49b589a86a Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:57:31 -0400 Subject: [PATCH 74/83] make the linter happy --- tests/src/back/game/GameManager.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/src/back/game/GameManager.test.ts index 68a6a1c24..b5ea4b210 100644 --- a/tests/src/back/game/GameManager.test.ts +++ b/tests/src/back/game/GameManager.test.ts @@ -485,7 +485,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'ASC', @@ -510,7 +510,7 @@ describe('GameManager.findGamePageKeyset()', () => { 'title', true ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'DESC', @@ -534,7 +534,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'developer' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'developer', 'ASC', @@ -558,7 +558,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null && game.id !== gameArray[0].id, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( { searchQuery: { genericBlacklist: [], @@ -594,7 +594,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); + const result = await GameManager.findGamePageKeyset({}, 'title', 'ASC', 3, 1); for (const key in result.keyset) { expect([result.keyset[key]?.id, result.keyset[key]?.title]).toEqual([ filtered[Number(key) - 2].id, @@ -612,7 +612,7 @@ describe('GameManager.findGamePageKeyset()', () => { (game: Game) => game.parentGameId === null, 'title' ); - let result = await GameManager.findGamePageKeyset( + const result = await GameManager.findGamePageKeyset( {}, 'title', 'ASC', From 9af7406a68aa5f4e07866d43eb3048c47f07f821 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 09:28:15 -0400 Subject: [PATCH 75/83] fix: implement child editing, fix default date. Implement the child edit function and turn on cascades for children. Set the default child modified and added dates to the unix epoch. Previously, the all-zeroes date was resulting in a null datetime object. --- src/database/entity/Game.ts | 2 +- .../migration/1648251821422-ChildCurations.ts | 3 ++- .../components/RightBrowseSidebar.tsx | 16 ++++++++++++- .../components/RightBrowseSidebarAddApp.tsx | 23 +++++-------------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/database/entity/Game.ts b/src/database/entity/Game.ts index 0d8b984d0..d9118e76a 100644 --- a/src/database/entity/Game.ts +++ b/src/database/entity/Game.ts @@ -23,7 +23,7 @@ export class Game { parentGameId: string | null; // Careful: potential infinite loop here. DO NOT eager-load this. - @OneToMany((type) => Game, (game) => game.parentGame) + @OneToMany((type) => Game, (game) => game.parentGame, { cascade: true }) children?: Game[]; @Column({collation: 'NOCASE'}) diff --git a/src/database/migration/1648251821422-ChildCurations.ts b/src/database/migration/1648251821422-ChildCurations.ts index 865d02d92..146fac36a 100644 --- a/src/database/migration/1648251821422-ChildCurations.ts +++ b/src/database/migration/1648251821422-ChildCurations.ts @@ -9,7 +9,8 @@ export class ChildCurations1648251821422 implements MigrationInterface { await queryRunner.query(`UPDATE game SET message = a.launchCommand FROM additional_app a WHERE game.id=a.parentGameId AND a.applicationPath=':message:'`); await queryRunner.query(`UPDATE game SET extras = a.launchCommand, extrasName = a.name FROM additional_app a WHERE game.id = a.parentGameId AND a.applicationPath = ':extras:'`, undefined); await queryRunner.query(`UPDATE game SET parentGameId = NULL WHERE id IS parentGameId`, undefined); - await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"0000-00-00 00:00:00.000" AS dateAdded,"0000-00-00 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); + // Default value for dateAdded and dateModified values is the unix epoch. Later UI will have to realize this. + await queryRunner.query(`INSERT INTO game SELECT a.id,a.parentGameId,a.name AS title,"" AS alternateTitles,"" AS series,"" AS developer,"" AS publisher,"1970-01-01 00:00:00.000" AS dateAdded,"1970-01-01 00:00:00.000" AS dateModified,"" AS platform,false AS broken,g.extreme AS extreme,"" AS playMode,"" AS status,"" AS notes,"" AS source,a.applicationPath,a.launchCommand,"" AS releaseDate,"" AS version,"" AS originalDescription,"" AS language,library,LOWER(a.name) AS orderTitle,NULL AS activeDataId,false AS activeDataOnDisk,"" AS tagsStr,NULL as extras,NULL AS extrasName,NULL AS message FROM additional_app a INNER JOIN game g ON a.parentGameId = g.id WHERE a.applicationPath != ':message:' AND a.applicationPath != ':extras:'`, undefined); await queryRunner.query(`DROP TABLE additional_app`, undefined); } diff --git a/src/renderer/components/RightBrowseSidebar.tsx b/src/renderer/components/RightBrowseSidebar.tsx index de3d297ac..2994d4f66 100644 --- a/src/renderer/components/RightBrowseSidebar.tsx +++ b/src/renderer/components/RightBrowseSidebar.tsx @@ -635,6 +635,7 @@ export class RightBrowseSidebar extends React.Component { addApp && this.props.onGameLaunch(addApp.id) .then(this.onForceUpdateGameData); @@ -974,10 +975,23 @@ export class RightBrowseSidebar extends React.Component) => { + if (this.props.currentGame && this.props.currentGame.children) { + const newChildren = [...this.props.currentGame.children]; + const childIndex = this.props.currentGame.children.findIndex(child => child.id === childId); + if (childIndex !== -1) { + newChildren[childIndex] = {...newChildren[childIndex], ...diff} as Game; + this.props.onEditGame({children: newChildren}); + } else { + throw new Error('Can\'t edit additional application because it was not found.'); + } + } + }; + onScreenshotClick = (): void => { this.setState({ showPreview: true }); } diff --git a/src/renderer/components/RightBrowseSidebarAddApp.tsx b/src/renderer/components/RightBrowseSidebarAddApp.tsx index 9062d0b06..b0ba87d73 100644 --- a/src/renderer/components/RightBrowseSidebarAddApp.tsx +++ b/src/renderer/components/RightBrowseSidebarAddApp.tsx @@ -10,7 +10,7 @@ export type RightBrowseSidebarChildProps = { /** Additional Application to show and edit */ child: Game; /** Called when a field is edited */ - onEdit?: () => void; + onEdit?: (childId: string, diff: Partial) => void; /** Called when a field is edited */ onDelete?: (childId: string) => void; /** Called when the launch button is clicked */ @@ -25,9 +25,9 @@ export interface RightBrowseSidebarChild { /** Displays an additional application for a game in the right sidebar of BrowsePage. */ export class RightBrowseSidebarChild extends React.Component { - onNameEditDone = this.wrapOnTextChange((addApp, text) => { addApp.title = text; }); - onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { addApp.applicationPath = text; }); - onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { addApp.launchCommand = text; }); + onNameEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({title: text}); }); + onApplicationPathEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({applicationPath: text}); }); + onLaunchCommandEditDone = this.wrapOnTextChange((addApp, text) => { this.onEdit({launchCommand: text}); }); render() { const allStrings = this.context; @@ -109,9 +109,9 @@ export class RightBrowseSidebarChild extends React.Component): void { if (this.props.onEdit) { - this.props.onEdit(); + this.props.onEdit(this.props.child.id, diff); } } @@ -126,16 +126,5 @@ export class RightBrowseSidebarChild extends React.Component void) { - return () => { - if (!this.props.editDisabled) { - func(this.props.child); - this.onEdit(); - this.forceUpdate(); - } - }; - } - static contextType = LangContext; } From 5aec01ec37c15cbf1c768fbe2dba9cc3f8db3f01 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 10:47:27 -0400 Subject: [PATCH 76/83] fix: check that extras exist before importing Also remove some todos. --- src/back/importGame.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/back/importGame.ts b/src/back/importGame.ts index de050cdea..432ab23b7 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -51,7 +51,6 @@ export const onWillImportCuration: ApiEmitter = new ApiEmit * Import a curation. * @returns A promise that resolves when the import is complete. */ -// Ardil TODO export async function importCuration(opts: ImportCurationOpts): Promise { if (opts.date === undefined) { opts.date = new Date(); } const { @@ -109,8 +108,29 @@ export async function importCuration(opts: ImportCurationOpts): Promise { // Build content list const contentToMove = []; if (curation.meta.extras && curation.meta.extras.length > 0) { - // Add extras folder if meta has an entry - contentToMove.push([path.join(getCurationFolder(curation, fpPath), 'Extras'), path.join(fpPath, 'Extras', curation.meta.extras)]); + const extrasPath = path.join(getCurationFolder(curation, fpPath), 'Extras'); + // Check that extras exist. + try { + // If this doesn't error out, the extras exist. + await fs.promises.stat(extrasPath); + // Add extras folder if meta has an entry + contentToMove.push([extrasPath, path.join(fpPath, 'Extras', curation.meta.extras)]); + } catch { + // It did error out, we need to tell the user. + const response = await opts.openDialog({ + title: 'Overwriting Game', + message: 'The curation claims to have extras but lacks an Extras folder!\nContinue importing this curation? Warning: this will remove the extras.\n\n' + + `Curation:\n\tTitle: ${curation.meta.title}\n\tLaunch Command: ${curation.meta.launchCommand}\n\tPlatform: ${curation.meta.platform}\n\t` + + `Expected extras path: ${extrasPath}`, + buttons: ['Yes', 'No'] + }); + if (response === 1) { + throw new Error('User Cancelled Import'); + } + curation.meta.extras = undefined; + curation.meta.extrasName = undefined; + } + } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); @@ -300,7 +320,6 @@ function logMessage(text: string, curation: EditCuration): void { * @param curation Curation to get data from. * @param gameId ID to use for Game */ -// Ardil TODO async function createGameFromCurationMeta(gameId: string, gameMeta: EditCurationMeta, date: Date): Promise { const game: Game = new Game(); Object.assign(game, { From deb8a5a4919a08ad3bad0ecdde860e127861e56d Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:16:04 -0400 Subject: [PATCH 77/83] style: make the linter happy --- src/back/importGame.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 432ab23b7..aaf7d0038 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -130,7 +130,6 @@ export async function importCuration(opts: ImportCurationOpts): Promise { curation.meta.extras = undefined; curation.meta.extrasName = undefined; } - } // Create and add game and additional applications const gameId = validateSemiUUID(curation.key) ? curation.key : uuid(); From 9d73b79fad4678239f9fd4d41f20c418d70a1577 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:58:39 -0400 Subject: [PATCH 78/83] style: remove a bunch of irrelevant todos --- src/back/extensions/ApiImplementation.ts | 1 - src/back/game/GameManager.ts | 3 +-- src/back/importGame.ts | 3 +-- src/back/responses.ts | 15 +++------------ src/renderer/components/CurateBox.tsx | 1 - 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index de6036501..196348af9 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -142,7 +142,6 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, findGamesWithTag: GameManager.findGamesWithTag, updateGame: GameManager.save, updateGames: GameManager.updateGames, - // Ardil TODO removeGameAndChildren: (gameId: string) => GameManager.removeGameAndChildren(gameId, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)), isGameExtreme: (game: Game) => { const extremeTags = state.preferences.tagFilters.filter(t => t.extreme).reduce((prev, cur) => prev.concat(cur.tags), []); diff --git a/src/back/game/GameManager.ts b/src/back/game/GameManager.ts index 3ab0f9a11..f279415c2 100644 --- a/src/back/game/GameManager.ts +++ b/src/back/game/GameManager.ts @@ -345,7 +345,7 @@ export async function save(game: Game): Promise { return savedGame; } -// Ardil TODO fix this. +// TODO this needs to re-parent somehow? Idk, wherever you want to put it. export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: string): Promise { const gameRepository = getManager().getRepository(Game); const game = await findGame(gameId); @@ -358,7 +358,6 @@ export async function removeGameAndChildren(gameId: string, dataPacksFolderPath: await GameDataManager.remove(gameData.id); } // Delete children - // Ardil TODO do Seirade's suggestion. if (game.children) { for (const child of game.children) { await gameRepository.remove(child); diff --git a/src/back/importGame.ts b/src/back/importGame.ts index aaf7d0038..a1eaffbd3 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -284,7 +284,6 @@ export async function importCuration(opts: ImportCurationOpts): Promise { * Create and launch a game from curation metadata. * @param curation Curation to launch */ -// Ardil TODO export async function launchCuration(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit, onWillEvent:ApiEmitter, onDidEvent: ApiEmitter) { if (!skipLink || !symlinkCurationContent) { await linkContentFolder(key, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } @@ -298,7 +297,7 @@ export async function launchCuration(key: string, meta: EditCurationMeta, symlin onDidEvent.fire(game); } -// Ardil TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. +// TODO this won't work, fix it. Actually, it's okay for now: the related back event *should* never be called. export async function launchCurationExtras(key: string, meta: EditCurationMeta, symlinkCurationContent: boolean, skipLink: boolean, opts: Omit) { if (meta.extras) { diff --git a/src/back/responses.ts b/src/back/responses.ts index 328f8cb18..a4705f225 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -166,7 +166,6 @@ export function registerRequestCallbacks(state: BackState): void { return { done }; }); - // Ardil TODO state.socketServer.register(BackIn.GET_SUGGESTIONS, async (event) => { const startTime = Date.now(); const suggestions: GamePropSuggestions = { @@ -238,7 +237,7 @@ export function registerRequestCallbacks(state: BackState): void { if (game.platform === '') { game.platform = game.parentGame.platform; } - // Ardil TODO any more I should add? + // TODO any more I should add? } // Make sure Server is set to configured server - Curations may have changed it const configServer = state.serviceInfo ? state.serviceInfo.server.find(s => s.name === state.config.server) : undefined; @@ -349,7 +348,8 @@ export function registerRequestCallbacks(state: BackState): void { }); state.socketServer.register(BackIn.DELETE_GAME, async (event, id) => { - // Ardil TODO figure out this thing. + // TODO This needs to somehow re-parent instead of just deleting all the children. It will have to wait + // until the frontend changes are made, I guess. const game = await GameManager.removeGameAndChildren(id, path.join(state.config.flashpointPath, state.preferences.dataPacksFolderPath)); state.queries = {}; // Clear entire cache @@ -640,13 +640,11 @@ export function registerRequestCallbacks(state: BackState): void { return GameDataManager.findSourceDataForHashes(hashes); }); - // Ardil TODO state.socketServer.register(BackIn.GET_ALL_GAMES, async (event) => { const games: Game[] = await GameManager.findAllGames(); return games; }); - // Ardil TODO state.socketServer.register(BackIn.RANDOM_GAMES, async (event, data) => { const flatFilters = data.tagFilters ? data.tagFilters.reduce((prev, cur) => prev.concat(cur.tags), []) : []; return await GameManager.findRandomGames(data.count, data.broken, data.excludedLibraries, flatFilters); @@ -661,7 +659,6 @@ export function registerRequestCallbacks(state: BackState): void { }; }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_PAGE, async (event, data) => { data.query.filter = adjustGameFilter(data.query.filter); const startTime = new Date(); @@ -790,7 +787,6 @@ export function registerRequestCallbacks(state: BackState): void { return result; }); - // Ardil TODO state.socketServer.register(BackIn.BROWSE_VIEW_INDEX, async (event, gameId, query) => { const position = await GameManager.findGameRow( gameId, @@ -848,7 +844,6 @@ export function registerRequestCallbacks(state: BackState): void { catch (error: any) { log.error('Launcher', error); } }); - // Ardil TODO add pref to make add-apps searchable? Later? state.socketServer.register(BackIn.UPDATE_PREFERENCES, async (event, data, refresh) => { const dif = difObjects(defaultPreferencesData, state.preferences, data); if (dif) { @@ -1350,7 +1345,6 @@ function adjustGameFilter(filterOpts: FilterGameOpts): FilterGameOpts { return filterOpts; } -// Ardil TODO /** * Creates a function that will run any game launch info given to it and register it as a service */ @@ -1388,7 +1382,6 @@ function runGameFactory(state: BackState) { }; } -// Ardil TODO function createCommand(filename: string, useWine: boolean, execFile: boolean): string { // This whole escaping thing is horribly broken. We probably want to switch // to an array representing the argv instead and not have a shell @@ -1412,7 +1405,6 @@ function createCommand(filename: string, useWine: boolean, execFile: boolean): s * @param command Command to run * @param args Arguments for the command */ -// Ardil TODO what is this? async function runCommand(state: BackState, command: string, args: any[] = []): Promise { const callback = state.registry.commands.get(command); let res = undefined; @@ -1432,7 +1424,6 @@ async function runCommand(state: BackState, command: string, args: any[] = []): /** * Returns a set of AppProviders from all extension registered Applications, complete with callbacks to run them. */ -// Ardil TODO async function getProviders(state: BackState): Promise { return state.extensionsService.getContributions('applications') .then(contributions => { diff --git a/src/renderer/components/CurateBox.tsx b/src/renderer/components/CurateBox.tsx index 570a86aa6..9d47080fc 100644 --- a/src/renderer/components/CurateBox.tsx +++ b/src/renderer/components/CurateBox.tsx @@ -480,7 +480,6 @@ export function CurateBox(props: CurateBoxProps) { const disabled = props.curation ? props.curation.locked : false; // Whether the platform used by the curation is native locked - // Ardil TODO what is this used for? // eslint-disable-next-line @typescript-eslint/no-unused-vars const native = useMemo(() => { if (props.curation && props.curation.meta.platform) { From f62ec64304423eca9bfd90745ae7b4a1d5e045b1 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 26 May 2022 19:48:45 -0400 Subject: [PATCH 79/83] fix: explictly cast an error to a string, so that it will compile --- src/back/responses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/back/responses.ts b/src/back/responses.ts index a4705f225..8a3979c2e 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -300,7 +300,7 @@ export function registerRequestCallbacks(state: BackState): void { }, 250); }); } catch (error) { - state.socketServer.broadcast(BackOut.OPEN_ALERT, error); + state.socketServer.broadcast(BackOut.OPEN_ALERT, error as string); log.info('Game Launcher', `Game Launch Aborted: ${error}`); return; } From 55d8bbc2e7fa41d5f507bc798787ed3905a0800b Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 26 May 2022 20:53:46 -0400 Subject: [PATCH 80/83] Supress the undefined and null errors. --- package-lock.json | 2044 +++++++++++++++++++++-------- src/back/util/misc.ts | 2 +- src/shared/socket/SocketAPI.ts | 2 +- src/shared/socket/SocketServer.ts | 3 +- tsconfig.json | 2 +- tsconfig.renderer.json | 3 +- 6 files changed, 1538 insertions(+), 518 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f33867f2..56bfd0c48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "redux-devtools-extension": "2.13.8", "reflect-metadata": "0.1.10", "remark-gfm": "^2.0.0", - "sqlite3": "4.2.0", + "sqlite3": "^5.0.2", "tail": "2.0.3", "typeorm": "0.2.37", "typesafe-actions": "4.4.2", @@ -49,10 +49,10 @@ "@types/electron-devtools-installer": "2.2.0", "@types/follow-redirects": "1.8.0", "@types/fs-extra": "8.1.0", - "@types/jest": "^27.5.1", + "@types/jest": "27.5.1", "@types/mime": "2.0.1", "@types/node": "14.14.31", - "@types/ps-tree": "^1.1.2", + "@types/ps-tree": "1.1.2", "@types/react": "16.9.34", "@types/react-color": "3.0.1", "@types/react-dom": "16.9.7", @@ -75,8 +75,8 @@ "eslint-plugin-only-warn": "1.0.2", "eslint-plugin-react": "7.20.0", "gulp": "4.0.2", - "jest": "^28.1.0", - "ts-jest": "^28.0.3", + "jest": "28.1.0", + "ts-jest": "28.0.3", "ts-loader": "9.3.0", "ts-transform-paths": "2.0.1", "tsconfig-paths-webpack-plugin": "3.2.0", @@ -779,6 +779,12 @@ "react": ">=16.x" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, "node_modules/@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -1108,21 +1114,6 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, - "node_modules/@jest/core/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jest/core/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1870,6 +1861,60 @@ "node": ">= 10.0.0" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", + "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@sinclair/typebox": { "version": "0.23.5", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", @@ -2719,7 +2764,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "dependencies": { "debug": "4" }, @@ -2727,6 +2771,33 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3061,9 +3132,9 @@ } }, "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "node_modules/archy": { "version": "1.0.0", @@ -3072,12 +3143,55 @@ "dev": true }, "node_modules/are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "dependencies": { "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/are-we-there-yet/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" } }, "node_modules/arg": { @@ -4331,6 +4445,47 @@ "node": ">=0.10.0" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -4560,9 +4715,12 @@ } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -4618,6 +4776,15 @@ "node": ">=0.10.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -5133,7 +5300,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, "bin": { "color-support": "bin.js" } @@ -5525,9 +5691,9 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -5584,6 +5750,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, "engines": { "node": ">=4.0.0" } @@ -5715,6 +5882,15 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -5725,14 +5901,11 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -6188,21 +6361,6 @@ "unzip-crx-3": "^0.2.0" } }, - "node_modules/electron-devtools-installer/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/electron-is-dev": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz", @@ -6464,6 +6622,27 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6506,6 +6685,12 @@ "node": ">=4" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, "node_modules/errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -7961,11 +8146,14 @@ } }, "node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dependencies": { - "minipass": "^2.6.0" + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/fs-mkdirp-stream": { @@ -8013,18 +8201,62 @@ "dev": true }, "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "dependencies": { - "aproba": "^1.0.3", + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/gensync": { @@ -8733,7 +8965,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -8751,6 +8982,15 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -8772,6 +9012,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -8807,14 +9048,6 @@ "node": ">= 4" } }, - "node_modules/ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dependencies": { - "minimatch": "^3.0.4" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -8866,11 +9099,26 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8888,7 +9136,8 @@ "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true }, "node_modules/inline-style-parser": { "version": "0.1.1", @@ -9066,6 +9315,12 @@ "node": ">=0.10.0" } }, + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "optional": true + }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -9328,6 +9583,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "optional": true + }, "node_modules/is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -12468,11 +12729,6 @@ "node": ">=10" } }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -12483,7 +12739,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -12498,7 +12753,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -12509,6 +12763,56 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -13440,6 +13744,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13453,39 +13758,91 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/minipass/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dependencies": { - "minipass": "^2.9.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/mixin-deep": { @@ -13557,7 +13914,9 @@ "node_modules/nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true }, "node_modules/nanomatch": { "version": "1.2.13", @@ -13587,36 +13946,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", - "dependencies": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true, "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dependencies": { - "ms": "^2.1.1" + "node": ">= 0.6" } }, - "node_modules/needle/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -13661,41 +13999,223 @@ "dev": true, "optional": true }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } }, - "node_modules/node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "dependencies": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" }, "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" } }, - "node_modules/node-pre-gyp/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "node_modules/node-gyp/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", + "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/node-gyp/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/node-gyp/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/node-gyp/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, "bin": { - "semver": "bin/semver" + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", @@ -13703,15 +14223,17 @@ "dev": true }, "node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/normalize-package-data": { @@ -13769,14 +14291,6 @@ "node": ">= 0.10" } }, - "node_modules/npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dependencies": { - "npm-normalize-package-bin": "^1.0.1" - } - }, "node_modules/npm-conf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", @@ -13790,21 +14304,6 @@ "node": ">=4" } }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "node_modules/npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dependencies": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -13827,14 +14326,14 @@ } }, "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" } }, "node_modules/number-is-nan": { @@ -14097,14 +14596,6 @@ "readable-stream": "^2.0.1" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -14121,19 +14612,11 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "node_modules/p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", @@ -14169,6 +14652,21 @@ "node": ">=8" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -14609,6 +15107,25 @@ "node": ">=0.4.0" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -14746,6 +15263,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -15459,15 +15977,27 @@ "node": ">=0.12" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/roarr": { @@ -15531,7 +16061,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -15832,7 +16363,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "optional": true, "engines": { "node": ">= 6.0.0", @@ -15988,6 +16518,34 @@ "node": ">=0.10.0" } }, + "node_modules/socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "optional": true, + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz", + "integrity": "sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16107,15 +16665,32 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "node_modules/sqlite3": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", - "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.8.tgz", + "integrity": "sha512-f2ACsbSyb2D1qFFcqIXPfFscLtPVOWJr5GmUzYxf4W+0qelu5MWrR+FAQE1d5IUArEltBrzSDxDORG8P/IkqyQ==", "hasInstallScript": true, "dependencies": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.11.0" + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } } }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + }, "node_modules/sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -16141,6 +16716,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -16359,6 +16946,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -16541,40 +17129,31 @@ } }, "node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": ">=4.5" + "node": ">= 10" } }, - "node_modules/tar/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/temp-file": { "version": "3.4.0", @@ -16849,21 +17428,6 @@ "tmp": "^0.2.0" } }, - "node_modules/tmp-promise/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tmp-promise/node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -17873,6 +18437,24 @@ "node": ">=0.10.0" } }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unique-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", @@ -18672,11 +19254,11 @@ } }, "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "dependencies": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "node_modules/widest-line": { @@ -18947,9 +19529,9 @@ "dev": true }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.1.0", @@ -19636,6 +20218,12 @@ "prop-types": "^15.7.2" } }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, "@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -19883,15 +20471,6 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -20455,6 +21034,50 @@ } } }, + "@mapbox/node-pre-gyp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", + "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + } + }, + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + } + } + }, "@sinclair/typebox": { "version": "0.23.5", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.23.5.tgz", @@ -21220,11 +21843,31 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "requires": { "debug": "4" } }, + "agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -21492,9 +22135,9 @@ } }, "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "archy": { "version": "1.0.0", @@ -21503,12 +22146,37 @@ "dev": true }, "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "requires": { "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "arg": { @@ -22456,6 +23124,40 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + } + } + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -22624,9 +23326,9 @@ } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, "chrome-trace-event": { "version": "1.0.3", @@ -22675,6 +23377,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -23072,8 +23780,7 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, "colorette": { "version": "2.0.16", @@ -23367,9 +24074,9 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" }, @@ -23410,7 +24117,8 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true }, "deep-is": { "version": "0.1.3", @@ -23513,6 +24221,12 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -23520,9 +24234,9 @@ "dev": true }, "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" }, "detect-newline": { "version": "3.1.0", @@ -23877,17 +24591,6 @@ "rimraf": "^3.0.2", "semver": "^7.2.1", "unzip-crx-3": "^0.2.0" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } }, "electron-is-dev": { @@ -24096,6 +24799,26 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "optional": true }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -24126,6 +24849,12 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -25272,11 +26001,11 @@ } }, "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "requires": { - "minipass": "^2.6.0" + "minipass": "^3.0.0" } }, "fs-mkdirp-stream": { @@ -25313,18 +26042,49 @@ "dev": true }, "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "requires": { - "aproba": "^1.0.3", + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "gensync": { @@ -25896,7 +26656,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, "requires": { "agent-base": "6", "debug": "4" @@ -25908,6 +26667,15 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, "iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -25923,6 +26691,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -25938,14 +26707,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "requires": { - "minimatch": "^3.0.4" - } - }, "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -25982,7 +26743,19 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "devOptional": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true }, "inflight": { "version": "1.0.6", @@ -26001,7 +26774,8 @@ "ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true }, "inline-style-parser": { "version": "0.1.1", @@ -26136,6 +26910,12 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, + "ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "optional": true + }, "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -26325,6 +27105,12 @@ "is-path-inside": "^3.0.2" } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "optional": true + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -28681,13 +29467,6 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } } }, "lunr": { @@ -28700,7 +29479,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "requires": { "semver": "^6.0.0" }, @@ -28708,8 +29486,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -28719,6 +29496,49 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + } + } + }, "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", @@ -29315,6 +30135,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -29325,27 +30146,68 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "requires": { + "minipass": "^3.0.0" } }, "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "requires": { - "minipass": "^2.9.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" } }, "mixin-deep": { @@ -29407,7 +30269,9 @@ "nan": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -29434,30 +30298,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "optional": true }, "neo-async": { "version": "2.6.2", @@ -29500,36 +30345,166 @@ "dev": true, "optional": true }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" }, "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "are-we-there-yet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", + "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "optional": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } } } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, "node-releases": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", @@ -29537,12 +30512,11 @@ "dev": true }, "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1" } }, "normalize-package-data": { @@ -29590,14 +30564,6 @@ "once": "^1.3.2" } }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-conf": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", @@ -29608,21 +30574,6 @@ "pify": "^3.0.0" } }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -29641,14 +30592,14 @@ } }, "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" } }, "number-is-nan": { @@ -29847,11 +30798,6 @@ "readable-stream": "^2.0.1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -29864,16 +30810,8 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true }, "p-cancelable": { "version": "1.1.0", @@ -29898,6 +30836,15 @@ "p-limit": "^2.2.0" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -30241,6 +31188,22 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "optional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -30358,6 +31321,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -30909,10 +31873,16 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "optional": true + }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" } @@ -30971,7 +31941,8 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true }, "sanitize-filename": { "version": "1.6.3", @@ -31213,7 +32184,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "optional": true }, "snapdragon": { @@ -31338,6 +32308,27 @@ } } }, + "socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "optional": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz", + "integrity": "sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -31439,12 +32430,21 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sqlite3": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz", - "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.8.tgz", + "integrity": "sha512-f2ACsbSyb2D1qFFcqIXPfFscLtPVOWJr5GmUzYxf4W+0qelu5MWrR+FAQE1d5IUArEltBrzSDxDORG8P/IkqyQ==", "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.11.0" + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "node-gyp": "8.x", + "tar": "^6.1.11" + }, + "dependencies": { + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + } } }, "sshpk": { @@ -31464,6 +32464,15 @@ "tweetnacl": "~0.14.0" } }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "requires": { + "minipass": "^3.1.1" + } + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -31639,7 +32648,8 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true }, "style-to-object": { "version": "0.3.0", @@ -31778,23 +32788,22 @@ "dev": true }, "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" } } }, @@ -32014,15 +33023,6 @@ "tmp": "^0.2.0" }, "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -32703,6 +33703,24 @@ "set-value": "^2.0.1" } }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, "unique-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", @@ -33304,11 +34322,11 @@ } }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "widest-line": { @@ -33506,9 +34524,9 @@ "dev": true }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "2.1.0", diff --git a/src/back/util/misc.ts b/src/back/util/misc.ts index 69a572ca2..d5bba4367 100644 --- a/src/back/util/misc.ts +++ b/src/back/util/misc.ts @@ -316,7 +316,7 @@ export function runService(state: BackState, id: string, name: string, basePath: proc.spawn(); } catch (error) { log.error(SERVICES_SOURCE, `An unexpected error occurred while trying to run the background process "${proc.name}".` + - ` ${error.toString()}`); + ` ${(error as Error).toString()}`); } state.apiEmitters.services.onServiceNew.fire(proc); return proc; diff --git a/src/shared/socket/SocketAPI.ts b/src/shared/socket/SocketAPI.ts index e680353e9..50a35a7ea 100644 --- a/src/shared/socket/SocketAPI.ts +++ b/src/shared/socket/SocketAPI.ts @@ -126,7 +126,7 @@ export async function api_handle_message< result = await callback(event, ...data.args as any); } catch (e) { // console.error(`An error was thrown from inside a callback (type: ${data.type}).`, e); - error = e.toString(); + error = (e as Error).toString(); } } diff --git a/src/shared/socket/SocketServer.ts b/src/shared/socket/SocketServer.ts index 11944488b..c4a53a577 100644 --- a/src/shared/socket/SocketServer.ts +++ b/src/shared/socket/SocketServer.ts @@ -88,7 +88,8 @@ export function server_request< if (sent.error !== undefined) { reject(sent.error); } else { - resolve(sent.result); + // Hacky and bad, but makes it compile. + resolve(sent.result as U[T]); } }, }); diff --git a/tsconfig.json b/tsconfig.json index c6d53411b..de20c5cc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictPropertyInitialization": false, - "strictNullChecks": false, + "strictNullChecks": true, "paths": { "@shared/*": [ "./src/shared/*" ], "@main/*": [ "./src/main/*" ], diff --git a/tsconfig.renderer.json b/tsconfig.renderer.json index 99ddab96b..61d9f25ba 100644 --- a/tsconfig.renderer.json +++ b/tsconfig.renderer.json @@ -2,10 +2,11 @@ "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react", + "strictNullChecks": false, }, "exclude": [ "./src/main", "./src/back", "./tests" ] -} \ No newline at end of file +} From 8ccc0f33b4f229f6f0abab4059019643becb8884 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 26 May 2022 21:16:24 -0400 Subject: [PATCH 81/83] Move the GameManager tests to the right place --- tests/{src => unit}/back/game/GameManager.test.ts | 0 tests/{src => unit}/back/game/exampleDB.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{src => unit}/back/game/GameManager.test.ts (100%) rename tests/{src => unit}/back/game/exampleDB.ts (100%) diff --git a/tests/src/back/game/GameManager.test.ts b/tests/unit/back/game/GameManager.test.ts similarity index 100% rename from tests/src/back/game/GameManager.test.ts rename to tests/unit/back/game/GameManager.test.ts diff --git a/tests/src/back/game/exampleDB.ts b/tests/unit/back/game/exampleDB.ts similarity index 100% rename from tests/src/back/game/exampleDB.ts rename to tests/unit/back/game/exampleDB.ts From d4d2e474d42cbd27732acf52a242ebb3fb783053 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 26 May 2022 21:50:19 -0400 Subject: [PATCH 82/83] Make db-creation code shared. --- tests/unit/back/TestDB.ts | 72 +++++++++++++++++ tests/unit/back/game/GameManager.test.ts | 77 +++---------------- .../back/{game/exampleDB.ts => smallDB.ts} | 0 3 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 tests/unit/back/TestDB.ts rename tests/unit/back/{game/exampleDB.ts => smallDB.ts} (100%) diff --git a/tests/unit/back/TestDB.ts b/tests/unit/back/TestDB.ts new file mode 100644 index 000000000..efd5cc6f1 --- /dev/null +++ b/tests/unit/back/TestDB.ts @@ -0,0 +1,72 @@ +import { Game } from '@database/entity/Game'; +import { GameData } from '@database/entity/GameData'; +import { Playlist } from '@database/entity/Playlist'; +import { PlaylistGame } from '@database/entity/PlaylistGame'; +import { Source } from '@database/entity/Source'; +import { SourceData } from '@database/entity/SourceData'; +import { Tag } from '@database/entity/Tag'; +import { TagAlias } from '@database/entity/TagAlias'; +import { TagCategory } from '@database/entity/TagCategory'; +import { Initial1593172736527 } from '@database/migration/1593172736527-Initial'; +import { AddExtremeToPlaylist1599706152407 } from '@database/migration/1599706152407-AddExtremeToPlaylist'; +import { GameData1611753257950 } from '@database/migration/1611753257950-GameData'; +import { SourceDataUrlPath1612434225789 } from '@database/migration/1612434225789-SourceData_UrlPath'; +import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-Source_FileURL'; +import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; +import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; +import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; +import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; +import { + ConnectionOptions, + createConnection, + getConnectionManager, + getManager +} from 'typeorm'; +import { gameArray as gameArray_small } from './smallDB'; + +const entities = [ + Game, + Playlist, + PlaylistGame, + Tag, + TagAlias, + TagCategory, + GameData, + Source, + SourceData, +]; +type entityType = typeof entities[number]; + +export async function createDefaultDB(path = ':memory:') { + if (!getConnectionManager().has('default')) { + const options: ConnectionOptions = { + type: 'sqlite', + database: path, + entities: entities, + migrations: [ + Initial1593172736527, + AddExtremeToPlaylist1599706152407, + GameData1611753257950, + SourceDataUrlPath1612434225789, + SourceFileURL1612435692266, + SourceFileCount1612436426353, + GameTagsStr1613571078561, + GameDataParams1619885915109, + ChildCurations1648251821422, + ], + }; + const connection = await createConnection(options); + // TypeORM forces on but breaks Playlist Game links to unimported games + await connection.query('PRAGMA foreign_keys=off;'); + await connection.runMigrations(); + } +} + +export async function clearDB(entity: entityType) { + await getManager().getRepository(entity).clear(); +} + +// TODO make this do Playlist, etc. instead of just Game. +export async function setSmall_gameOnly() { + await getManager().getRepository(Game).save(gameArray_small); +} \ No newline at end of file diff --git a/tests/unit/back/game/GameManager.test.ts b/tests/unit/back/game/GameManager.test.ts index b5ea4b210..91ac16cc1 100644 --- a/tests/unit/back/game/GameManager.test.ts +++ b/tests/unit/back/game/GameManager.test.ts @@ -1,30 +1,8 @@ import * as GameManager from '@back/game/GameManager'; import { uuid } from '@back/util/uuid'; import { Game } from '@database/entity/Game'; -import { GameData } from '@database/entity/GameData'; -import { Playlist } from '@database/entity/Playlist'; -import { PlaylistGame } from '@database/entity/PlaylistGame'; -import { Source } from '@database/entity/Source'; -import { SourceData } from '@database/entity/SourceData'; -import { Tag } from '@database/entity/Tag'; -import { TagAlias } from '@database/entity/TagAlias'; -import { TagCategory } from '@database/entity/TagCategory'; -import { Initial1593172736527 } from '@database/migration/1593172736527-Initial'; -import { AddExtremeToPlaylist1599706152407 } from '@database/migration/1599706152407-AddExtremeToPlaylist'; -import { GameData1611753257950 } from '@database/migration/1611753257950-GameData'; -import { SourceDataUrlPath1612434225789 } from '@database/migration/1612434225789-SourceData_UrlPath'; -import { SourceFileURL1612435692266 } from '@database/migration/1612435692266-Source_FileURL'; -import { SourceFileCount1612436426353 } from '@database/migration/1612436426353-SourceFileCount'; -import { GameTagsStr1613571078561 } from '@database/migration/1613571078561-GameTagsStr'; -import { GameDataParams1619885915109 } from '@database/migration/1619885915109-GameDataParams'; -import { ChildCurations1648251821422 } from '@database/migration/1648251821422-ChildCurations'; -import { - ConnectionOptions, - createConnection, - getConnectionManager, - getManager, -} from 'typeorm'; -import { gameArray } from './exampleDB'; +import { gameArray } from '../smallDB'; +import { setSmall_gameOnly, clearDB, createDefaultDB } from '../TestDB'; import * as v8 from 'v8'; // Only the keys of T that can't be null or undefined. @@ -97,38 +75,7 @@ const filterAndSort = ( }; beforeAll(async () => { - if (!getConnectionManager().has('default')) { - const options: ConnectionOptions = { - type: 'sqlite', - database: ':memory:', - entities: [ - Game, - Playlist, - PlaylistGame, - Tag, - TagAlias, - TagCategory, - GameData, - Source, - SourceData, - ], - migrations: [ - Initial1593172736527, - AddExtremeToPlaylist1599706152407, - GameData1611753257950, - SourceDataUrlPath1612434225789, - SourceFileURL1612435692266, - SourceFileCount1612436426353, - GameTagsStr1613571078561, - GameDataParams1619885915109, - ChildCurations1648251821422, - ], - }; - const connection = await createConnection(options); - // TypeORM forces on but breaks Playlist Game links to unimported games - await connection.query('PRAGMA foreign_keys=off;'); - await connection.runMigrations(); - } + await createDefaultDB(); }); /* ASSUMPTIONS MADE: @@ -137,10 +84,10 @@ beforeAll(async () => { describe('GameManager.findGame()', () => { beforeAll(async () => { - await getManager().getRepository(Game).save(gameArray); + await setSmall_gameOnly(); }); afterAll(async () => { - await getManager().getRepository(Game).clear(); + await clearDB(Game); }); test('Find game by UUID', async () => { expect( @@ -191,10 +138,10 @@ describe('GameManager.findGame()', () => { describe('GameManager.countGames()', () => { beforeEach(async () => { - await getManager().getRepository(Game).save(gameArray); + await setSmall_gameOnly(); }); afterEach(async () => { - await getManager().getRepository(Game).clear(); + await clearDB(Game); }); test('Count games', async () => { // Count the number of games that have a null parentGameId. @@ -207,17 +154,17 @@ describe('GameManager.countGames()', () => { expect(await GameManager.countGames()).toBe(count); }); test('Count zero games', async () => { - await getManager().getRepository(Game).clear(); + await clearDB(Game); expect(await GameManager.countGames()).toBe(0); }); }); describe('GameManager.findGameRow()', () => { beforeAll(async () => { - await getManager().getRepository(Game).save(gameArray); + await setSmall_gameOnly(); }); afterAll(async () => { - await getManager().getRepository(Game).clear(); + await clearDB(Game); }); test('Valid game ID, orderBy title', async () => { // People on the internet say that this will be suboptimal. I don't care too much. @@ -471,10 +418,10 @@ describe('GameManager.findGameRow()', () => { describe('GameManager.findGamePageKeyset()', () => { beforeAll(async () => { - await getManager().getRepository(Game).save(gameArray); + await setSmall_gameOnly(); }); afterAll(async () => { - await getManager().getRepository(Game).clear(); + await clearDB(Game); }); test('No filters, orderby title, pagesize 1', async () => { arrayCopy = v8.deserialize( diff --git a/tests/unit/back/game/exampleDB.ts b/tests/unit/back/smallDB.ts similarity index 100% rename from tests/unit/back/game/exampleDB.ts rename to tests/unit/back/smallDB.ts From f1e50785516a1eb0694ae0534e69cbaaf0116735 Mon Sep 17 00:00:00 2001 From: LindirQuenya <53021080+LindirQuenya@users.noreply.github.com> Date: Thu, 26 May 2022 21:51:16 -0400 Subject: [PATCH 83/83] Organize imports --- tests/unit/back/game/GameManager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/back/game/GameManager.test.ts b/tests/unit/back/game/GameManager.test.ts index 91ac16cc1..2a988fad3 100644 --- a/tests/unit/back/game/GameManager.test.ts +++ b/tests/unit/back/game/GameManager.test.ts @@ -1,9 +1,9 @@ import * as GameManager from '@back/game/GameManager'; import { uuid } from '@back/util/uuid'; import { Game } from '@database/entity/Game'; -import { gameArray } from '../smallDB'; -import { setSmall_gameOnly, clearDB, createDefaultDB } from '../TestDB'; import * as v8 from 'v8'; +import { gameArray } from '../smallDB'; +import { clearDB, createDefaultDB, setSmall_gameOnly } from '../TestDB'; // Only the keys of T that can't be null or undefined. type DefinedKeysOf = {