0ad/binaries/data/mods/mod/gui/modio/modio.js
Dunedan 93ce94655d
Use @stylistic/brace-style for eslint
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace
style for JavaScript code. This plugin was however updated the last time
over 9 years ago and will be incompatible with ESLint v10, as that
[removes `context.getSourceCode()`][1], the plugin relies on.

To keep the eslint config working with ESLint v10, this replaces
`eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule
from `@stylistic/eslint-plugin`, a package we already use.

While `@stylistic/brace-style` doesn't offer an option to format braces
in exactly the same way as before, the "allman" style seems to be the
one closest to the existing code.

[1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members
[2]: https://eslint.style/rules/brace-style
2026-01-12 21:33:52 +01:00

385 lines
12 KiB
JavaScript

var g_ModsAvailableOnline = [];
/**
* Indicates if we have encountered an error in one of the network-interaction attempts.
*
* We use a global so we don't get multiple messageBoxes appearing (one for each "tick").
*
* Set to `true` by showErrorMessageBox
* Set to `false` by init, updateModList, downloadFile, and cancelRequest
*/
var g_Failure;
/**
* Indicates if the user has cancelled a request.
*
* Primarily used so the user can cancel the mod list fetch, as whenever that get cancelled,
* the modio state reverts to "ready", even if we've successfully listed mods before.
*
* Set to `true` by cancelRequest
* Set to `false` by updateModList, and downloadFile
*/
var g_RequestCancelled;
var g_RequestStartTime;
var g_ModIOState = {
/**
* Finished status indicators
*/
"ready": (progressData, closePageCallback) =>
{
// GameID acquired, ready to fetch mod list
if (!g_RequestCancelled)
updateModList(closePageCallback);
},
"listed": progressData =>
{
// List of available mods acquired
// Only run this once (for each update).
if (Engine.GetGUIObjectByName("modsAvailableList").list.length)
return;
hideDialog();
Engine.GetGUIObjectByName("refreshButton").enabled = true;
g_ModsAvailableOnline = Engine.ModIoGetMods();
displayMods();
},
"success": progressData =>
{
// Successfully acquired a mod file
hideDialog();
Engine.GetGUIObjectByName("downloadButton").enabled = true;
},
/**
* In-progress status indicators.
*/
"gameid": progressData =>
{
// Acquiring GameID from mod.io
},
"listing": progressData =>
{
// Acquiring list of available mods from mod.io
},
"downloading": progressData =>
{
// Downloading a mod file
updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize);
},
/**
* Error/Failure status indicators.
*/
"failed_gameid": async(progressData, closePageCallback) =>
{
// Game ID couldn't be retrieved
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Game ID could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Initialization Error"),
[translate("Abort"), translate("Retry")]);
if (!promise)
return;
if (await promise === 0)
closePageCallback();
else
init();
},
"failed_listing": async(progressData, closePageCallback) =>
{
// Mod list couldn't be retrieved
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Fetch Error"),
[translate("Abort"), translate("Retry")]);
if (!promise)
return;
if (await promise === 0)
cancelModListUpdate(closePageCallback);
else
updateModList(closePageCallback);
},
"failed_downloading": async(progressData) =>
{
// File couldn't be retrieved
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File download failed.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Download Error"),
[translate("Abort"), translate("Retry")]);
if (!promise)
return;
if (await promise === 0)
cancelRequest();
else
downloadMod();
},
"failed_filecheck": async(progressData) =>
{
// The file is corrupted
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "File verification error.\n\n%(technicalDetails)s"), {
"technicalDetails": progressData.error
}),
translateWithContext("mod.io error message", "Verification Error"),
[translate("Abort")]);
if (!promise)
return;
await promise;
cancelRequest();
},
/**
* Default
*/
"none": progressData =>
{
// Nothing has happened yet.
}
};
function init(data)
{
const promise = progressDialog(
translate("Initializing mod.io interface."),
translate("Initializing"),
false,
translate("Cancel"));
g_Failure = false;
Engine.ModIoStartGetGameId();
return Promise.race([
promise,
new Promise(closePageCallback =>
{
Engine.GetGUIObjectByName("backButton").onPress = closePageCallback;
Engine.GetGUIObjectByName("modio").onTick = onTick.bind(null, closePageCallback);
})
]);
}
function onTick(closePageCallback)
{
const progressData = Engine.ModIoGetDownloadProgress();
const handler = g_ModIOState[progressData.status];
if (!handler)
{
warn("Unrecognized progress status: " + progressData.status);
return;
}
handler(progressData, closePageCallback);
if (!progressData.status.startsWith("failed"))
Engine.ModIoAdvanceRequest();
}
function displayMods()
{
const modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
const selectedMod = modsAvailableList.list[modsAvailableList.selected];
modsAvailableList.selected = -1;
let displayedMods = clone(g_ModsAvailableOnline);
for (let i = 0; i < displayedMods.length; ++i)
displayedMods[i].i = i;
const filterColumns = ["name", "name_id", "summary"];
const filterText = Engine.GetGUIObjectByName("modFilter").caption.toLowerCase();
if (Engine.GetGUIObjectByName("compatibilityFilter").checked)
displayedMods = displayedMods.filter(mod => !mod.invalid);
displayedMods = displayedMods.filter(mod => filterColumns.some(column => mod[column].toLowerCase().indexOf(filterText) != -1));
displayedMods.sort((mod1, mod2) =>
modsAvailableList.selected_column_order *
(modsAvailableList.selected_column == "filesize" ?
mod1.filesize - mod2.filesize :
String(mod1[modsAvailableList.selected_column]).localeCompare(String(mod2[modsAvailableList.selected_column]))));
modsAvailableList.list_name = displayedMods.map(mod => compatibilityColor(mod.name, !mod.invalid));
modsAvailableList.list_name_id = displayedMods.map(mod => compatibilityColor(mod.name_id, !mod.invalid));
modsAvailableList.list_version = displayedMods.map(mod => compatibilityColor(mod.version || "", !mod.invalid));
modsAvailableList.list_filesize = displayedMods.map(mod => compatibilityColor(mod.filesize !== undefined ? filesizeToString(mod.filesize) : filesizeToString(mod.filesize), !mod.invalid));
modsAvailableList.list_dependencies = displayedMods.map(mod => compatibilityColor((mod.dependencies || []).join(" "), !mod.invalid));
modsAvailableList.list = displayedMods.map(mod => mod.i);
modsAvailableList.selected = modsAvailableList.list.indexOf(selectedMod);
}
function clearModList()
{
const modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
modsAvailableList.selected = -1;
modsAvailableList.list_name = [];
modsAvailableList.list_name_id = [];
modsAvailableList.list_version = [];
modsAvailableList.list_filesize = [];
modsAvailableList.list_dependencies = [];
modsAvailableList.list = [];
}
function selectedModIndex()
{
const modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList");
if (modsAvailableList.selected == -1)
return undefined;
return +modsAvailableList.list[modsAvailableList.selected];
}
function isSelectedModInvalid(selected)
{
return selected !== undefined && !!g_ModsAvailableOnline[selected].invalid && g_ModsAvailableOnline[selected].invalid == "true";
}
function showModDescription()
{
const selected = selectedModIndex();
const isSelected = selected !== undefined;
const isInvalid = isSelectedModInvalid(selected);
Engine.GetGUIObjectByName("downloadButton").enabled = isSelected && !isInvalid;
Engine.GetGUIObjectByName("modDescription").caption = isSelected && !isInvalid ? g_ModsAvailableOnline[selected].summary : "";
Engine.GetGUIObjectByName("modError").caption = isSelected && isInvalid ? sprintf(translate("Invalid mod: %(error)s"), { "error": g_ModsAvailableOnline[selected].error }) : "";
}
function cancelModListUpdate(closePageCallback)
{
cancelRequest();
if (!g_ModsAvailableOnline.length)
{
closePageCallback();
return;
}
displayMods();
Engine.GetGUIObjectByName('refreshButton').enabled = true;
}
async function updateModList(closePageCallback)
{
clearModList();
Engine.GetGUIObjectByName("refreshButton").enabled = false;
const promise = progressDialog(
translate("Fetching and updating list of available mods."),
translate("Updating"),
false,
translate("Cancel Update"));
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartListMods();
await promise;
cancelModListUpdate(closePageCallback);
}
async function downloadMod()
{
const selected = selectedModIndex();
if (isSelectedModInvalid(selected))
return;
const promise = progressDialog(
sprintf(translate("Downloading “%(modname)s”"), {
"modname": g_ModsAvailableOnline[selected].name
}),
translate("Downloading"),
true,
translate("Cancel Download"));
Engine.GetGUIObjectByName("downloadButton").enabled = false;
g_Failure = false;
g_RequestCancelled = false;
Engine.ModIoStartDownloadMod(selected);
await promise;
Engine.GetGUIObjectByName("downloadButton").enabled = true;
}
function cancelRequest()
{
g_Failure = false;
g_RequestCancelled = true;
Engine.ModIoCancelRequest();
hideDialog();
}
function showErrorMessageBox(caption, title, buttonCaptions)
{
if (g_Failure)
return null;
g_Failure = true;
return messageBox(500, 250, caption, title, buttonCaptions);
}
async function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption)
{
Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle;
const downloadDialogCaption = Engine.GetGUIObjectByName("downloadDialog_caption");
downloadDialogCaption.caption = dialogCaption;
downloadDialogCaption.size.rbottom = showProgressBar ? 40 : 80;
Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar;
Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar;
Engine.GetGUIObjectByName("downloadDialog").hidden = false;
g_RequestStartTime = Date.now();
const downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button");
downloadDialog_button.caption = buttonCaption;
await new Promise(resolve =>
{
downloadDialog_button.onPress = resolve;
});
cancelRequest();
}
/*
* The "remaining time" and "average speed" texts both naively assume that
* the connection remains relatively stable throughout the download.
*/
function updateProgressBar(progress, totalSize)
{
const progressPercent = Math.ceil(progress * 100);
Engine.GetGUIObjectByName("downloadDialog_progressBar").progress = progressPercent;
const transferredSize = progress * totalSize;
const transferredSizeObj = filesizeToObj(transferredSize);
// Translation: Mod file download indicator. Current size over expected final size, with percentage complete.
Engine.GetGUIObjectByName("downloadDialog_progressText").caption = sprintf(translate("%(current)s / %(total)s (%(percent)s%%)"), {
"current": filesizeToObj(totalSize).unit == transferredSizeObj.unit ? transferredSizeObj.filesize : filesizeToString(transferredSize),
"total": filesizeToString(totalSize),
"percent": progressPercent
});
const elapsedTime = Date.now() - g_RequestStartTime;
const remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0;
const avgSpeed = filesizeToObj(transferredSize / (elapsedTime / 1000));
// Translation: Mod file download status message.
Engine.GetGUIObjectByName("downloadDialog_status").caption = sprintf(translate("Time Elapsed: %(elapsed)s\nEstimated Time Remaining: %(remaining)s\nAverage Speed: %(avgSpeed)s"), {
"elapsed": timeToString(elapsedTime),
"remaining": remainingTime ? timeToString(remainingTime) : translate("∞"),
// Translation: Average download speed, used to give the user a very rough and naive idea of the download time. For example: 123.4 KiB/s
"avgSpeed": sprintf(translate("%(number)s %(unit)s/s"), {
"number": avgSpeed.filesize,
"unit": avgSpeed.unit
})
});
}
function hideDialog()
{
Engine.GetGUIObjectByName("downloadDialog").hidden = true;
}