Pendrokar's picture
xVASynth v3 code for English
19c8b95
raw
history blame
38.9 kB
"use strict"
window.nexusModelsList = []
window.endorsedRepos = new Set()
window.nexusState = {
key: null,
applicationSlug: "xvasynth",
socket: null,
uuid: null,
token: null,
downloadQueue: [],
installQueue: [],
finished: 0,
repoLinks: [],
primaryColumnSort: "game",
columnSortModifier: -1
}
// TEMP, maybe move to utils
// ==========================
const http = require("http")
const https = require("https")
// Utility for printing out in the dev console all the numerical game IDs on the Nexus
window.nexusGameIdToGameName = {}
window.getAllNexusGameIDs = (gameName) => {
return new Promise((resolve) => {
getData("", undefined, "GET").then(results => {
results = gameName ? results.filter(x=>x.name.toLowerCase().includes(gameName)) : results
resolve(results)
})
})
}
window.mod_search_nexus = (game_id, query) => {
return new Promise(resolve => {
doFetch(`https://search.nexusmods.com/mods/?game_id=${game_id}&terms=${encodeURI(query.split(' ').toString())}&include_adult=true`)
.then(r=>r.text())
.then(r => {
try {
const data = JSON.parse(r).results.map(res => {
return {
downloads: res.downloads,
endorsements: res.endorsements,
game_id: res.game_id,
name: res.name,
author: res.username,
url: `https://www.nexusmods.com/${res.game_name}/mods/${res.mod_id}`
}
})
resolve([r.total, data])
} catch (e) {
window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
}
})
})
}
window.nexusDownload = (url, dest) => {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest)
const request = https.get(url.replace("http:", "https:"), (response) => {
// check if response is success
if (response.statusCode !== 200) {
console.log("url", url)
console.log("Response status was " + response.statusCode, response)
resolve()
return
}
response.pipe(file)
})
file.on("finish", () => {
file.close()
resolve()
})
// check for request error too
request.on("error", (err) => {
fs.unlink(dest)
return reject(err.message)
})
file.on("error", (err) => { // Handle errors
fs.unlink(dest) // Delete the file async. (But we don't check the result)
return reject(err.message)
})
})
}
window.initNexus = () => {
const data = fs.readFileSync(`${window.path}/repositories.json`, "utf8")
window.nexusReposList = JSON.parse(data)
return new Promise((resolve) => {
window.nexusState.key = localStorage.getItem("nexus_API_key")
if (window.nexusState.key) {
window.showUserName()
nexusLogInButton.innerHTML = window.i18n.LOG_OUT
resolve()
} else {
try {
window.nexusState.socket = new WebSocket("wss://sso.nexusmods.com")
} catch (e) {
console.log(e)
}
window.nexusState.socket.onclose = event => {
console.log("socket closed")
if (!window.nexusState.key) {
setTimeout(() => {window.initNexus()}, 5000)
}
}
window.nexusState.socket.onmessage = event => {
const response = JSON.parse(event.data)
if (response && response.success) {
if (response.data.hasOwnProperty('connection_token')) {
localStorage.setItem('connection_token', response.data.connection_token)
} else if (response.data.hasOwnProperty('api_key')) {
console.log("API Key Received: " + response.data.api_key)
window.nexusState.key = response.data.api_key
localStorage.setItem('uuid', window.nexusState.uuid)
localStorage.setItem('nexus_API_key', window.nexusState.key)
window.showUserName()
window.pluginsManager.updateUI()
closeModal(undefined, nexusContainer)
nexusLogInButton.innerHTML = window.i18n.LOG_OUT
resolve()
}
} else {
window.errorModal(`${window.i18n.ERR_LOGGING_INTO_NEXUS}: ${response.error}`)
reject()
}
}
}
})
}
nexusLogInButton.addEventListener("click", () => {
if (nexusLogInButton.innerHTML==window.i18n.LOG_IN) {
window.spinnerModal(window.i18n.LOGGING_INTO_NEXUS)
window.nexusState.uuid = localStorage.getItem("uuid")
window.nexusState.token = localStorage.getItem("connection_token")
if (window.nexusState.uuid==null) {
window.nexusState.uuid = uuidv4()
}
const data = {
id: window.nexusState.uuid,
token: window.nexusState.token,
protocol: 2
}
window.nexusState.socket.send(JSON.stringify(data))
shell.openExternal(`https://www.nexusmods.com/sso?id=${window.nexusState.uuid}&application=${window.nexusState.applicationSlug}`)
} else {
nexusNameDisplayContainer.style.opacity = 0
localStorage.removeItem("nexus_API_key")
localStorage.removeItem("uuid")
localStorage.removeItem("connection_token")
nexusAvatar.innerHTML = ""
nexusUserName.innerHTML = ""
nexusLogInButton.innerHTML = window.i18n.LOG_IN
window.nexusState.uuid = null
window.nexusState.key = null
window.pluginsManager.updateUI()
}
})
window.downloadFile = ([nexusGameId, nexusRepoId, outputFileName, fileId]) => {
nexusDownloadLog.appendChild(createElem("div", `Downloading: ${outputFileName}`))
return new Promise(async (resolve, reject) => {
if (!fs.existsSync(`${window.path}/downloads`)) {
fs.mkdirSync(`${window.path}/downloads`)
}
const downloadLink = await getData(`${nexusGameId}/mods/${nexusRepoId}/files/${fileId}/download_link.json`)
if (!downloadLink.length && downloadLink.code==403) {
window.errorModal(`${window.i18n.NEXUS_PREMIUM}<br><br>${window.i18n.NEXUS_ORIG_ERR}:<br>${downloadLink.message}`).then(() => {
const queueIndex = window.nexusState.downloadQueue.findIndex(it => it[1]==fileId)
window.nexusState.downloadQueue.splice(queueIndex, 1)
nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
nexusDownloadLog.appendChild(createElem("div", `${window.i18n.FAILED_DOWNLOAD}: ${outputFileName}`))
reject()
})
} else {
await window.nexusDownload(downloadLink[0].URI.replace("https", "http"), `${window.path}/downloads/${outputFileName}.zip`)
const queueIndex = window.nexusState.downloadQueue.findIndex(it => it[1]==fileId)
window.nexusState.downloadQueue.splice(queueIndex, 1)
nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
resolve()
}
})
}
window.installDownloadedModel = ([game, zipName]) => {
nexusDownloadLog.appendChild(createElem("div", `${window.i18n.INSTALLING} ${zipName}`))
return new Promise(resolve => {
try {
const modelsFolder = window.userSettings[`modelspath_${game}`]
const unzipper = require('unzipper')
const zipPath = `${window.path}/downloads/${zipName}.zip`
if (!fs.existsSync(modelsFolder)) {
fs.mkdirSync(modelsFolder)
}
if (!fs.existsSync(`${window.path}/downloads`)) {
fs.mkdirSync(`${window.path}/downloads`)
}
fs.createReadStream(zipPath).pipe(unzipper.Parse()).on("entry", entry => {
const fileName = entry.path
const dirOrFile = entry.type
if (/\/$/.test(fileName)) { // It's a directory
return
}
let fileContainerFolderPath = fileName.split("/").reverse()
const justFileName = fileContainerFolderPath[0]
entry.pipe(fs.createWriteStream(`${modelsFolder}/${justFileName}`))
})
.promise()
.then(() => {
window.appLogger.log(`${window.i18n.DONE_INSTALLING} ${zipName}`)
const queueIndex = window.nexusState.installQueue.findIndex(it => it[1]==zipName)
window.nexusState.installQueue.splice(queueIndex, 1)
nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
nexusDownloadLog.appendChild(createElem("div", `${window.i18n.FINISHED} ${zipName}`))
resolve()
}, e => {
console.log(e)
window.appLogger.log(e)
window.errorModal(e.message)
})
} catch (e) {
console.log(e)
window.appLogger.log(e)
window.errorModal(e.message)
resolve()
}
})
}
nexusDownloadAllBtn.addEventListener("click", async () => {
for (let mi=0; mi<window.nexusState.filteredDownloadableModels.length; mi++) {
const modelMeta = window.nexusState.filteredDownloadableModels[mi]
window.nexusState.downloadQueue.push([modelMeta.voiceId, modelMeta.nexus_file_id])
nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
}
for (let mi=0; mi<window.nexusState.filteredDownloadableModels.length; mi++) {
const modelMeta = window.nexusState.filteredDownloadableModels[mi]
await window.downloadFile([modelMeta.nexusGameId, modelMeta.nexusRepoId, modelMeta.voiceId, modelMeta.nexus_file_id])
// Install the downloaded voice
window.nexusState.installQueue.push([modelMeta.game, modelMeta.voiceId])
nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
await window.installDownloadedModel([modelMeta.game, modelMeta.voiceId])
fs.unlinkSync(`${window.path}/downloads/${modelMeta.voiceId}.zip`)
window.nexusState.finished += 1
nexusFinishedCount.innerHTML = window.nexusState.finished
window.displayAllModels(true)
window.loadAllModels(true).then(() => {
changeGame(window.currentGame)
})
}
})
const getJSONData = (url) => {
return new Promise(resolve => {
doFetch(url).then(r=>r.json())
})
}
window.showUserName = async () => {
const data = await getuserData("validate.json")
const img = createElem("img")
img.src = data.profile_url
img.style.height = "40px"
nexusAvatar.innerHTML = ""
img.addEventListener("load", () => {
nexusAvatar.appendChild(img)
nexusUserName.innerHTML = data.name
nexusNameDisplayContainer.style.opacity = 1
})
}
const getuserData = (url, data) => {
return new Promise(resolve => {
doFetch(`https://api.nexusmods.com/v1/users/${url}`, {
method: "GET",
headers: {
apikey: window.nexusState.key
}
})
.then(r=>r.text())
.then(r => {
try {
resolve(JSON.parse(r))
} catch (e) {
window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
}
})
.catch(err => {
console.log("err", err)
resolve()
})
})
}
const getData = (url, data, type="GET") => {
return new Promise(resolve => {
const payload = {
method: type,
headers: {
apikey: window.nexusState.key
}
}
if (type=="POST") {
const params = new URLSearchParams()
Object.keys(data).forEach(key => {
params.append(key, data[key])
})
payload.body = params
}
doFetch(`https://api.nexusmods.com/v1/games/${url}`, payload)
.then(r=>r.text())
.then(r => {
try {
resolve(JSON.parse(r))
} catch (e) {
window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
}
})
.catch(err => {
console.log("err", err)
resolve()
})
})
}
window.nexus_getData = getData
// ==========================
let hasPopulatedNexusGameListDropdown = false
window.openNexusWindow = () => {
closeModal(undefined, nexusContainer).then(() => {
nexusContainer.style.opacity = 0
nexusContainer.style.display = "flex"
requestAnimationFrame(() => requestAnimationFrame(() => nexusContainer.style.opacity = 1))
requestAnimationFrame(() => requestAnimationFrame(() => chromeBar.style.opacity = 1))
})
const gameColours = {}
Object.keys(window.gameAssets).forEach(gameId => {
const colour = window.gameAssets[gameId].themeColourPrimary
gameColours[gameId] = colour
})
nexusGamesList.innerHTML = ""
Object.keys(window.gameAssets).sort((a,b)=>a<b?-1:1).forEach(gameId => {
const gameSelectContainer = createElem("div")
const gameCheckbox = createElem(`input#ngl_${gameId}`, {type: "checkbox"})
gameCheckbox.checked = true
const gameButton = createElem("button.fixedColour")
gameButton.style.setProperty("background-color", `#${gameColours[gameId]}`, "important")
gameButton.style.display = "flex"
gameButton.style.alignItems = "center"
gameButton.style.margin = "auto"
gameButton.style.marginTop = "8px"
const buttonLabel = createElem("span", window.gameAssets[gameId].gameName)
gameButton.addEventListener("contextmenu", e => {
if (e.target==gameButton || e.target==buttonLabel) {
Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = false)
gameCheckbox.click()
window.displayAllModels()
}
})
gameButton.addEventListener("click", e => {
if (e.target==gameButton || e.target==buttonLabel) {
gameCheckbox.click()
window.displayAllModels()
}
})
gameCheckbox.addEventListener("change", () => {
window.displayAllModels()
})
gameButton.appendChild(gameCheckbox)
gameButton.appendChild(buttonLabel)
gameSelectContainer.appendChild(gameButton)
nexusGamesList.appendChild(gameSelectContainer)
})
// Populate the game IDs for the Nexus repo searching
if (!hasPopulatedNexusGameListDropdown) {
window.getAllNexusGameIDs().then(results => {
if (results && results.length) {
results = results.sort((a,b)=>a.name.toLowerCase()<b.name.toLowerCase()?-1:1)
results.forEach(res => {
window.nexusGameIdToGameName[res.id] = res.name
const opt = createElem("option", {value: res.id})
opt.innerHTML = res.name
nexusAllGamesSelect.appendChild(opt)
})
if (fs.existsSync(`${window.path}/repositories.json`)) {
const data = fs.readFileSync(`${window.path}/repositories.json`, "utf8")
window.nexusReposList = JSON.parse(data)
window.nexusUpdateModsUsedPanel()
}
hasPopulatedNexusGameListDropdown = true
}
})
}
}
window.setupModal(nexusMenuButton, nexusContainer, window.openNexusWindow)
nexusGamesListEnableAllBtn.addEventListener("click", () => {
Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = true)
window.displayAllModels()
})
nexusGamesListDisableAllBtn.addEventListener("click", () => {
Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = false)
window.displayAllModels()
})
nexusSearchBar.addEventListener("keyup", () => {
window.displayAllModels()
})
window.getLatestModelsList = async () => {
if (!nexusState.key) {
return window.errorModal(window.i18n.YOU_MUST_BE_LOGGED_IN)
} else {
try {
window.spinnerModal(window.i18n.CHECKING_NEXUS)
window.nexusModelsList = []
const idToGame = {}
Object.keys(window.gameAssets).forEach(gameId => {
const id = window.gameAssets[gameId].gameCode.toLowerCase()
idToGame[id] = gameId
})
const repoLinks = window.nexusReposList.repos.filter(r=>r.enabled).map(r=>r.url)
for (let li=0; li<repoLinks.length; li++) {
const link = repoLinks[li].replace("\r","")
const repoInfo = await getData(`${link.split(".com/")[1]}.json`)
const author = repoInfo.author
const nexusRepoId = repoInfo.mod_id
const nexusRepoVersion = repoInfo.version
const nexusGameId = repoInfo.domain_name
const files = await getData(`${link.split(".com/")[1]}/files.json`)
files["files"].forEach(file => {
if (file.category_name=="OPTIONAL" || file.category_name=="OPTIONAL") {
if (!file.description.includes("Voice model")) {
return
}
const description = file.description
const parts = description.split("<br />")
let voiceId = parts.filter(line => line.startsWith("Voice ID:") || line.startsWith("VoiceID:"))[0]
voiceId = voiceId.includes("Voice ID: ") ? voiceId.split("Voice ID: ")[1].split(" ")[0] : voiceId.split("VoiceID: ")[1].split(" ")[0]
const game = idToGame[voiceId.split("_")[0]]
const name = parts.filter(line => line.startsWith("Voice model"))[0].split(" - ")[1]
const date = file.uploaded_time
const nexus_file_id = file.file_id
if (repoInfo.endorsement.endorse_status=="Endorsed") {
window.endorsedRepos.add(game)
}
const hasT2 = description.includes("Tacotron2")
const hasHiFi = description.includes("HiFi-GAN")
const version = file.version
let type
if (description.includes("Model:")) {
type = parts.filter(line => line.startsWith("Model: "))[0].split("Model: ")[1]
} else {
type = "FastPitch"
if (type=="FastPitch") {
if (hasT2) {
type = "T2+"+type
}
}
if (hasHiFi) {
type += "+HiFi"
}
}
const notes = description.includes("Notes:") ? parts.filter(line => line.startsWith("Notes: "))[0].split("Notes: ")[1] : ""
const meta = {author, description, version, voiceId, game, name, type, notes, date, nexusRepoId, nexusRepoVersion, nexusGameId, nexus_file_id, repoLink: link}
window.nexusModelsList.push(meta)
}
})
}
window.closeModal(undefined, nexusContainer)
window.displayAllModels()
} catch (e) {
console.log(e)
window.appLogger.log(e)
window.errorModal(e.message)
}
}
}
const clearColumsFocus = () => {
nexusRecordsHeader.querySelectorAll("div").forEach(elem => elem.style.textDecoration = "none")
}
const setColumnSort = (elem, key) => {
clearColumsFocus()
elem.style.textDecoration = "underline"
if (window.nexusState.primaryColumnSort==key) {
window.nexusState.columnSortModifier = window.nexusState.columnSortModifier * -1
}
window.nexusState.primaryColumnSort = key
window.displayAllModels()
}
i18n_nexush_game.addEventListener("click", () => setColumnSort(i18n_nexush_game, "game"))
i18n_nexush_name.addEventListener("click", () => setColumnSort(i18n_nexush_game, "name"))
i18n_nexush_author.addEventListener("click", () => setColumnSort(i18n_nexush_author, "author"))
i18n_nexush_version.addEventListener("click", () => setColumnSort(i18n_nexush_version, "version"))
i18n_nexush_date.addEventListener("click", () => setColumnSort(i18n_nexush_date, "date"))
i18n_nexush_type.addEventListener("click", () => setColumnSort(i18n_nexush_type, "type"))
const runNestedSort = (modelsList, primKey) => {
// Perform the primary sorting
const primaryGroup = {}
let modelsOrder = []
modelsList.forEach(item => {
if (!Object.keys(primaryGroup).includes(item[primKey]||"")) {
modelsOrder.push(item[primKey]||"")
primaryGroup[item[primKey]||""] = []
}
primaryGroup[item[primKey]||""].push(item)
})
// Sort the primary key in the correct direction
modelsOrder = modelsOrder.sort((a,b) => a<b?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
// Sort the secondary criteria (the voice names) within the primary groups
modelsOrder.forEach(primaryKey => {
primaryGroup[primaryKey] = primaryGroup[primaryKey].sort((a,b) => (a.name||"").toLowerCase()<(b.name||"").toLowerCase()?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
})
// Collate everything back into the final order
const finalOrder = []
modelsOrder.forEach(primaryKey => {
primaryGroup[primaryKey].forEach(record => finalOrder.push(record))
})
return finalOrder
}
window.displayAllModels = (forceUpdate=false) => {
if (!forceUpdate && window.nexusState.installQueue.length) {
return
}
const enabledGames = Array.from(nexusGamesList.querySelectorAll("input"))
.map(elem => [elem.checked, elem.id.replace("ngl_", "")])
.filter(checkedId => checkedId[0])
.map(checkedId => checkedId[1])
const gameColours = {}
Object.keys(window.gameAssets).forEach(gameId => {
const colour = window.gameAssets[gameId].themeColourPrimary
gameColours[gameId] = colour
})
const gameTitles = {}
Object.keys(window.gameAssets).forEach(gameId => {
const title = window.gameAssets[gameId].gameName
gameTitles[gameId] = title
})
nexusRecordsContainer.innerHTML = ""
window.nexusState.filteredDownloadableModels = []
let sortedModelsList = []
// Allow sorting by another column. But should still sort based on voice name alphabetically, as a secondary criteria
// Primary sortable columns: Game, VoiceName, Author, Version, Date, Type
if (window.nexusState.primaryColumnSort=="name") {
sortedModelsList = window.nexusModelsList.sort((a,b) => (a.name||"").toLowerCase()<(b.name||"").toLowerCase()?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
} else {
sortedModelsList = runNestedSort(window.nexusModelsList, window.nexusState.primaryColumnSort)
}
sortedModelsList.forEach(modelMeta => {
if (!enabledGames.includes(modelMeta.game)) {
return
}
if (nexusSearchBar.value.toLowerCase().trim().length && !modelMeta.name.toLowerCase().includes(nexusSearchBar.value.toLowerCase().trim())) {
return
}
let existingModel = undefined
if (Object.keys(window.games).includes(modelMeta.game)) {
for (let mi=0; mi<window.games[modelMeta.game].models.length; mi++) {
if (existingModel) continue
const variants = window.games[modelMeta.game].models[mi].variants
for (let vi=0; vi<variants.length; vi++) {
if (variants[vi].voiceId==modelMeta.voiceId) {
existingModel = variants[vi]
break
}
}
}
}
if (existingModel && nexusOnlyNewUpdatedCkbx.checked && (window.checkVersionRequirements(modelMeta.version, String(existingModel.modelVersion)) || (modelMeta.version.replace(".0","")==String(existingModel.modelVersion))) ){
return
}
const recordRow = createElem("div")
const actionsElem = createElem("div")
// Open link to the repo in the browser
const openButton = createElem("button.smallButton.fixedColour", window.i18n.OPEN)
openButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
openButton.addEventListener("click", () => {
shell.openExternal(modelMeta.repoLink)
})
actionsElem.appendChild(openButton)
// Download
const downloadButton = createElem("button.smallButton.fixedColour", window.i18n.DOWNLOAD)
downloadButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
downloadButton.addEventListener("click", async () => {
// Download the voice
window.nexusState.downloadQueue.push([modelMeta.voiceId, modelMeta.nexus_file_id])
nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
try {
await window.downloadFile([modelMeta.nexusGameId, modelMeta.nexusRepoId, modelMeta.voiceId, modelMeta.nexus_file_id])
// Install the downloaded voice
window.nexusState.installQueue.push([modelMeta.game, modelMeta.voiceId])
nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
await window.installDownloadedModel([modelMeta.game, modelMeta.voiceId])
window.nexusState.finished += 1
nexusFinishedCount.innerHTML = window.nexusState.finished
window.displayAllModels()
window.loadAllModels(true).then(() => {
changeGame(window.currentGame)
})
} catch (e) {}
})
if (existingModel && (modelMeta.version.replace(".0","")==String(existingModel.modelVersion) || window.checkVersionRequirements(modelMeta.version, String(existingModel.modelVersion)) ) ) {
} else {
window.nexusState.filteredDownloadableModels.push(modelMeta)
actionsElem.appendChild(downloadButton)
}
// Endorse
const endorsed = window.endorsedRepos.has(modelMeta.game)
const endorseButton = createElem("button.smallButton.fixedColour", endorsed?"Unendorse":"Endorse")
if (endorsed) {
endorseButton.style.background = "none"
endorseButton.style.border = `2px solid #${gameColours[modelMeta.game]}`
} else {
endorseButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
}
endorseButton.addEventListener("click", async () => {
let response
if (endorsed) {
response = await getData(`${modelMeta.nexusGameId}/mods/${modelMeta.nexusRepoId}/abstain.json`, {
game_domain_name: modelMeta.nexusGameId,
id: modelMeta.nexusRepoId,
version: modelMeta.nexusRepoVersion
}, "POST")
} else {
response = await getData(`${modelMeta.nexusGameId}/mods/${modelMeta.nexusRepoId}/endorse.json`, {
game_domain_name: modelMeta.nexusGameId,
id: modelMeta.nexusRepoId,
version: modelMeta.nexusRepoVersion
}, "POST")
}
if (response && response.message && response.status=="Error") {
if (response.message=="NOT_DOWNLOADED_MOD") {
response.message = window.i18n.NEXUS_NOT_DOWNLOADED_MOD
} else if (response.message=="TOO_SOON_AFTER_DOWNLOAD") {
response.message = window.i18n.NEXUS_TOO_SOON_AFTER_DOWNLOAD
} else if (response.message=="IS_OWN_MOD") {
response.message = window.i18n.NEXUS_IS_OWN_MOD
}
window.errorModal(response.message)
} else {
if (endorsed) {
window.endorsedRepos.delete(modelMeta.game)
} else {
window.endorsedRepos.add(modelMeta.game)
}
window.displayAllModels()
}
})
actionsElem.appendChild(endorseButton)
const gameElem = createElem("div", gameTitles[modelMeta.game])
gameElem.title = gameTitles[modelMeta.game]
const nameElem = createElem("div", modelMeta.name)
nameElem.title = modelMeta.name
const authorElem = createElem("div", modelMeta.author)
authorElem.title = modelMeta.author
let versionElemText
if (existingModel) {
const yoursVersion = String(existingModel.modelVersion).includes(".") ? existingModel.modelVersion : existingModel.modelVersion+".0"
versionElemText = `${modelMeta.version} (${window.i18n.YOURS}: ${yoursVersion})`
} else {
versionElemText = modelMeta.version
}
const versionElem = createElem("div", versionElemText)
versionElem.title = versionElemText
const date = new Date(modelMeta.date)
const dateString = `${date.getDate()}/${date.getMonth()+1}/${date.getYear()+1900}`
const dateElem = createElem("div", dateString)
dateElem.title = dateString
const typeElem = createElem("div", modelMeta.type)
typeElem.title = modelMeta.type
const notesElem = createElem("div", modelMeta.notes)
notesElem.title = modelMeta.notes
recordRow.appendChild(actionsElem)
recordRow.appendChild(gameElem)
recordRow.appendChild(nameElem)
recordRow.appendChild(authorElem)
recordRow.appendChild(versionElem)
recordRow.appendChild(dateElem)
recordRow.appendChild(typeElem)
recordRow.appendChild(notesElem)
nexusRecordsContainer.appendChild(recordRow)
})
}
nexusCheckNow.addEventListener("click", () => window.getLatestModelsList())
window.setupModal(nexusManageReposButton, nexusReposContainer)
window.nexusUpdateModsUsedPanel = () => {
nexusReposUsedContainer.innerHTML = ""
window.nexusReposList.repos.forEach((repo, ri) => {
const row = createElem("div")
const enabledCkbx = createElem("input", {type: "checkbox"})
enabledCkbx.checked = repo.enabled
enabledCkbx.addEventListener("click", () => {
window.nexusReposList.repos[ri].enabled = enabledCkbx.checked
fs.writeFileSync(`${window.path}/repositories.json`, JSON.stringify(window.nexusReposList, null, 4), "utf8")
})
const enabledCkbxElem = createElem("div", enabledCkbx)
const removeButton = createElem("button.smallButton", window.i18n.REMOVE)
removeButton.style.background = `#${window.currentGame.themeColourPrimary}`
const removeButtonElem = createElem("div", removeButton)
const linkButton = createElem("button.smallButton", window.i18n.OPEN)
linkButton.style.background = `#${window.currentGame.themeColourPrimary}`
linkButton.addEventListener("click", () => {
shell.openExternal(repo.url)
})
const linkButtonElem = createElem("div", linkButton)
const gameElem = createElem("div", window.nexusGameIdToGameName[repo.game_id])
gameElem.title = window.nexusGameIdToGameName[repo.game_id]
const nameElem = createElem("div", repo.name)
nameElem.title = repo.name
const authorElem = createElem("div", repo.author)
authorElem.title = repo.author
const endorsementsElem = createElem("div", String(repo.endorsements))
const downloadsElem = createElem("div", String(repo.downloads))
endorsementsElem.style.display = "none" // TEMP
downloadsElem.style.display = "none" // TEMP
row.appendChild(enabledCkbxElem)
row.appendChild(linkButtonElem)
row.appendChild(gameElem)
row.appendChild(nameElem)
row.appendChild(authorElem)
row.appendChild(endorsementsElem)
row.appendChild(downloadsElem)
row.appendChild(removeButtonElem)
nexusReposUsedContainer.appendChild(row)
})
}
window.addRepoToApp = (repo) => {
repo.enabled = true
window.nexusReposList.repos.push(repo)
window.nexusReposList.repos = window.nexusReposList.repos.sort((a,b)=>a.endorsements<b.endorsements?1:-1)
fs.writeFileSync(`${window.path}/repositories.json`, JSON.stringify(window.nexusReposList, null, 4), "utf8")
window.nexusUpdateModsUsedPanel()
}
nexusReposSearchBar.addEventListener("keydown", e => {
if (e.key.toLowerCase()=="enter" && nexusReposSearchBar.value.length) {
searchNexusButton.click()
}
})
searchNexusButton.addEventListener("click", () => {
const gameId = nexusAllGamesSelect.value ? parseInt(nexusAllGamesSelect.value) : undefined
const query = nexusReposSearchBar.value
nexusSearchContainer.innerHTML = ""
window.mod_search_nexus(gameId, query).then(results => {
const numResults = results[0]
results = results[1]
results.forEach(repo => {
const row = createElem("div")
const addButton = createElem("button.smallButton", window.i18n.ADD)
const addButtonElem = createElem("div", addButton)
addButton.style.background = `#${window.currentGame.themeColourPrimary}`
if (window.nexusReposList.repos.find(r=>r.url==repo.url)) {
addButton.disabled = true
}
addButton.addEventListener("click", () => {
window.addRepoToApp(repo)
addButton.disabled = true
})
const linkButton = createElem("button.smallButton", window.i18n.OPEN)
linkButton.style.background = `#${window.currentGame.themeColourPrimary}`
linkButton.addEventListener("click", () => {
shell.openExternal(repo.url)
})
const linkButtonElem = createElem("div", linkButton)
const gameElem = createElem("div", window.nexusGameIdToGameName[repo.game_id])
gameElem.title = window.nexusGameIdToGameName[repo.game_id]
const nameElem = createElem("div", repo.name)
nameElem.title = repo.name
const authorElem = createElem("div", repo.author)
authorElem.title = repo.author
const endorsementsElem = createElem("div", String(repo.endorsements))
const downloadsElem = createElem("div", String(repo.downloads))
row.appendChild(addButtonElem)
row.appendChild(linkButtonElem)
row.appendChild(gameElem)
row.appendChild(nameElem)
row.appendChild(authorElem)
row.appendChild(endorsementsElem)
row.appendChild(downloadsElem)
nexusSearchContainer.appendChild(row)
})
})
})
nexusOnlyNewUpdatedCkbx.addEventListener("change", () => window.displayAllModels())
window.initNexus()
// The app will support voice installation via Steam workshop. However, workshop installations can only install voices into the game directory
// Moreover, users can pick their own locations for voice models. To handle this, I'll have all voices go into the "workshop" folder. From here,
// the app will (on start-up) check if there's anything there, and it will move it to the correct location
window.checkForWorkshopInstallations = () => {
let voicesInstalled = 0
let badGameIDs = []
if (fs.existsSync(`${window.path}/workshop`) && fs.existsSync(`${window.path}/workshop/voices`)) {
const gameFolders = fs.readdirSync(`${window.path}/workshop/voices`)
gameFolders.forEach(gameId => {
const userModelDir = window.userSettings[`modelspath_${gameId}`]
if (!userModelDir) {
badGameIDs.push(gameId)
return
}
const voiceIDs_jsons = fs.readdirSync(`${window.path}/workshop/voices/${gameId}`).filter(fName => fName.endsWith(".json"))
voiceIDs_jsons.forEach(voiceIDs_json => {
const voiceID = voiceIDs_json.replace(".json", "")
const voiceFiles = fs.readdirSync(`${window.path}/workshop/voices/${gameId}`).filter(fName => fName.includes(voiceID))
voiceFiles.forEach(voiceFileName => {
if (!fs.existsSync(`${userModelDir}/${voiceFileName}`)) {
fs.copyFileSync(`${window.path}/workshop/voices/${gameId}/${voiceFileName}`, `${userModelDir}/${voiceFileName}`)
}
fs.unlinkSync(`${window.path}/workshop/voices/${gameId}/${voiceFileName}`)
})
voicesInstalled++
})
})
}
if (voicesInstalled || badGameIDs.length) {
setTimeout(() => {
let modalMessage = ""
if (voicesInstalled) {
modalMessage += window.i18n.X_WORKSHOP_VOICES_INSTALLED.replace("_1", voicesInstalled)
}
if (voicesInstalled && badGameIDs.length) {
modalMessage += "<br><br>"
}
if (badGameIDs) {
modalMessage += window.i18n.WORKSHOP_GAMES_NOT_RECOGNISED.replace("_1", badGameIDs.join(","))
}
createModal("error", modalMessage)
window.updateGameList()
}, 1000)
}
}