diff --git a/.gitmodules b/.gitmodules index 002bfe8fd..7719e3199 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "docs/api"] path = docs/api url = https://github.com/FlashpointProject/launcher_ApiDocs.git -[submodule "extensions/core-curation"] - path = extensions/core-curation - url = https://github.com/FlashpointProject/Sys-Extension-Core-Curation.git diff --git a/extensions/core-curation b/extensions/core-curation deleted file mode 160000 index edb028ab1..000000000 --- a/extensions/core-curation +++ /dev/null @@ -1 +0,0 @@ -Subproject commit edb028ab104b398178bdc58e146d5d4a1479b793 diff --git a/lang/en.json b/lang/en.json index f0b49b493..40ad22978 100644 --- a/lang/en.json +++ b/lang/en.json @@ -411,6 +411,7 @@ "headerFileOperations": "File", "headerEditCuration": "Edit", "headerTest": "Test", + "headerFpfss": "FPFSS", "importAll": "Import All", "importAllDesc": "Import all curations that are currently loaded", "deleteAll": "Delete All", @@ -491,7 +492,8 @@ "contextCopyAsURL": "Copy File Path as URL", "contextShowInExplorer": "Show File in Explorer", "contextOpenFolderInExplorer": "Open Folder in Explorer", - "shortcuts": "Shortcuts" + "shortcuts": "Shortcuts", + "fpfssOpenSubmissionPage": "Open Submission Page" }, "playlist": { "enterDescriptionHere": "Enter a description here...", diff --git a/package-lock.json b/package-lock.json index b49ab7cdd..504a42d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.1.18", "@fparchive/flashpoint-archive": "0.7.13", + "@types/node-7z": "^2.1.8", "@types/react-virtualized": "^9.21.21", "axios": "1.6.7", "connected-react-router": "6.9.2", @@ -27,7 +28,7 @@ "lodash": "^4.17.21", "mime": "2.4.4", "minimist": "^1.2.7", - "node-7z": "1.1.1", + "node-7z": "3.0.0", "open": "^10.1.0", "ps-tree": "1.2.0", "react": "17.0.2", @@ -3324,9 +3325,16 @@ }, "node_modules/@types/node": { "version": "14.14.31", - "dev": true, "license": "MIT" }, + "node_modules/@types/node-7z": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@types/node-7z/-/node-7z-2.1.8.tgz", + "integrity": "sha512-VjiU7yEbczNc3EFKN4GJcAUqAMkn92P/92r6ARjMSXEdixunMD9lC79mTX81vKxTlNYXuvCJ7zvnzlDbFTt2Vw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.2", "dev": true, @@ -6589,27 +6597,6 @@ "node": ">= 8" } }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.2", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/csstype": { "version": "3.0.6", "license": "MIT" @@ -13970,6 +13957,16 @@ "dev": true, "peer": true }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" + }, + "node_modules/lodash.defaultto": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/lodash.defaultto/-/lodash.defaultto-4.14.0.tgz", + "integrity": "sha512-G6tizqH6rg4P5j32Wy4Z3ZIip7OfG8YWWlPFzUFGcYStH1Ld0l1tWs6NevEQNEDnO1M3NZYjuHuraaFSN5WqeQ==" + }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", @@ -13984,6 +13981,16 @@ "dev": true, "peer": true }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "license": "MIT" @@ -14009,6 +14016,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.negate": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.negate/-/lodash.negate-3.0.2.tgz", + "integrity": "sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA==" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -15068,19 +15080,21 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "license": "MIT" - }, "node_modules/node-7z": { - "version": "1.1.1", - "license": "ISC", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-7z/-/node-7z-3.0.0.tgz", + "integrity": "sha512-KIznWSxIkOYO/vOgKQfJEaXd7rgoFYKZbaurainCEdMhYc7V7mRHX+qdf2HgbpQFcdJL/Q6/XOPrDLoBeTfuZA==", "dependencies": { - "cross-spawn": "^6.0.4", - "debug": "^4.1.1", - "lodash": "^4.17.12", - "normalize-path": "^3.0.0", - "regexp-polyfill": "^1.0.1" + "debug": "^4.3.2", + "lodash.defaultsdeep": "^4.6.1", + "lodash.defaultto": "^4.14.0", + "lodash.flattendeep": "^4.4.0", + "lodash.isempty": "^4.4.0", + "lodash.negate": "^3.0.2", + "normalize-path": "^3.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/node-int64": { @@ -15661,6 +15675,7 @@ }, "node_modules/path-key": { "version": "2.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -16588,10 +16603,6 @@ "node": ">=0.10.0" } }, - "node_modules/regexp-polyfill": { - "version": "1.0.1", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "dev": true, @@ -17168,6 +17179,7 @@ }, "node_modules/shebang-command": { "version": "1.2.0", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" @@ -17178,6 +17190,7 @@ }, "node_modules/shebang-regex": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" diff --git a/package.json b/package.json index 1347e4711..31b0cec43 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.1.18", "@fparchive/flashpoint-archive": "0.7.13", + "@types/node-7z": "2.1.8", "@types/react-virtualized": "^9.21.21", "axios": "1.6.7", "connected-react-router": "6.9.2", @@ -46,7 +47,7 @@ "lodash": "^4.17.21", "mime": "2.4.4", "minimist": "^1.2.7", - "node-7z": "1.1.1", + "node-7z": "3.0.0", "open": "^10.1.0", "ps-tree": "1.2.0", "react": "17.0.2", diff --git a/src/back/curate/fpfss.ts b/src/back/curate/fpfss.ts new file mode 100644 index 000000000..0dc4b275a --- /dev/null +++ b/src/back/curate/fpfss.ts @@ -0,0 +1,34 @@ +import { FPFSS_INFO_FILENAME } from '@shared/curate/fpfss'; +import { str } from '@shared/utils/Coerce'; +import { ObjectParser } from '@shared/utils/ObjectParser'; +import { CurationFpfssInfo } from 'flashpoint-launcher'; +import * as fs from 'fs'; +import * as path from 'path'; + +export async function getCurationFpfssInfo(folder: string): Promise { + return fs.promises.readFile(path.join(folder, FPFSS_INFO_FILENAME), { encoding: 'utf-8' }) + .then((dataStr) => { + return parseCurationFpfssInfo(JSON.parse(dataStr)); + }) + .catch(() => { + return null; + }); +} + +export async function saveCurationFpfssInfo(folder: string, info: CurationFpfssInfo) { + const data = parseCurationFpfssInfo(info); + return fs.promises.writeFile(path.join(folder, FPFSS_INFO_FILENAME), JSON.stringify(data, undefined, 2)); +} + +export function parseCurationFpfssInfo(data: any): CurationFpfssInfo { + const info: CurationFpfssInfo = { + id: '' + }; + + const parser = new ObjectParser({ + input: data + }); + parser.prop('id', v => info.id = str(v)); + + return info; +} diff --git a/src/back/curate/util.ts b/src/back/curate/util.ts index 320d2f248..cd2e81d1c 100644 --- a/src/back/curate/util.ts +++ b/src/back/curate/util.ts @@ -6,12 +6,11 @@ import { uuid } from '@back/util/uuid'; import { fixSlashes } from '@shared/Util'; import { BackOut } from '@shared/back/types'; import { CURATIONS_FOLDER_WORKING } from '@shared/constants'; -import { LoadedCuration } from '@shared/curate/types'; import { getContentFolderByKey } from '@shared/curate/util'; import { GamePropSuggestions } from '@shared/interfaces'; import { LangContainer } from '@shared/lang'; import axios from 'axios'; -import { AddAppCuration, CurationState, CurationWarnings } from 'flashpoint-launcher'; +import { AddAppCuration, CurationFpfssInfo, CurationState, CurationWarnings, LoadedCuration } from 'flashpoint-launcher'; import * as fs from 'fs-extra'; import * as http from 'http'; import { Progress } from 'node-7z'; @@ -20,6 +19,7 @@ import { checkAndDownloadGameData, extractFullPromise, fpDatabase } from '..'; import { loadCurationIndexImage } from './parse'; import { readCurationMeta } from './read'; import { saveCuration } from './write'; +import { getCurationFpfssInfo } from './fpfss'; const whitelistedBaseFiles = ['logo.png', 'ss.png']; @@ -30,7 +30,7 @@ export type UpdateCurationFileFunc = (folder: string, relativePath: string, data export type RemoveCurationFileFunc = (folder: string, relativePath: string) => Promise; export const onFileServerRequestPostCuration = - async (pathname: string, url: URL, req: http.IncomingMessage, res: http.ServerResponse, tempCurationsPath: string, onNewCuration: (filePath: string, onProgress?: (progress: Progress) => void) => Promise) => { + async (pathname: string, url: URL, req: http.IncomingMessage, res: http.ServerResponse, tempCurationsPath: string, onNewCuration: (filePath: string, fpfssInfo: CurationFpfssInfo | null, onProgress?: (progress: Progress) => void) => Promise) => { if (req.method === 'POST') { const chunks: any[] = []; req.on('data', (chunk) => { @@ -46,7 +46,7 @@ export const onFileServerRequestPostCuration = const randomFilePath = path.join(tempCurationsPath, `${uuid()}.7z`); await fs.promises.mkdir(path.dirname(randomFilePath), { recursive: true }); await fs.promises.writeFile(randomFilePath, data); - await onNewCuration(randomFilePath) + await onNewCuration(randomFilePath, null) .then(() => { res.writeHead(200); res.end(); @@ -213,6 +213,7 @@ export async function loadCurationFolder(rootPath: string, folderName: string, s group: parsedMeta.group, game: parsedMeta.game, addApps: parsedMeta.addApps, + fpfssInfo: null, thumbnail: await loadCurationIndexImage(path.join(rootPath, folderName, 'logo.png')), screenshot: await loadCurationIndexImage(path.join(rootPath, folderName, 'ss.png')) }; @@ -222,6 +223,8 @@ export async function loadCurationFolder(rootPath: string, folderName: string, s alreadyImported, warnings: await genCurationWarnings(loadedCuration, state.config.flashpointPath, state.suggestions, state.languageContainer.curate, state.apiEmitters.curations.onWillGenCurationWarnings) }; + // Try and load fpfss data + curation.fpfssInfo = await getCurationFpfssInfo(path.join(rootPath, folderName)); state.loadedCurations.push(curation); genContentTree(getContentFolderByKey(folderName, state.config.flashpointPath)).then((contentTree) => { const curationIdx = state.loadedCurations.findIndex((c) => c.folder === folderName); @@ -358,6 +361,7 @@ export async function makeCurationFromGame(state: BackState, gameId: string, ski folder, uuid: game.id, group: '', + fpfssInfo: null, game: { ...game, tags: game.detailedTags, diff --git a/src/back/curate/write.ts b/src/back/curate/write.ts index 5aca3bfb8..a5b3df21a 100644 --- a/src/back/curate/write.ts +++ b/src/back/curate/write.ts @@ -1,7 +1,8 @@ -import { LoadedCuration } from '@shared/curate/types'; import * as fs from 'fs'; import * as path from 'path'; import * as YAML from 'yaml'; +import { saveCurationFpfssInfo } from './fpfss'; +import { LoadedCuration } from 'flashpoint-launcher'; type CurationMetaFile = { 'Application Path'?: string; @@ -47,9 +48,15 @@ type CurationFormatAddApp = { export async function saveCuration(fullCurationPath: string, curation: LoadedCuration): Promise { + // Save Meta const metaPath = path.join(fullCurationPath, 'meta.yaml'); const meta = YAML.stringify(convertEditToCurationMetaFile(curation)); await fs.promises.writeFile(metaPath, meta); + + // Save FPFSS info + if (curation.fpfssInfo) { + await saveCurationFpfssInfo(fullCurationPath, curation.fpfssInfo); + } } function convertEditToCurationMetaFile(curation: LoadedCuration): CurationMetaFile { diff --git a/src/back/extensions/ApiImplementation.ts b/src/back/extensions/ApiImplementation.ts index f8c216071..498c6ff0c 100644 --- a/src/back/extensions/ApiImplementation.ts +++ b/src/back/extensions/ApiImplementation.ts @@ -23,7 +23,7 @@ import { BrowsePageLayout, ScreenshotPreviewMode } from '@shared/BrowsePageLayou import { ILogEntry, LogLevel } from '@shared/Log/interface'; import { BackOut } from '@shared/back/types'; import { CURATIONS_FOLDER_WORKING } from '@shared/constants'; -import { CurationMeta, LoadedCuration } from '@shared/curate/types'; +import { CurationMeta } from '@shared/curate/types'; import { getContentFolderByKey } from '@shared/curate/util'; import { CurationTemplate, IExtensionManifest } from '@shared/extensions/interfaces'; import { ProcessState, Task } from '@shared/interfaces'; @@ -410,7 +410,7 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, status: `Loading ${filePath}` }); } - const curState = await loadCurationArchive(filePath) + const curState = await loadCurationArchive(filePath, null) .catch((error) => { log.error('Curate', `Failed to load curation archive! ${error.toString()}`); state.socketServer.broadcast(BackOut.OPEN_ALERT, formatString(state.languageContainer['dialog'].failedToLoadCuration, error.toString()) as string); @@ -517,12 +517,13 @@ export function createApiFactory(extId: string, extManifest: IExtensionManifest, const contentFolder = path.join(curPath, 'content'); await fs.promises.mkdir(contentFolder, { recursive: true }); - const data: LoadedCuration = { + const data: flashpoint.LoadedCuration = { folder, uuid: uuid(), group: '', game: meta || {}, addApps: [], + fpfssInfo: null, thumbnail: await loadCurationIndexImage(path.join(curPath, 'logo.png')), screenshot: await loadCurationIndexImage(path.join(curPath, 'ss.png')) }; diff --git a/src/back/importGame.ts b/src/back/importGame.ts index 5c2846065..9f90e32f1 100644 --- a/src/back/importGame.ts +++ b/src/back/importGame.ts @@ -1,13 +1,13 @@ import { LOGOS, SCREENSHOTS } from '@shared/constants'; import { CurationIndexImage } from '@shared/curate/OLD_types'; import { convertEditToCurationMetaFile } from '@shared/curate/metaToMeta'; -import { AddAppCuration, CurationMeta, LoadedCuration } from '@shared/curate/types'; +import { AddAppCuration, CurationMeta } from '@shared/curate/types'; import { getCurationFolder } from '@shared/curate/util'; import { TaskProgress } from '@shared/utils/TaskProgress'; import { newGame } from '@shared/utils/misc'; import * as child_process from 'child_process'; import { execFile } from 'child_process'; -import { AdditionalApp, Game, GameLaunchInfo, Platform, Tag, TagCategory } from 'flashpoint-launcher'; +import { AdditionalApp, Game, GameLaunchInfo, LoadedCuration, Platform, Tag, TagCategory } from 'flashpoint-launcher'; import * as fs from 'fs-extra'; import * as crypto from 'crypto'; import * as path from 'path'; @@ -376,6 +376,7 @@ export async function launchCuration(curation: LoadedCuration, symlinkCurationCo if (!skipLink || !symlinkCurationContent) { await linkContentFolder(curation.folder, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } curationLog(`Launching Curation ${curation.game.title}`); const game = await createGameFromCurationMeta(curation.folder, curation.game, [], new Date()); + clearWininetCache(); await GameLauncher.launchGame({ ...opts, game: game @@ -404,6 +405,7 @@ export async function launchAddAppCuration(folder: string, appCuration: AddAppCu if (!skipLink || !symlinkCurationContent) { await linkContentFolder(folder, opts.fpPath, opts.isDev, opts.exePath, opts.htdocsPath, symlinkCurationContent); } const addApp = createAddAppFromCurationMeta(appCuration, createPlaceholderGame(platforms)); await onWillEvent.fire(addApp); + clearWininetCache(); await GameLauncher.launchAdditionalApplication({ ...opts, addApp: addApp @@ -411,6 +413,16 @@ export async function launchAddAppCuration(folder: string, appCuration: AddAppCu await onDidEvent.fire(addApp); } +function clearWininetCache() { + if (process.platform === 'win32') { + child_process.exec('RunDll32.exe InetCpl.cpl,ClearMyTracksByProcess 8', (err) => { + if (err) { + log.error('Launcher', `Error clearing WinINet Cache: ${err}`); + } + }); + } +} + function logMessage(text: string, folder: string): void { console.log(`- ${text}\n (id: ${folder})`); } diff --git a/src/back/index.ts b/src/back/index.ts index b32fbde26..6a5e573c4 100644 --- a/src/back/index.ts +++ b/src/back/index.ts @@ -7,8 +7,7 @@ import { } from '@shared/Util'; import * as os from 'os'; import { BackIn, BackInit, BackInitArgs, BackOut, BackResParams, ComponentState, ComponentStatus, DownloadDetails } from '@shared/back/types'; -import { LoadedCuration } from '@shared/curate/types'; -import { getContentFolderByKey } from '@shared/curate/util'; +import { getContentFolderByKey, getCurationFolder } from '@shared/curate/util'; import { ILogoSet, LogoSet } from '@shared/extensions/interfaces'; import { IBackProcessInfo, RecursivePartial } from '@shared/interfaces'; import { LangFileContent, getDefaultLocalization } from '@shared/lang'; @@ -74,6 +73,7 @@ import { onDidInstallGameData, onDidRemoveGame, onDidRemovePlaylistGame, onDidUn import { dispose } from './util/lifecycle'; import { formatString } from '@shared/utils/StringFormatter'; import { awaitDialog } from './util/dialog'; +import { saveCurationFpfssInfo } from './curate/fpfss'; export const VERBOSE = { enabled: false @@ -1455,7 +1455,7 @@ async function removeFileServerDownloadItem(item: ImageDownloadItem): Promise= 0) { state.fileServerDownloads.current.splice(index, 1); } } -export async function loadCurationArchive(filePath: string, onProgress?: (progress: Progress) => void): Promise { +export async function loadCurationArchive(filePath: string, fpfssInfo: flashpoint.CurationFpfssInfo | null, onProgress?: (progress: Progress) => void): Promise { const key = uuid(); const extractPath = path.resolve(state.config.flashpointPath, CURATIONS_FOLDER_EXTRACTING, key); // Extract to temp folder @@ -1488,12 +1488,13 @@ export async function loadCurationArchive(filePath: string, onProgress?: (progre const parsedMeta = await readCurationMeta(curationPath, state.platformAppPaths); if (!parsedMeta) { throw new Error('Fail'); } - const loadedCuration: LoadedCuration = { + const loadedCuration: flashpoint.LoadedCuration = { folder: key, uuid: parsedMeta.uuid || uuid(), group: parsedMeta.group, game: parsedMeta.game, addApps: parsedMeta.addApps, + fpfssInfo, thumbnail: await loadCurationIndexImage(path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, key, 'logo.png')), screenshot: await loadCurationIndexImage(path.join(state.config.flashpointPath, CURATIONS_FOLDER_WORKING, key, 'ss.png')), }; @@ -1503,6 +1504,9 @@ export async function loadCurationArchive(filePath: string, onProgress?: (progre alreadyImported, warnings: await genCurationWarnings(loadedCuration, state.config.flashpointPath, state.suggestions, state.languageContainer.curate, state.apiEmitters.curations.onWillGenCurationWarnings) }; + if (fpfssInfo) { + await saveCurationFpfssInfo(getCurationFolder(curation, state.config.flashpointPath), fpfssInfo); + } genContentTree(getContentFolderByKey(key, state.config.flashpointPath)) .then((contentTree) => { diff --git a/src/back/responses.ts b/src/back/responses.ts index aeb0e2a5e..a8f8b50c5 100644 --- a/src/back/responses.ts +++ b/src/back/responses.ts @@ -6,7 +6,6 @@ import { BackIn, BackInit, BackOut, ComponentState, CurationImageEnum, DownloadD import { overwriteConfigData } from '@shared/config/util'; import { CURATIONS_FOLDER_EXPORTED, CURATIONS_FOLDER_TEMP, CURATIONS_FOLDER_WORKING, LOGOS, SCREENSHOTS, VIEW_PAGE_SIZE } from '@shared/constants'; import { convertGameToCurationMetaFile } from '@shared/curate/metaToMeta'; -import { LoadedCuration } from '@shared/curate/types'; import { getContentFolderByKey, getCurationFolder } from '@shared/curate/util'; import { AppProvider, BrowserApplicationOpts } from '@shared/extensions/interfaces'; import { DeepPartial, GamePropSuggestions, ProcessAction, ProcessState } from '@shared/interfaces'; @@ -21,7 +20,7 @@ import { throttle } from '@shared/utils/throttle'; import * as axiosImport from 'axios'; import * as child_process from 'child_process'; import { execSync } from 'child_process'; -import { ConfigSchema, CurationState, Game, GameConfig, GameData, GameLaunchInfo, GameMetadataSource, GameMiddlewareInfo, RequestGameRange, ResponseGameRange, Tag, TagCategory } from 'flashpoint-launcher'; +import { ConfigSchema, CurationState, Game, GameConfig, GameData, GameLaunchInfo, GameMetadataSource, GameMiddlewareInfo, LoadedCuration, RequestGameRange, ResponseGameRange, Tag, TagCategory } from 'flashpoint-launcher'; import * as fs from 'fs-extra'; import * as fs_extra from 'fs-extra'; import * as https from 'https'; @@ -69,6 +68,7 @@ import { runService } from './util/misc'; import { uuid } from './util/uuid'; +import { FPFSS_INFO_FILENAME } from '@shared/curate/fpfss'; const axios = axiosImport.default; @@ -1977,7 +1977,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise { + await loadCurationArchive(filePath, null, throttle((progress: Progress) => { taskProgress.setStageProgress((progress.percent / 100), `Extracting Files - ${progress.fileCount}`); }, 200)) .catch((error) => { @@ -2170,9 +2170,10 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise((resolve) => { - return add(filePath, curPath, { recursive: true, $bin: pathTo7zBack(state.isDev, state.exePath) }) + // Cast required until types fixed + return (add as any)(filePath, curPath, { recursive: true, exclude: [`!${FPFSS_INFO_FILENAME}`], $bin: pathTo7zBack(state.isDev, state.exePath) }) .on('end', () => { resolve(); }) - .on('error', (error) => { + .on('error', (error: any) => { log.error('Curate', error.message); resolve(); }); @@ -2233,6 +2234,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise Promise { + state.socketServer.register(BackIn.FPFSS_OPEN_CURATION, async (event, fpfssInfo, url, accessToken, taskId) => { // Setup task info const taskProgress = new TaskProgress(2); if (taskId) { @@ -2283,7 +2285,7 @@ export function registerRequestCallbacks(state: BackState, init: () => Promise { + await loadCurationArchive(tempFile, fpfssInfo, throttle((progress: Progress) => { taskProgress.setStageProgress((progress.percent / 100), `Extracting Files - ${progress.fileCount}`); }, 200)) .catch((error) => { diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index c6e953ec0..07005aa4d 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -11,7 +11,7 @@ import { arrayShallowStrictEquals } from '@shared/utils/compare'; import { debounce } from '@shared/utils/debounce'; import axios from 'axios'; import { clipboard, ipcRenderer, Menu, MenuItemConstructorOptions } from 'electron'; -import { DialogField, DialogState, Game, Playlist, PlaylistGame, RequestGameRange } from 'flashpoint-launcher'; +import { CurationFpfssInfo, DialogField, DialogState, Game, Playlist, PlaylistGame, RequestGameRange } from 'flashpoint-launcher'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as React from 'react'; @@ -146,18 +146,25 @@ export class App extends React.Component { } else { switch (parts[1]) { case 'open_curation': { - this.performFpfssAction(async (user) => { - try { + if (parts.length > 4) { + this.performFpfssAction(async (user) => { + const fpfssInfo: CurationFpfssInfo = { + id: parts[4] + }; // Build url const url = `${this.props.preferencesData.fpfssBaseUrl}/${parts.slice(2).join('/')}`; // Generate task const newTask = newCurateTask('Importing FPFSS Submission...', 'Importing...', this.props.addTask); // Import - await window.Shared.back.request(BackIn.FPFSS_OPEN_CURATION, url, user.accessToken, newTask.id); - } catch (err) { - alert(`Error fetching curation: ${err}`); - } - }); + await window.Shared.back.request(BackIn.FPFSS_OPEN_CURATION, fpfssInfo, url, user.accessToken, newTask.id) + .catch((err) => { + newTask.error = err; + newTask.finished = true; + this.props.setTask(newTask.id, newTask); + throw err; + }); + }); + } break; } case 'edit_game': { @@ -2115,7 +2122,29 @@ export class App extends React.Component { if (user) { // User exists, carry on to callback - await cb(user); + let retries = 0; + while (retries <= 1) { + retries += 1; + try { + await cb(user); + break; + } catch (err) { + log.error('Launcher', `[FPFSS] Failed to execute action - ${err}`); + if (retries <= 1) { + // Reauth if the action failed + log.info('Launcher', '[FPFSS] Attempting reauth'); + user = await fpfssLogin(this.props.dispatchMain, this.props.main.dialogResEvent) + .catch((err) => { + if (err !== 'User Cancelled') { + alert(err); + } + }) as FpfssUser | null; + if (!user) { + break; + } + } + } + } } } } diff --git a/src/renderer/components/pages/CuratePage.tsx b/src/renderer/components/pages/CuratePage.tsx index 904f7912d..c1ae7b0ce 100644 --- a/src/renderer/components/pages/CuratePage.tsx +++ b/src/renderer/components/pages/CuratePage.tsx @@ -59,6 +59,13 @@ export function CuratePage(props: CuratePageProps) { const onSaveImportedCurationChange = onCheckboxChange('saveImportedCurations'); const onTagFiltersInCurateChange = onCheckboxChange('tagFiltersInCurate'); + const onOpenSubmissionPage = () => { + if (curation?.fpfssInfo) { + const subPage = `${props.preferencesData.fpfssBaseUrl}/web/submission/${curation.fpfssInfo.id}`; + remote.shell.openExternal(subPage); + } + }; + const onDupeCurations = React.useCallback(() => { const selected = props.curate.selected; props.dispatchCurate({ @@ -773,6 +780,14 @@ export function CuratePage(props: CuratePageProps) { checked={props.preferencesData.symlinkCurationContent} /> +
+
{strings.curate.headerFpfss}
+ +
{extButtons} diff --git a/src/shared/back/types.ts b/src/shared/back/types.ts index 775c7581b..8828fb84c 100644 --- a/src/shared/back/types.ts +++ b/src/shared/back/types.ts @@ -1,11 +1,11 @@ import { ChangedMeta, MetaEditFlags } from '@shared/MetaEdit'; import { EditCurationMeta } from '@shared/curate/OLD_types'; -import { AddAppCuration, ContentTree, LoadedCuration, PlatformAppPathSuggestions } from '@shared/curate/types'; +import { AddAppCuration, ContentTree, PlatformAppPathSuggestions } from '@shared/curate/types'; import { ExtensionContribution, IExtensionDescription, LogoSet } from '@shared/extensions/interfaces'; import { Legacy_GamePlatform } from '@shared/legacy/interfaces'; import { SocketTemplate } from '@shared/socket/types'; import { MessageBoxOptions, OpenDialogOptions, OpenExternalOptions, SaveDialogOptions } from 'electron'; -import { AppPreferencesData, ConfigSchema, CurationState, CurationWarnings, DialogState, DialogStateTemplate, Game, GameConfig, GameData, GameDataSource, GameMetadataSource, GameMiddlewareConfig, GameMiddlewareInfo, MergeTagData, Platform, Playlist, PlaylistGame, Tag, TagCategory, TagFilterGroup, TagSuggestion } from 'flashpoint-launcher'; +import { AppPreferencesData, ConfigSchema, CurationFpfssInfo, CurationState, CurationWarnings, DialogState, DialogStateTemplate, Game, GameConfig, GameData, GameDataSource, GameMetadataSource, GameMiddlewareConfig, GameMiddlewareInfo, LoadedCuration, MergeTagData, Platform, Playlist, PlaylistGame, Tag, TagCategory, TagFilterGroup, TagSuggestion } from 'flashpoint-launcher'; import { ILogEntry, ILogPreEntry, LogLevel } from '../Log/interface'; import { Theme } from '../ThemeFile'; import { AppConfigData, AppExtConfigData } from '../config/interfaces'; @@ -359,7 +359,7 @@ export type BackInTemplate = SocketTemplate boolean; // FPFSS - [BackIn.FPFSS_OPEN_CURATION]: (url: string, accessToken: string, taskId: string) => void; + [BackIn.FPFSS_OPEN_CURATION]: (fpfssInfo: CurationFpfssInfo, url: string, accessToken: string, taskId: string) => void; // Curate [BackIn.CURATE_LOAD_ARCHIVES]: (filePaths: string[], taskId?: string) => void; diff --git a/src/shared/curate/fpfss.ts b/src/shared/curate/fpfss.ts new file mode 100644 index 000000000..348195433 --- /dev/null +++ b/src/shared/curate/fpfss.ts @@ -0,0 +1 @@ +export const FPFSS_INFO_FILENAME = 'fpfss.info'; diff --git a/src/shared/curate/types.ts b/src/shared/curate/types.ts index b5c5780df..f3e06232b 100644 --- a/src/shared/curate/types.ts +++ b/src/shared/curate/types.ts @@ -1,15 +1,4 @@ import { Platform, Tag } from 'flashpoint-launcher'; -import { CurationIndexImage } from './OLD_types'; - -export type LoadedCuration = { - folder: string; - uuid: string; - group: string; - game: CurationMeta; - addApps: AddAppCuration[]; - thumbnail: CurationIndexImage; - screenshot: CurationIndexImage; -} export type ContentTree = { // Root node - 'content' folder diff --git a/src/shared/lang.ts b/src/shared/lang.ts index 134072804..f52dcc1de 100644 --- a/src/shared/lang.ts +++ b/src/shared/lang.ts @@ -423,6 +423,7 @@ const langTemplate = { 'headerFileOperations', 'headerEditCuration', 'headerTest', + 'headerFpfss', 'importAll', 'importAllDesc', 'deleteAll', @@ -505,6 +506,7 @@ const langTemplate = { 'exportDataPacks', 'exportSelectedDataPacks', 'shortcuts', + 'fpfssOpenSubmissionPage', ] as const, playlist: [ 'enterDescriptionHere', diff --git a/typings/flashpoint-launcher.d.ts b/typings/flashpoint-launcher.d.ts index a67705f7f..7fc23a62a 100644 --- a/typings/flashpoint-launcher.d.ts +++ b/typings/flashpoint-launcher.d.ts @@ -1339,6 +1339,10 @@ declare module 'flashpoint-launcher' { logLevel: number; } + type CurationFpfssInfo = { + id: string; + }; + export type LoadedCuration = { folder: string; uuid: string; @@ -1347,7 +1351,8 @@ declare module 'flashpoint-launcher' { addApps: AddAppCuration[]; thumbnail: CurationIndexImage; screenshot: CurationIndexImage; - } + fpfssInfo: CurationFpfssInfo | null; + } export type CurationState = LoadedCuration & { alreadyImported: boolean; diff --git a/typings/node-7z.d.ts b/typings/node-7z.d.ts deleted file mode 100644 index 3100df4e8..000000000 --- a/typings/node-7z.d.ts +++ /dev/null @@ -1,108 +0,0 @@ -// OVERRIDES BROKEN NODE-7Z TYPINGS - DO NOT REMOVE - -// Type definitions for node-7z v0.4.1 -// Project: https://github.com/quentinrossetti/node-7z -// Definitions by: Erik Rothoff Andersson -// Colin Berry -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped/ - -declare module 'node-7z' { - import Readable = require('stream'); - - // @TODO Verify interfaces are correct - interface Data { - file: string; - status: string; - attributes?: string; - size?: number; - sizeCompressed?: number; - hash?: string; - } - - interface Progress { - percent: number; - fileCount: number; - file: string; - } - - import * as events from 'events'; - - class internal extends events.EventEmitter { - pipe(destination: T, options?: { end?: boolean; }): T; - } - - class Stream extends internal { } - - class ZipReadable extends Stream implements NodeJS.ReadableStream { - readable: boolean; - readonly readableHighWaterMark: number; - readonly readableLength: number; - _read(size: number): void; - read(size?: number): any; - setEncoding(encoding: string): this; - pause(): this; - resume(): this; - isPaused(): boolean; - unpipe(destination?: NodeJS.WritableStream): this; - unshift(chunk: any, encoding?: BufferEncoding): void; - wrap(oldStream: NodeJS.ReadableStream): this; - push(chunk: any, encoding?: string): boolean; - _destroy(error: Error | null, callback: (error?: Error | null) => void): void; - destroy(error?: Error): void; - - info: Map; - - addListener(event: 'end', listener: () => void): this; - addListener(event: 'data', listener: (data: Data) => void): this; - addListener(event: 'progress', listener: (progress: Progress) => void): this; - addListener(event: 'error', listener: (err: Error) => void): this; - - emit(event: 'end'): boolean; - emit(event: 'data', data: Data): boolean; - emit(event: 'progress', progress: Progress): boolean; - emit(event: 'error', listener: (err: Error) => void): this; - - on(event: 'end', listener: () => void): this; - on(event: 'data', listener: (data: Data) => void): this; - on(event: 'progress', listener: (progress: Progress) => void): this; - on(event: 'error', listener: (err: Error) => void): this; - - once(event: 'end', listener: () => void): this; - once(event: 'data', listener: (data: Data) => void): this; - once(event: 'progress', listener: (progress: Progress) => void): this; - once(event: 'error', listener: (err: Error) => void): this; - - prependListener(event: 'end', listener: () => void): this; - prependListener(event: 'data', listener: (data: Data) => void): this; - prependListener(event: 'progress', listener: (progress: Progress) => void): this; - prependListener(event: 'error', listener: (err: Error) => void): this; - - prependOnceListener(event: 'end', listener: () => void): this; - prependOnceListener(event: 'data', listener: (data: Data) => void): this; - prependOnceListener(event: 'progress', listener: (progress: Progress) => void): this; - prependOnceListener(event: 'error', listener: (err: Error) => void): this; - - removeListener(event: 'end', listener: () => void): this; - removeListener(event: 'data', listener: (data: Data) => void): this; - removeListener(event: 'progress', listener: (data: Data) => void): this; - removeListener(event: 'error', listener: (error: Error) => void): this; - - [Symbol.asyncIterator](): AsyncIterableIterator; - } - - // Options are mapped to the 7z program so there is no idea to define all possible types here - interface CommandLineSwitches { - raw?: Array; - [key: string]: any - } - - function add(archive: string, files: string | Array, options?: CommandLineSwitches): ZipReadable; - // @TODO Figure out how to get delete and test working as function names - // function _delete(archive: string, files: string | Array, options: CommandLineSwitches): PromiseWithProgress<{}>; - function extract(archive: string, dest: string, options?: CommandLineSwitches): ZipReadable; - function extractFull(archive: string, dest: string, options?: CommandLineSwitches): ZipReadable; - function list(archive: string, options?: CommandLineSwitches): ZipReadable; - // function _test(archive: string, options: CommandLineSwitches): PromiseWithProgress<{}>; - function update(archive: string, files: string | Array, options?: CommandLineSwitches): ZipReadable; - -}