Changeset View
Standalone View
binaries/data/mods/mod/gui/modio/modio.js
- This file was added.
Property | Old Value | New Value |
---|---|---|
svn:eol-style | null | native \ No newline at end of property |
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; | |||||
/** | |||||
* Returns true if ModIoAdvanceRequest should be called. | |||||
*/ | |||||
var g_ModIOState = { | |||||
/** | |||||
* Finished status indicators | |||||
*/ | |||||
"ready": progressData => { | |||||
// GameID acquired, ready to fetch mod list | |||||
if (!g_RequestCancelled) | |||||
updateModList(); | |||||
return true; | |||||
}, | |||||
"listed": progressData => { | |||||
// List of available mods acquired | |||||
// Only run this once (for each update). | |||||
if (Engine.GetGUIObjectByName("modsAvailableList").list.length) | |||||
return true; | |||||
hideDialog(); | |||||
Engine.GetGUIObjectByName("refreshButton").enabled = true; | |||||
g_ModsAvailableOnline = Engine.ModIoGetMods(); | |||||
displayMods(); | |||||
return true; | |||||
}, | |||||
"success": progressData => { | |||||
// Successfully acquired a mod file | |||||
hideDialog(); | |||||
Engine.GetGUIObjectByName("downloadButton").enabled = true; | |||||
return true; | |||||
}, | |||||
/** | |||||
* In-progress status indicators. | |||||
*/ | |||||
"gameid": progressData => { | |||||
// Acquiring GameID from mod.io | |||||
return true; | |||||
}, | |||||
"listing": progressData => { | |||||
// Acquiring list of available mods from mod.io | |||||
return true; | |||||
}, | |||||
"downloading": progressData => { | |||||
// Downloading a mod file | |||||
updateProgressBar(progressData.progress, g_ModsAvailableOnline[selectedModIndex()].filesize); | |||||
return true; | |||||
}, | |||||
/** | |||||
* Error/Failure status indicators. | |||||
*/ | |||||
"failed_gameid": progressData => { | |||||
// Game ID couldn't be retrieved | |||||
showErrorMessageBox( | |||||
leper: Might be nice to omit the `current` suffix if it is the same as the `total` suffix, but that… | |||||
Not Done Inline Actions1 KiB /1.2 GiB elexis: 1 KiB /1.2 GiB
1 MiB / 1.2 GIB,
1 / 1.2GiB
Because omitting units when writing numbers is… | |||||
Not Done Inline ActionsYou seem to be the only one with that opinion so far ? Personally both look equally good to me, as already said, and using sarcasm usually has opposite effects on me (and on most people). Itms: You seem to be the only one with that opinion so far ? Personally both look equally good to me… | |||||
Not Done Inline ActionsIf the quantity of expressed opinion decides, then concerns on commits could be closed if the one raising the concern is in the minority. elexis: If the quantity of expressed opinion decides, then concerns on commits could be closed if the… | |||||
Not Done Inline ActionsIf anything is making it harder to the reader then I'd vote for the split of the $number $unit thing from just having $number unit, but well. Now that we've deadlocked Itms in his sarcasm avoidance, we should figure out how to turn that into profit. And if review comments aren't evaluated based on whether they are valid and only based on who might or might not be expressing them, then you seem to have bad times ahead of you as you will hopefully learn those the hard way, but at least soon. leper: If anything is making it harder to the reader then I'd vote for the split of the $number $unit… | |||||
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")], | |||||
[closePage, init]); | |||||
return false; | |||||
}, | |||||
"failed_listing": progressData => { | |||||
Done Inline Actions"Estimated Time Remaining"? temple: "Estimated Time Remaining"? | |||||
// Mod list couldn't be retrieved | |||||
Done Inline ActionsLots of double spaces starting from here, see the linter. leper: Lots of double spaces starting from here, see the linter. | |||||
showErrorMessageBox( | |||||
sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), { | |||||
Done Inline ActionsPossibly suggest that the average download speed is (mis)used as a prediction. leper: Possibly suggest that the average download speed is (mis)used as a prediction. | |||||
"technicalDetails": progressData.error | |||||
}), | |||||
translateWithContext("mod.io error message", "Fetch Error"), | |||||
[translate("Abort"), translate("Retry")], | |||||
[cancelModListUpdate, updateModList]); | |||||
return false; | |||||
}, | |||||
"failed_downloading": progressData => { | |||||
// File couldn't be retrieved | |||||
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")], | |||||
Not Done Inline ActionsThe code for handling the absence of a proper error here and below seems to be a bad idea. leper: The code for handling the absence of a proper error here and below seems to be a bad idea. | |||||
Not Done Inline ActionsWhat do you mean? Is the "-" bad or the actual code? Itms: What do you mean? Is the "-" bad or the actual code? | |||||
Done Inline ActionsWhy is there a need for || "-"? leper: Why is there a need for `|| "-"`? | |||||
[cancelRequest, downloadMod]); | |||||
return false; | |||||
}, | |||||
"failed_filecheck": progressData => { | |||||
// The file is corrupted | |||||
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")], | |||||
[cancelRequest]); | |||||
return false; | |||||
}, | |||||
/** | |||||
* Default | |||||
*/ | |||||
"none": progressData => { | |||||
// Nothing has happened yet. | |||||
return true; | |||||
} | |||||
}; | |||||
function init(data) | |||||
{ | |||||
progressDialog( | |||||
translate("Initializing mod.io interface."), | |||||
translate("Initializing"), | |||||
false, | |||||
translate("Cancel"), | |||||
closePage); | |||||
g_Failure = false; | |||||
Engine.ModIoStartGetGameId(); | |||||
} | |||||
function onTick() | |||||
{ | |||||
let progressData = Engine.ModIoGetDownloadProgress(); | |||||
let handler = g_ModIOState[progressData.status]; | |||||
Done Inline ActionsWhy is this a plural form? leper: Why is this a plural form? | |||||
if (!handler) | |||||
{ | |||||
warn("Unrecognized progress status: " + progressData.status); | |||||
return; | |||||
} | |||||
if (handler(progressData)) | |||||
Engine.ModIoAdvanceRequest(); | |||||
} | |||||
function displayMods() | |||||
{ | |||||
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); | |||||
let 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; | |||||
let filterColumns = ["name", "name_id", "summary"]; | |||||
let filterText = Engine.GetGUIObjectByName("modFilter").caption.toLowerCase(); | |||||
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 => mod.name); | |||||
modsAvailableList.list_name_id = displayedMods.map(mod => mod.name_id); | |||||
modsAvailableList.list_version = displayedMods.map(mod => mod.version); | |||||
modsAvailableList.list_filesize = displayedMods.map(mod => filesizeToString(mod.filesize)); | |||||
modsAvailableList.list_dependencies = displayedMods.map(mod => (mod.dependencies || []).join(" ")); | |||||
modsAvailableList.list = displayedMods.map(mod => mod.i); | |||||
modsAvailableList.selected = modsAvailableList.list.indexOf(selectedMod); | |||||
} | |||||
function clearModList() | |||||
{ | |||||
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); | |||||
modsAvailableList.selected = -1; | |||||
for (let listIdx of Object.keys(modsAvailableList).filter(key => key.startsWith("list"))) | |||||
modsAvailableList[listIdx] = []; | |||||
} | |||||
Done Inline ActionsWhy did someone consider it a good idea to reuse one of the worst ideas of the initial mod selector GUI? leper: Why did someone consider it a good idea to reuse one of the worst ideas of the initial mod… | |||||
Not Done Inline ActionsI like this filter! Let's hope we will have enough mods for it to be useful! Itms: I like this filter! Let's hope we will have enough mods for it to be useful! | |||||
function selectedModIndex() | |||||
{ | |||||
let modsAvailableList = Engine.GetGUIObjectByName("modsAvailableList"); | |||||
if (modsAvailableList.selected == -1) | |||||
return undefined; | |||||
return +modsAvailableList.list[modsAvailableList.selected]; | |||||
} | |||||
function showModDescription() | |||||
{ | |||||
let selected = selectedModIndex(); | |||||
Engine.GetGUIObjectByName("downloadButton").enabled = selected !== undefined; | |||||
Engine.GetGUIObjectByName("modDescription").caption = selected !== undefined ? g_ModsAvailableOnline[selected].summary : ""; | |||||
} | |||||
function cancelModListUpdate() | |||||
{ | |||||
Done Inline ActionsNot actually clearing the current state after a user action seems to just beg for bugs that will be hard to debug down the line. Yes, the current code does not have an issue, but the current structure will cause someone to introduce a bug, that nobody is going to notice, which is going to remain unnoticed for a while, and I wouldn't want to be the one who has to debug such an issue that could have been prevented before it was introduced. leper: Not actually clearing the current state after a user action seems to just beg for bugs that… | |||||
cancelRequest(); | |||||
if (!g_ModsAvailableOnline.length) | |||||
{ | |||||
closePage(); | |||||
return; | |||||
Done Inline ActionsProbably correct. leper: Probably correct. | |||||
} | |||||
displayMods(); | |||||
Engine.GetGUIObjectByName('refreshButton').enabled = true; | |||||
} | |||||
function updateModList() | |||||
{ | |||||
clearModList(); | |||||
Engine.GetGUIObjectByName("refreshButton").enabled = false; | |||||
progressDialog( | |||||
translate("Fetching and updating list of available mods."), | |||||
translate("Updating"), | |||||
false, | |||||
translate("Cancel Update"), | |||||
cancelModListUpdate); | |||||
g_Failure = false; | |||||
g_RequestCancelled = false; | |||||
Engine.ModIoStartListMods(); | |||||
} | |||||
function downloadMod() | |||||
{ | |||||
let selected = selectedModIndex(); | |||||
progressDialog( | |||||
sprintf(translate("Downloading “%(modname)s”"), { | |||||
"modname": g_ModsAvailableOnline[selected].name | |||||
}), | |||||
translate("Downloading"), | |||||
true, | |||||
translate("Cancel Download"), | |||||
() => { Engine.GetGUIObjectByName("downloadButton").enabled = true; }); | |||||
Engine.GetGUIObjectByName("downloadButton").enabled = false; | |||||
Done Inline ActionsNow what would happen if someone initiated another list refresh after starting a download? If you cannot say nothing evil or unexpected immediately something went wrong at some point. leper: Now what would happen if someone initiated another list refresh after starting a download? If… | |||||
g_Failure = false; | |||||
g_RequestCancelled = false; | |||||
Engine.ModIoStartDownloadMod(selected); | |||||
} | |||||
function cancelRequest() | |||||
{ | |||||
g_Failure = false; | |||||
g_RequestCancelled = true; | |||||
Engine.ModIoCancelRequest(); | |||||
hideDialog(); | |||||
} | |||||
function closePage(data) | |||||
{ | |||||
Engine.PopGuiPageCB(undefined); | |||||
} | |||||
function showErrorMessageBox(caption, title, buttonCaptions, buttonActions) | |||||
{ | |||||
if (g_Failure) | |||||
return; | |||||
messageBox(500, 250, caption, title, buttonCaptions, buttonActions); | |||||
g_Failure = true; | |||||
} | |||||
function progressDialog(dialogCaption, dialogTitle, showProgressBar, buttonCaption, buttonAction) | |||||
{ | |||||
Engine.GetGUIObjectByName("downloadDialog_title").caption = dialogTitle; | |||||
let downloadDialog_caption = Engine.GetGUIObjectByName("downloadDialog_caption"); | |||||
Done Inline ActionsIs that big enough with all possible translations? If you aren't sure, why not play it safe, last I checked the minimum resolution is still a lot larger. leper: Is that big enough with all possible translations? If you aren't sure, why not play it safe… | |||||
downloadDialog_caption.caption = dialogCaption; | |||||
let size = downloadDialog_caption.size; | |||||
size.rbottom = showProgressBar ? 40 : 80; | |||||
downloadDialog_caption.size = size; | |||||
Engine.GetGUIObjectByName("downloadDialog_progress").hidden = !showProgressBar; | |||||
Engine.GetGUIObjectByName("downloadDialog_status").hidden = !showProgressBar; | |||||
let downloadDialog_button = Engine.GetGUIObjectByName("downloadDialog_button"); | |||||
downloadDialog_button.caption = buttonCaption; | |||||
downloadDialog_button.onPress = () => { cancelRequest(); buttonAction(); }; | |||||
Engine.GetGUIObjectByName("downloadDialog").hidden = false; | |||||
g_RequestStartTime = Date.now(); | |||||
} | |||||
/* | |||||
* The "remaining time" and "average speed" texts both naively assume that | |||||
* the connection remains relatively stable throughout the download. | |||||
*/ | |||||
function updateProgressBar(progress, totalSize) | |||||
{ | |||||
Not Done Inline ActionsWhy is that even an option? leper: Why is that even an option? | |||||
Not Done Inline ActionsThat was referring to the progressPercent option above. I take it that one of the progress dialogs does not display a progress bar. leper: That was referring to the `progressPercent` option above. I take it that one of the progress… | |||||
Not Done Inline ActionsYeah one of the dialogs has a bar, the others don't. The boolean leaves us with one neat function with no code duplication (and its's cleaner in the current version of the patch). Itms: Yeah one of the dialogs has a bar, the others don't. The boolean leaves us with one neat… | |||||
let progressPercent = Math.ceil(progress * 100); | |||||
Engine.GetGUIObjectByName("downloadDialog_progressBar").caption = progressPercent; | |||||
let transferredSize = progress * totalSize; | |||||
let 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 | |||||
}); | |||||
let elapsedTime = Date.now() - g_RequestStartTime; | |||||
let remainingTime = progressPercent ? (100 - progressPercent) * elapsedTime / progressPercent : 0; | |||||
let 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; | |||||
} |
Might be nice to omit the current suffix if it is the same as the total suffix, but that does not really seem important, and if that is done it should be moved to some helper function.