From fb67402f17d52646cbb1b3dc98a0a8720fdb4341 Mon Sep 17 00:00:00 2001 From: 3top1a <3top1a.official@gmail.com> Date: Sat, 22 Jan 2022 17:00:03 +0100 Subject: Basic, prob. linux only --- src/utils.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 50ffed0..11f7dcb 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ const path = require("path"); const fs = require("fs-extra"); +const os = require("os") const copy = require("copy-dir"); const { app, dialog, ipcMain, Notification } = require("electron"); @@ -14,6 +15,7 @@ const unzip = require("unzipper"); const run = require("child_process").spawn; const exec = require("child_process").exec; const { https } = require("follow-redirects"); +const vdf = require('simple-vdf'); process.chdir(app.getPath("appData")); @@ -119,6 +121,68 @@ function setpath(win) { if (! win) { // CLI settings.gamepath = cli.param("setpath"); } else { // GUI + // Autodetect path + + + // Windows only using powershell and windows registery + // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ + // https://nodejs.org/api/os.html#osplatform + if (os.platform() == "win32") + { + // TODO + } + + // Detect using + // Windows: C:\Program Files (x86)\Steam\steamapps + // Linux: ~/.steam/steam/steamapps/ + // Mac: ~/Library/Application\ Support/Steam/steamapps + + function read_libraryfolders(read_data) + { + // Parse read_data + data = vdf.parse(read_data); + + //data['libraryfolders'] + // `.length - 1` This is because the last value is `contentstatsid` + for (let pathIterator = 0; pathIterator < Object.values(data['libraryfolders']).length - 1; pathIterator++) { + let data_array = Object.values(data['libraryfolders'][pathIterator]) + + if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { + // Found the location + settings.gamepath = data_array[0] + "/steamapps/common/Titanfall2/"; + settings.zip = path.join(settings.gamepath + "/northstar.zip"); + saveSettings(); + win.webContents.send("newpath", settings.gamepath); + ipcMain.emit("newpath", null, settings.gamepath); + + saveSettings(); + cli.exit(); + // Return 1 if success + return 1; + } + } + } + + switch (os.platform()) { + case "win32": + if (fs.existsSync( "C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) + { + let read_data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") + if (read_libraryfolders(read_data.toString()) == 1) + return + } + break; + case "linux": + if (fs.existsSync( os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf")) + { + let read_data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") + if (read_libraryfolders(read_data.toString()) == 1) + return + } + break; + } + + // Let user choose manually dialog.showOpenDialog({properties: ["openDirectory"]}).then(res => { if (res.canceled) { ipcMain.emit("newpath", null, false); @@ -134,11 +198,12 @@ function setpath(win) { saveSettings(); win.webContents.send("newpath", settings.gamepath); ipcMain.emit("newpath", null, settings.gamepath); + + saveSettings(); + cli.exit(); + return; }).catch(err => {console.error(err)}) } - - saveSettings(); - cli.exit(); } // As to not have to do the same one liner a million times, this -- cgit v1.2.3 From 6a0fe7b963a7934741d06416f5161e854505f36a Mon Sep 17 00:00:00 2001 From: 3top1a <3top1a.official@gmail.com> Date: Sat, 22 Jan 2022 17:02:53 +0100 Subject: Don't save settings twice --- src/utils.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 11f7dcb..86ea5ae 100644 --- a/src/utils.js +++ b/src/utils.js @@ -199,7 +199,6 @@ function setpath(win) { win.webContents.send("newpath", settings.gamepath); ipcMain.emit("newpath", null, settings.gamepath); - saveSettings(); cli.exit(); return; }).catch(err => {console.error(err)}) -- cgit v1.2.3 From b1b7d260a1a333618c045d3c43b7af5573c274a3 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sat, 22 Jan 2022 19:10:08 +0100 Subject: initial review of VDF parsing --- src/utils.js | 46 +++++++++++++++------------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 86ea5ae..521931d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,5 @@ const path = require("path"); const fs = require("fs-extra"); -const os = require("os") const copy = require("copy-dir"); const { app, dialog, ipcMain, Notification } = require("electron"); @@ -11,11 +10,11 @@ const cli = require("./cli"); const lang = require("./lang"); const requests = require("./requests"); +const vdf = require("simple-vdf"); const unzip = require("unzipper"); const run = require("child_process").spawn; const exec = require("child_process").exec; const { https } = require("follow-redirects"); -const vdf = require('simple-vdf'); process.chdir(app.getPath("appData")); @@ -122,30 +121,19 @@ function setpath(win) { settings.gamepath = cli.param("setpath"); } else { // GUI // Autodetect path - - // Windows only using powershell and windows registery // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ - // https://nodejs.org/api/os.html#osplatform - if (os.platform() == "win32") - { - // TODO - } - - // Detect using - // Windows: C:\Program Files (x86)\Steam\steamapps - // Linux: ~/.steam/steam/steamapps/ - // Mac: ~/Library/Application\ Support/Steam/steamapps + if (process.platform == "win32") {/* TODO */} - function read_libraryfolders(read_data) - { + // Detect using Steam VDF + function readvdf(data) { // Parse read_data - data = vdf.parse(read_data); + data = vdf.parse(data); //data['libraryfolders'] // `.length - 1` This is because the last value is `contentstatsid` - for (let pathIterator = 0; pathIterator < Object.values(data['libraryfolders']).length - 1; pathIterator++) { - let data_array = Object.values(data['libraryfolders'][pathIterator]) + for (let pathIterator = 0; pathIterator < Object.values(data["libraryfolders"]).length - 1; pathIterator++) { + let data_array = Object.values(data["libraryfolders"][pathIterator]) if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { // Found the location @@ -163,26 +151,22 @@ function setpath(win) { } } - switch (os.platform()) { + switch (process.platform) { case "win32": - if (fs.existsSync( "C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) - { - let read_data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") - if (read_libraryfolders(read_data.toString()) == 1) - return + if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { + let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") + if (readvdf(data.toString()) == 1) {return} } break; case "linux": - if (fs.existsSync( os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf")) - { - let read_data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") - if (read_libraryfolders(read_data.toString()) == 1) - return + if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { + let data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") + if (readvdf(data.toString()) == 1) {return} } break; } - // Let user choose manually + // Fallback to manual selection dialog.showOpenDialog({properties: ["openDirectory"]}).then(res => { if (res.canceled) { ipcMain.emit("newpath", null, false); -- cgit v1.2.3 From 6a785c3a4c41bb7128508c34a1a2af5df12b9f8e Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sat, 22 Jan 2022 22:19:16 +0100 Subject: implement #52 into #51 --- src/utils.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 521931d..50ad240 100644 --- a/src/utils.js +++ b/src/utils.js @@ -116,14 +116,33 @@ northstar_auto_updates: { // // If running with CLI it takes in the --setpath argument otherwise it // open the systems file browser for the user to select a path. -function setpath(win) { +async function setpath(win) { if (! win) { // CLI settings.gamepath = cli.param("setpath"); } else { // GUI + function setGamepath(folder) { + settings.gamepath = folder + "/steamapps/common/Titanfall2/"; + settings.zip = path.join(settings.gamepath + "/northstar.zip"); + saveSettings(); + win.webContents.send("newpath", settings.gamepath); + ipcMain.emit("newpath", null, settings.gamepath); + } + // Autodetect path // Windows only using powershell and windows registery // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ - if (process.platform == "win32") {/* TODO */} + if (process.platform == "linux") { + try { + let {stdout} = await exec("Get-Item -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\", {"shell":"powershell.exe"}); + let originPath = stdout.split('\n') + .filter(r => r.indexOf("Install Dir") !== -1)[0] + .replace(/\s+/g,' ') + .trim() + .replace("Install Dir : ",""); + setGamepath(originPath) + return + } catch (err) {} + } // Detect using Steam VDF function readvdf(data) { @@ -137,16 +156,11 @@ function setpath(win) { if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { // Found the location - settings.gamepath = data_array[0] + "/steamapps/common/Titanfall2/"; - settings.zip = path.join(settings.gamepath + "/northstar.zip"); - saveSettings(); - win.webContents.send("newpath", settings.gamepath); - ipcMain.emit("newpath", null, settings.gamepath); + setGamepath(data_array[0] + "/steamapps/common/Titanfall2") saveSettings(); cli.exit(); - // Return 1 if success - return 1; + return true; } } } @@ -155,13 +169,13 @@ function setpath(win) { case "win32": if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") - if (readvdf(data.toString()) == 1) {return} + if (readvdf(data.toString())) {return} } break; case "linux": if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { let data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") - if (readvdf(data.toString()) == 1) {return} + if (readvdf(data.toString())) {return} } break; } -- cgit v1.2.3 From baeae1fa21bf097df164ca48a1bfc3cd58967ac0 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sat, 22 Jan 2022 23:25:13 +0100 Subject: fixed checking for linux instead of win32 --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 50ad240..93326d5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -131,7 +131,7 @@ async function setpath(win) { // Autodetect path // Windows only using powershell and windows registery // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ - if (process.platform == "linux") { + if (process.platform == "win32") { try { let {stdout} = await exec("Get-Item -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\", {"shell":"powershell.exe"}); let originPath = stdout.split('\n') -- cgit v1.2.3 From a6dae7791e2f1a82e9f37e6a41961d77698c9a77 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sat, 22 Jan 2022 23:48:51 +0100 Subject: fixed setGamepath(folder) --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/utils.js b/src/utils.js index 93326d5..cf74a59 100644 --- a/src/utils.js +++ b/src/utils.js @@ -121,7 +121,7 @@ async function setpath(win) { settings.gamepath = cli.param("setpath"); } else { // GUI function setGamepath(folder) { - settings.gamepath = folder + "/steamapps/common/Titanfall2/"; + settings.gamepath = folder; settings.zip = path.join(settings.gamepath + "/northstar.zip"); saveSettings(); win.webContents.send("newpath", settings.gamepath); -- cgit v1.2.3 From 4f8884ea68c1360a7b0adec2386c60c7dc65c281 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sun, 23 Jan 2022 01:56:27 +0100 Subject: moved modules into extras/ I moved requests.js into extras and made the function for finding the potentional gamepath into it's own module. I also made the exec() called Promise based. --- src/extras/findgame.js | 64 +++++++++++++++++++++ src/extras/requests.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 2 +- src/requests.js | 153 ------------------------------------------------- src/utils.js | 74 ++++++------------------ 5 files changed, 234 insertions(+), 212 deletions(-) create mode 100644 src/extras/findgame.js create mode 100644 src/extras/requests.js delete mode 100644 src/requests.js (limited to 'src') diff --git a/src/extras/findgame.js b/src/extras/findgame.js new file mode 100644 index 0000000..029914a --- /dev/null +++ b/src/extras/findgame.js @@ -0,0 +1,64 @@ +const fs = require("fs"); +const path = require("path"); +const vdf = require("simple-vdf"); + +const util = require("util"); +const exec = util.promisify(require("child_process").exec); + +module.exports = () => { + let gamepath = ""; + + // Autodetect path + // Windows only using powershell and windows registery + // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ + if (process.platform == "win32") { + try { + exec("Get-Item -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\", {"shell":"powershell.exe"}, (err, stdout) => { + gamepath = stdout.split('\n') + .filter(r => r.indexOf("Install Dir") !== -1)[0] + .replace(/\s+/g,' ') + .trim() + .replace("Install Dir : ",""); + }); + + if (gamepath) {return gamepath} + } catch (err) {} + } + + // Detect using Steam VDF + function readvdf(data) { + // Parse read_data + data = vdf.parse(data); + + //data['libraryfolders'] + // `.length - 1` This is because the last value is `contentstatsid` + for (let pathIterator = 0; pathIterator < Object.values(data["libraryfolders"]).length - 1; pathIterator++) { + let data_array = Object.values(data["libraryfolders"][pathIterator]) + + if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { + return data_array[0] + "/steamapps/common/Titanfall2"; + } + } + } + + switch (process.platform) { + case "win32": + if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { + let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") + if (readvdf(data.toString())) {return data.toString()} + } + break; + case "linux": + if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { + let data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") + if (readvdf(data.toString())) {return data.toString()} + } + break; + } + + if (gamepath) { + return gamepath; + } else { + return false; + } +} diff --git a/src/extras/requests.js b/src/extras/requests.js new file mode 100644 index 0000000..16b1330 --- /dev/null +++ b/src/extras/requests.js @@ -0,0 +1,153 @@ +const { app } = require("electron"); +const path = require("path"); +const fs = require("fs"); +const { https } = require("follow-redirects"); + + +// all requests results are stored in this file +const cachePath = path.join(app.getPath("cache"), "requests.json"); +const NORTHSTAR_LATEST_RELEASE_KEY = "nsLatestRelease"; +const NORTHSTAR_RELEASE_NOTES_KEY = "nsReleaseNotes"; +const VIPER_RELEASE_NOTES_KEY = "vpReleaseNotes"; + + +function _saveCache(data) { + fs.writeFileSync(cachePath, JSON.stringify(data)); +} + +function _getRequestsCache() { + if (fs.existsSync(cachePath)) { + return JSON.parse(fs.readFileSync(cachePath, "utf8")); + } else { + fs.writeFileSync(cachePath, "{}"); + return {}; + } +} + +// Returns latest Northstar version available from GitHub releases. If +// there's no cache result for this request, or if cache exists but is +// old, refreshes cache with new data. +async function getLatestNsVersion() { + return new Promise((resolve, reject) => { + let cache = _getRequestsCache(); + + if (cache[NORTHSTAR_LATEST_RELEASE_KEY] && (Date.now() - cache[NORTHSTAR_LATEST_RELEASE_KEY]["time"]) < 5 * 60 * 1000) { + resolve( cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"]["tag_name"] ); + } else { + https.get({ + host: "api.github.com", + port: 443, + path: "/repos/R2Northstar/Northstar/releases/latest", + method: "GET", + headers: { "User-Agent": "viper" } + }, + + response => { + response.setEncoding("utf8"); + let responseData = ""; + + response.on("data", data => { + responseData += data; + }); + + response.on("end", _ => { + cache[NORTHSTAR_LATEST_RELEASE_KEY] = { + "time": Date.now(), + "body": JSON.parse(responseData) + }; + _saveCache(cache); + resolve( cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"]["tag_name"] ); + }); + }); + } + }); +} + +// Returns the download link to latest Northstar version. Should always +// be called after getLatestNsVersion, as it refreshes cache data (if +// needed). +function getLatestNsVersionLink() { + const cache = _getRequestsCache(); + return cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"].assets[0].browser_download_url; +} + +// Returns and caches the Northstar release notes +async function getNsReleaseNotes() { + return new Promise(resolve => { + let cache = _getRequestsCache(); + + if (cache[NORTHSTAR_RELEASE_NOTES_KEY] && (Date.now() - cache[NORTHSTAR_RELEASE_NOTES_KEY]["time"]) < 5 * 60 * 1000) { + resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); + } else { + https.get({ + host: "api.github.com", + port: 443, + path: "/repos/R2Northstar/Northstar/releases", + method: "GET", + headers: { "User-Agent": "viper" } + }, + + response => { + response.setEncoding("utf8"); + let responseData = ""; + + response.on("data", data => { + responseData += data; + }); + + response.on("end", _ => { + cache[NORTHSTAR_RELEASE_NOTES_KEY] = { + "time": Date.now(), + "body": JSON.parse(responseData) + }; + _saveCache(cache); + resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); + }); + }); + } + }); +} + +// Returns and caches the Viper release notes +async function getVpReleaseNotes() { + return new Promise(resolve => { + let cache = _getRequestsCache(); + + if (cache[VIPER_RELEASE_NOTES_KEY] && (Date.now() - cache[VIPER_RELEASE_NOTES_KEY]["time"]) < 5 * 60 * 1000) { + resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); + } else { + https.get({ + host: "api.github.com", + port: 443, + path: "/repos/0negal/viper/releases", + method: "GET", + headers: { "User-Agent": "viper" } + }, + + response => { + response.setEncoding("utf8"); + let responseData = ""; + + response.on("data", data => { + responseData += data; + }); + + response.on("end", _ => { + cache[VIPER_RELEASE_NOTES_KEY] = { + "time": Date.now(), + "body": JSON.parse(responseData) + }; + _saveCache(cache); + resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); + }); + }); + } + }); +} + +module.exports = { + getLatestNsVersion, + getLatestNsVersionLink, + getNsReleaseNotes, + getVpReleaseNotes +}; diff --git a/src/index.js b/src/index.js index 3f50840..f4207e7 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,7 @@ const events = new Emitter(); const utils = require("./utils"); const cli = require("./cli"); -const requests = require("./requests"); +const requests = require("./extras/requests"); // Starts the actual BrowserWindow, which is only run when using the // GUI, for the CLI this function is never called. diff --git a/src/requests.js b/src/requests.js deleted file mode 100644 index 16b1330..0000000 --- a/src/requests.js +++ /dev/null @@ -1,153 +0,0 @@ -const { app } = require("electron"); -const path = require("path"); -const fs = require("fs"); -const { https } = require("follow-redirects"); - - -// all requests results are stored in this file -const cachePath = path.join(app.getPath("cache"), "requests.json"); -const NORTHSTAR_LATEST_RELEASE_KEY = "nsLatestRelease"; -const NORTHSTAR_RELEASE_NOTES_KEY = "nsReleaseNotes"; -const VIPER_RELEASE_NOTES_KEY = "vpReleaseNotes"; - - -function _saveCache(data) { - fs.writeFileSync(cachePath, JSON.stringify(data)); -} - -function _getRequestsCache() { - if (fs.existsSync(cachePath)) { - return JSON.parse(fs.readFileSync(cachePath, "utf8")); - } else { - fs.writeFileSync(cachePath, "{}"); - return {}; - } -} - -// Returns latest Northstar version available from GitHub releases. If -// there's no cache result for this request, or if cache exists but is -// old, refreshes cache with new data. -async function getLatestNsVersion() { - return new Promise((resolve, reject) => { - let cache = _getRequestsCache(); - - if (cache[NORTHSTAR_LATEST_RELEASE_KEY] && (Date.now() - cache[NORTHSTAR_LATEST_RELEASE_KEY]["time"]) < 5 * 60 * 1000) { - resolve( cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"]["tag_name"] ); - } else { - https.get({ - host: "api.github.com", - port: 443, - path: "/repos/R2Northstar/Northstar/releases/latest", - method: "GET", - headers: { "User-Agent": "viper" } - }, - - response => { - response.setEncoding("utf8"); - let responseData = ""; - - response.on("data", data => { - responseData += data; - }); - - response.on("end", _ => { - cache[NORTHSTAR_LATEST_RELEASE_KEY] = { - "time": Date.now(), - "body": JSON.parse(responseData) - }; - _saveCache(cache); - resolve( cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"]["tag_name"] ); - }); - }); - } - }); -} - -// Returns the download link to latest Northstar version. Should always -// be called after getLatestNsVersion, as it refreshes cache data (if -// needed). -function getLatestNsVersionLink() { - const cache = _getRequestsCache(); - return cache[NORTHSTAR_LATEST_RELEASE_KEY]["body"].assets[0].browser_download_url; -} - -// Returns and caches the Northstar release notes -async function getNsReleaseNotes() { - return new Promise(resolve => { - let cache = _getRequestsCache(); - - if (cache[NORTHSTAR_RELEASE_NOTES_KEY] && (Date.now() - cache[NORTHSTAR_RELEASE_NOTES_KEY]["time"]) < 5 * 60 * 1000) { - resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); - } else { - https.get({ - host: "api.github.com", - port: 443, - path: "/repos/R2Northstar/Northstar/releases", - method: "GET", - headers: { "User-Agent": "viper" } - }, - - response => { - response.setEncoding("utf8"); - let responseData = ""; - - response.on("data", data => { - responseData += data; - }); - - response.on("end", _ => { - cache[NORTHSTAR_RELEASE_NOTES_KEY] = { - "time": Date.now(), - "body": JSON.parse(responseData) - }; - _saveCache(cache); - resolve( cache[NORTHSTAR_RELEASE_NOTES_KEY]["body"] ); - }); - }); - } - }); -} - -// Returns and caches the Viper release notes -async function getVpReleaseNotes() { - return new Promise(resolve => { - let cache = _getRequestsCache(); - - if (cache[VIPER_RELEASE_NOTES_KEY] && (Date.now() - cache[VIPER_RELEASE_NOTES_KEY]["time"]) < 5 * 60 * 1000) { - resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); - } else { - https.get({ - host: "api.github.com", - port: 443, - path: "/repos/0negal/viper/releases", - method: "GET", - headers: { "User-Agent": "viper" } - }, - - response => { - response.setEncoding("utf8"); - let responseData = ""; - - response.on("data", data => { - responseData += data; - }); - - response.on("end", _ => { - cache[VIPER_RELEASE_NOTES_KEY] = { - "time": Date.now(), - "body": JSON.parse(responseData) - }; - _saveCache(cache); - resolve( cache[VIPER_RELEASE_NOTES_KEY]["body"] ); - }); - }); - } - }); -} - -module.exports = { - getLatestNsVersion, - getLatestNsVersionLink, - getNsReleaseNotes, - getVpReleaseNotes -}; diff --git a/src/utils.js b/src/utils.js index cf74a59..bc0f66c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,9 +8,9 @@ const events = new Emitter(); const cli = require("./cli"); const lang = require("./lang"); -const requests = require("./requests"); +const requests = require("./extras/requests"); +const findgame = require("./extras/findgame"); -const vdf = require("simple-vdf"); const unzip = require("unzipper"); const run = require("child_process").spawn; const exec = require("child_process").exec; @@ -117,8 +117,16 @@ northstar_auto_updates: { // If running with CLI it takes in the --setpath argument otherwise it // open the systems file browser for the user to select a path. async function setpath(win) { + function setGamepath(folder) { + settings.gamepath = folder; + settings.zip = path.join(settings.gamepath + "/northstar.zip"); + saveSettings(); + win.webContents.send("newpath", settings.gamepath); + ipcMain.emit("newpath", null, settings.gamepath); + } + if (! win) { // CLI - settings.gamepath = cli.param("setpath"); + setGamepath(cli.param("setpath")); } else { // GUI function setGamepath(folder) { settings.gamepath = folder; @@ -128,56 +136,10 @@ async function setpath(win) { ipcMain.emit("newpath", null, settings.gamepath); } - // Autodetect path - // Windows only using powershell and windows registery - // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ - if (process.platform == "win32") { - try { - let {stdout} = await exec("Get-Item -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\", {"shell":"powershell.exe"}); - let originPath = stdout.split('\n') - .filter(r => r.indexOf("Install Dir") !== -1)[0] - .replace(/\s+/g,' ') - .trim() - .replace("Install Dir : ",""); - setGamepath(originPath) - return - } catch (err) {} - } - - // Detect using Steam VDF - function readvdf(data) { - // Parse read_data - data = vdf.parse(data); - - //data['libraryfolders'] - // `.length - 1` This is because the last value is `contentstatsid` - for (let pathIterator = 0; pathIterator < Object.values(data["libraryfolders"]).length - 1; pathIterator++) { - let data_array = Object.values(data["libraryfolders"][pathIterator]) - - if (fs.existsSync(data_array[0] + "/steamapps/common/Titanfall2/Titanfall2.exe")) { - // Found the location - setGamepath(data_array[0] + "/steamapps/common/Titanfall2") - - saveSettings(); - cli.exit(); - return true; - } - } - } - - switch (process.platform) { - case "win32": - if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { - let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") - if (readvdf(data.toString())) {return} - } - break; - case "linux": - if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { - let data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") - if (readvdf(data.toString())) {return} - } - break; + let gamepath = findgame(); + if (findgame()) { + setGamepath(gamepath); + return; } // Fallback to manual selection @@ -191,11 +153,7 @@ async function setpath(win) { return; } - settings.gamepath = res.filePaths[0]; - settings.zip = path.join(settings.gamepath + "/northstar.zip"); - saveSettings(); - win.webContents.send("newpath", settings.gamepath); - ipcMain.emit("newpath", null, settings.gamepath); + setGamepath(res.filePaths[0]) cli.exit(); return; -- cgit v1.2.3 From 77c03b45a5dd6861f23043221315c3016b615753 Mon Sep 17 00:00:00 2001 From: 3top1a <3top1a.official@gmail.com> Date: Sun, 23 Jan 2022 11:43:28 +0100 Subject: Repaired vdf reading - os nor app was imported - returned the entire vdf file instead --- src/extras/findgame.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/extras/findgame.js b/src/extras/findgame.js index 029914a..426d2e3 100644 --- a/src/extras/findgame.js +++ b/src/extras/findgame.js @@ -1,6 +1,7 @@ const fs = require("fs"); const path = require("path"); const vdf = require("simple-vdf"); +const { app } = require("electron"); const util = require("util"); const exec = util.promisify(require("child_process").exec); @@ -30,7 +31,6 @@ module.exports = () => { // Parse read_data data = vdf.parse(data); - //data['libraryfolders'] // `.length - 1` This is because the last value is `contentstatsid` for (let pathIterator = 0; pathIterator < Object.values(data["libraryfolders"]).length - 1; pathIterator++) { let data_array = Object.values(data["libraryfolders"][pathIterator]) @@ -45,13 +45,15 @@ module.exports = () => { case "win32": if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") - if (readvdf(data.toString())) {return data.toString()} + let read_vdf = readvdf(data.toString()) + if (read_vdf ) {return read_vdf} } break; case "linux": if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { - let data = fs.readFileSync(os.homedir() + "/.steam/steam/steamapps/libraryfolders.vdf") - if (readvdf(data.toString())) {return data.toString()} + let data = fs.readFileSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf")) + let read_vdf = readvdf(data.toString()) + if (read_vdf ) {return read_vdf} } break; } -- cgit v1.2.3 From c92db733034a549e53ebf2c83aaea828ca61697a Mon Sep 17 00:00:00 2001 From: Rémy Raes Date: Sun, 23 Jan 2022 20:05:37 +0100 Subject: fix: Finding Windows game folder (#55) --- src/extras/findgame.js | 16 ++++++++-------- src/utils.js | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/extras/findgame.js b/src/extras/findgame.js index 426d2e3..f2d7be1 100644 --- a/src/extras/findgame.js +++ b/src/extras/findgame.js @@ -6,7 +6,7 @@ const { app } = require("electron"); const util = require("util"); const exec = util.promisify(require("child_process").exec); -module.exports = () => { +module.exports = async () => { let gamepath = ""; // Autodetect path @@ -14,13 +14,13 @@ module.exports = () => { // Get-Item -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Respawn\Titanfall2\ if (process.platform == "win32") { try { - exec("Get-Item -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\", {"shell":"powershell.exe"}, (err, stdout) => { - gamepath = stdout.split('\n') - .filter(r => r.indexOf("Install Dir") !== -1)[0] - .replace(/\s+/g,' ') - .trim() - .replace("Install Dir : ",""); - }); + const {stdout} = await exec("Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Respawn\\Titanfall2\\ -Name \"Install Dir\"", {"shell":"powershell.exe"}); + + const gamepath = stdout.split('\n') + .filter(r => r.indexOf("Install Dir") !== -1)[0] + .replace(/\s+/g,' ') + .trim() + .replace("Install Dir : ",""); if (gamepath) {return gamepath} } catch (err) {} diff --git a/src/utils.js b/src/utils.js index bc0f66c..a2c61d4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -136,8 +136,8 @@ async function setpath(win) { ipcMain.emit("newpath", null, settings.gamepath); } - let gamepath = findgame(); - if (findgame()) { + let gamepath = await findgame(); + if (gamepath) { setGamepath(gamepath); return; } -- cgit v1.2.3 From 1362df77adf25a941cc31d3a82378f6911728f10 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Sun, 23 Jan 2022 21:20:43 +0100 Subject: simplify VDF code --- src/extras/findgame.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/extras/findgame.js b/src/extras/findgame.js index f2d7be1..f7da1c0 100644 --- a/src/extras/findgame.js +++ b/src/extras/findgame.js @@ -41,21 +41,20 @@ module.exports = async () => { } } + let folder = ""; switch (process.platform) { case "win32": - if (fs.existsSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf")) { - let data = fs.readFileSync("C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf") - let read_vdf = readvdf(data.toString()) - if (read_vdf ) {return read_vdf} - } - break; + folder = "C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf"; + break case "linux": - if (fs.existsSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"))) { - let data = fs.readFileSync(path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf")) - let read_vdf = readvdf(data.toString()) - if (read_vdf ) {return read_vdf} - } - break; + folder = path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"); + break + } + + if (fs.existsSync(folder)) { + let data = fs.readFileSync(folder) + let read_vdf = readvdf(data.toString()) + if (read_vdf ) {return read_vdf} } if (gamepath) { -- cgit v1.2.3 From 18d1166b6b8beef422590607cd02d613dd02d2d0 Mon Sep 17 00:00:00 2001 From: 3top1a <57371001+3top1a@users.noreply.github.com> Date: Mon, 24 Jan 2022 14:27:29 +0100 Subject: Edge case in vdf reading (#56) * Edge case * Alert user only when automatic detection failed * extra info on not found message * support for [Free/Open]BSD Co-authored-by: 0neGal --- .github/FUNDING.yml | 2 +- src/app/main.js | 1 - src/extras/findgame.js | 6 ++++-- src/lang/en.json | 2 +- src/utils.js | 2 ++ 5 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 96f466a..8991fb3 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ liberapay: 0neGal -custom: ["https://github.com/R2Northstar"] +custom: ["github.com/R2Northstar"] diff --git a/src/app/main.js b/src/app/main.js index 5f0cc9a..dab584e 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -32,7 +32,6 @@ if (fs.existsSync("viper.json")) { setpath(true); } } else { - alert(lang("general.missingpath")); setpath(); } diff --git a/src/extras/findgame.js b/src/extras/findgame.js index f7da1c0..42c9b85 100644 --- a/src/extras/findgame.js +++ b/src/extras/findgame.js @@ -41,17 +41,19 @@ module.exports = async () => { } } - let folder = ""; + let folder = null; switch (process.platform) { case "win32": folder = "C:\\Program Files (x86)\\Steam\\steamapps\\libraryfolders.vdf"; break case "linux": + case "openbsd": + case "freebsd": folder = path.join(app.getPath("home"), "/.steam/steam/steamapps/libraryfolders.vdf"); break } - if (fs.existsSync(folder)) { + if (fs.existsSync(folder) && folder) { let data = fs.readFileSync(folder) let read_vdf = readvdf(data.toString()) if (read_vdf ) {return read_vdf} diff --git a/src/lang/en.json b/src/lang/en.json index c2b6f98..8d4daeb 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -96,7 +96,7 @@ "general.mods.enabled": "Enabled mods:", "general.mods.disabled": "Disabled mods:", "general.mods.installed": "Installed mods:", - "general.missingpath": "Game path is not set!", + "general.missingpath": "Game location could not be found automatically! Please select it manually!", "general.notinstalled": "Northstar is not installed!", "general.launching": "Launching" } diff --git a/src/utils.js b/src/utils.js index a2c61d4..ef647fe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -142,6 +142,8 @@ async function setpath(win) { return; } + alert(lang("general.missingpath")); + // Fallback to manual selection dialog.showOpenDialog({properties: ["openDirectory"]}).then(res => { if (res.canceled) { -- cgit v1.2.3 From 5566e6c1cd572c18944f651083e6f30b8e7a4354 Mon Sep 17 00:00:00 2001 From: Remy Raes Date: Mon, 24 Jan 2022 15:38:17 +0100 Subject: [fix] update general.missingpath French translation --- src/lang/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lang/fr.json b/src/lang/fr.json index 59a3090..1ba7acf 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -96,7 +96,7 @@ "general.mods.enabled": "Mods activés :", "general.mods.disabled": "Mods désactivés :", "general.mods.installed": "Mods installés :", - "general.missingpath": "Le chemin du client n'est pas défini !", + "general.missingpath": "Le chemin du client n'a pu être trouvé automatiquement, merci de le sélectionner manuellement.", "general.notinstalled": "Northstar n'est pas installé !", "general.launching": "Lancement" } -- cgit v1.2.3 From 9c6ac295189acab7303952725755dfac87e18470 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Mon, 24 Jan 2022 18:21:13 +0100 Subject: no alert when manually changing the game path When changing the game path by clicking the button you shouldn't be told it can't find the game and you've to select one manually, as you know that already. More importantly, if it could be found automatically it'll just not do anything. With this change I also changed the "gui.setpath" string to be more logical, and to make it clear what it does. --- src/app/main.js | 1 - src/index.js | 10 +++++++--- src/lang/en.json | 2 +- src/utils.js | 30 ++++++++++++++++-------------- 4 files changed, 24 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/app/main.js b/src/app/main.js index dab584e..a3a1d04 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -26,7 +26,6 @@ if (fs.existsSync("viper.json")) { settings.zip = path.join(settings.gamepath + "/northstar.zip"); if (settings.gamepath.length === 0) { - alert(lang("general.missingpath")); setpath(false); } else { setpath(true); diff --git a/src/index.js b/src/index.js index f4207e7..c8bfc53 100644 --- a/src/index.js +++ b/src/index.js @@ -87,9 +87,13 @@ ipcMain.on("launchVanilla", (event) => {utils.launch("vanilla")}) ipcMain.on("update", (event) => {utils.update()}) ipcMain.on("setpathcli", (event) => {utils.setpath()}); ipcMain.on("setpath", (event, value) => { - if (!value) { - utils.setpath(win); - } else if (!win.isVisible()) { + if (! value) { + if (! win.isVisible()) { + utils.setpath(win); + } else { + utils.setpath(win, true); + } + } else if (! win.isVisible()) { win.show(); } }); diff --git a/src/lang/en.json b/src/lang/en.json index 8d4daeb..fe9b0b0 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -42,7 +42,7 @@ "gui.versions.northstar": "Northstar version", "gui.exit": "Exit", "gui.update": "Update", - "gui.setpath": "Game Path", + "gui.setpath": "Change Game Path", "gui.update.check": "Check for updates", "gui.mods": "Mods", diff --git a/src/utils.js b/src/utils.js index ef647fe..fb94b92 100644 --- a/src/utils.js +++ b/src/utils.js @@ -116,7 +116,7 @@ northstar_auto_updates: { // // If running with CLI it takes in the --setpath argument otherwise it // open the systems file browser for the user to select a path. -async function setpath(win) { +async function setpath(win, forcedialog) { function setGamepath(folder) { settings.gamepath = folder; settings.zip = path.join(settings.gamepath + "/northstar.zip"); @@ -128,21 +128,23 @@ async function setpath(win) { if (! win) { // CLI setGamepath(cli.param("setpath")); } else { // GUI - function setGamepath(folder) { - settings.gamepath = folder; - settings.zip = path.join(settings.gamepath + "/northstar.zip"); - saveSettings(); - win.webContents.send("newpath", settings.gamepath); - ipcMain.emit("newpath", null, settings.gamepath); - } + if (! forcedialog) { + function setGamepath(folder, forcedialog) { + settings.gamepath = folder; + settings.zip = path.join(settings.gamepath + "/northstar.zip"); + saveSettings(); + win.webContents.send("newpath", settings.gamepath); + ipcMain.emit("newpath", null, settings.gamepath); + } - let gamepath = await findgame(); - if (gamepath) { - setGamepath(gamepath); - return; - } + let gamepath = await findgame(); + if (gamepath) { + setGamepath(gamepath); + return; + } - alert(lang("general.missingpath")); + winAlert(lang("general.missingpath")); + } // Fallback to manual selection dialog.showOpenDialog({properties: ["openDirectory"]}).then(res => { -- cgit v1.2.3 From 61f9bc80b33ff4a3d9397640db7ca82f7bb79861 Mon Sep 17 00:00:00 2001 From: 0neGal Date: Tue, 25 Jan 2022 22:08:23 +0100 Subject: changed French localization, remove .vscode/ --- .vscode/launch.json | 17 ----------------- src/lang/fr.json | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 .vscode/launch.json (limited to 'src') diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index ef3f9b3..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "args" : ["."], - "outputCapture": "std" - } - ] - } \ No newline at end of file diff --git a/src/lang/fr.json b/src/lang/fr.json index 1ba7acf..2fbc89a 100644 --- a/src/lang/fr.json +++ b/src/lang/fr.json @@ -42,7 +42,7 @@ "gui.versions.northstar": "Version de Northstar", "gui.exit": "Fermer", "gui.update": "Mise à jour", - "gui.setpath": "Chemin du jeu", + "gui.setpath": "Mettre à jour le chemin du jeu", "gui.update.check": "Vérifier les mises à jour", "gui.mods": "Mods", -- cgit v1.2.3 From 6a08872d134f3afa3f81b92fabe51bdbd91f375d Mon Sep 17 00:00:00 2001 From: 0neGal Date: Wed, 26 Jan 2022 00:20:25 +0100 Subject: fixed mods not updating after changing path This also fixes mods not showing up after first install of NS --- src/app/main.js | 3 +++ src/utils.js | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/app/main.js b/src/app/main.js index a3a1d04..19f6c9e 100644 --- a/src/app/main.js +++ b/src/app/main.js @@ -154,6 +154,7 @@ function installmod() { // Frontend part of settings a new game path ipcRenderer.on("newpath", (event, newpath) => { settings.gamepath = newpath; + ipcRenderer.send("guigetmods"); }) // Continuation of log() @@ -162,6 +163,8 @@ ipcRenderer.on("alert", (event, msg) => {alert(msg)}) // Updates the installed mods ipcRenderer.on("mods", (event, mods) => { + if (! mods) {return} + modcount.innerHTML = `${lang("gui.mods.count")} ${mods.all.length}`; modsdiv.innerHTML = ""; diff --git a/src/utils.js b/src/utils.js index fb94b92..0507921 100644 --- a/src/utils.js +++ b/src/utils.js @@ -339,7 +339,6 @@ function winAlert(msg) { // // We can both get list of disabled mods, remove/install/toggle mods and // other things akin to that, all kinds of mod related stuff -let modpath = path.join(settings.gamepath, "R2Northstar/mods"); const mods = { // Returns a list of mods // @@ -347,6 +346,8 @@ const mods = { // combination of the other two, enabled being enabled mods, and you // guessed it, disabled being disabled mods. list: () => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + if (getNSVersion() == "unknown") { winLog(lang("general.notinstalled")) console.log("error: " + lang("general.notinstalled")) @@ -413,6 +414,8 @@ const mods = { // the absolute basics will be provided and we can't know the // version or similar. get: (mod) => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + if (getNSVersion() == "unknown") { winLog(lang("general.notinstalled")) console.log("error: " + lang("general.notinstalled")) @@ -437,6 +440,8 @@ const mods = { // inside the zip or folder to see if buried in another folder or // not, as sometimes that's the case. install: (mod) => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + if (getNSVersion() == "unknown") { winLog(lang("general.notinstalled")) console.log("error: " + lang("general.notinstalled")) @@ -515,6 +520,8 @@ const mods = { // Takes in the names of the mod then removes it, no confirmation, // that'd be up to the GUI. remove: (mod) => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + if (getNSVersion() == "unknown") { winLog(lang("general.notinstalled")) console.log("error: " + lang("general.notinstalled")) @@ -565,6 +572,8 @@ const mods = { // you checked for if a mod is already disable and if not run the // function. However we currently have no need for that. toggle: (mod, fork) => { + let modpath = path.join(settings.gamepath, "R2Northstar/mods"); + if (getNSVersion() == "unknown") { winLog(lang("general.notinstalled")) console.log("error: " + lang("general.notinstalled")) -- cgit v1.2.3