Files
libloot/resources/report/js/script.js
T
WrinklyNinja ef8ab2c986 Fixed crash when reading malformed plugins.
Error message now gets displayed in the LOOT GUI. Fixes #269.
2014-09-07 17:24:39 +01:00

1456 lines
59 KiB
JavaScript

/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2013-2014 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<http://www.gnu.org/licenses/>.
*/
'use strict';
var marked;
var loot = {
hasFocus: true,
installedGames: [],
settings: {},
game: {
folder: '',
globalMessages: [],
masterlist: {},
plugins: [],
/* Call whenever game is changed or game menu / game table are rewritten. */
updateSelectedGame: function() {
/* Highlight game in menu. Could use fa-chevron-right instead. */
var gameMenuItems = document.getElementById('gameMenu').children[0].children;
for (var i = 0; i < gameMenuItems.length; ++i) {
if (gameMenuItems[i].getAttribute('data-folder') == this.folder) {
gameMenuItems[i].getElementsByClassName('fa')[0].classList.toggle('fa-angle-double-right', true);
} else {
gameMenuItems[i].getElementsByClassName('fa')[0].classList.toggle('fa-angle-double-right', false);
}
}
/* Also disable deletion of the game's row in the settings dialog. */
var rows = document.getElementById('gameTable').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
for (var i = 0; i < rows.length; ++i) {
if (rows[i].getElementsByClassName('folder').length > 0) {
if (rows[i].getElementsByClassName('folder')[0].value == this.folder) {
document.getElementById('gameTable').setReadOnly(rows[i], ['fa-trash-o']);
} else {
document.getElementById('gameTable').setReadOnly(rows[i], ['fa-trash-o'], false);
}
}
}
},
/* Call whenever game is changed. */
updateRedatePluginsButtonState: function() {
/* Also enable/disable the redate plugins option. */
var index = undefined;
for (var i = 0; i < loot.settings.games.length; ++i) {
if (loot.settings.games[i].folder == this.folder) {
index = i;
break;
}
}
var redateButton = document.getElementById('redatePluginsButton');
if (index != undefined && loot.settings.games[index].type == 'Skyrim') {
redateButton.classList.toggle('disabled', false);
redateButton.title = '';
redateButton.removeEventListener('mouseenter', showHoverText, false);
redateButton.removeEventListener('mouseleave', hideHoverText, false);
} else {
redateButton.classList.toggle('disabled', true);
redateButton.title = 'A Skyrim-specific feature';
redateButton.addEventListener('mouseenter', showHoverText, false);
redateButton.addEventListener('mouseleave', hideHoverText, false);
}
},
pluginsObserver: function(changes) {
changes.forEach(function(change){
/* Need to handle plugin addition and removal.
Update plugin and message counts. */
var totalChange = 0;
var warnChange = 0;
var errorChange = 0;
var activeChange = 0;
var dirtyChange = 0;
if (change.addedCount > 0) {
/* Addition */
for (var i = change.index; i < change.index + change.addedCount; ++i) {
if (change.object[i].isActive) {
++activeChange;
}
if (change.object[i].isDirty) {
++dirtyChange;
}
if (change.object[i].messages) {
totalChange += change.object[i].messages.length;
change.object[i].messages.forEach(function(message) {
if (message.type == 'warn') {
++warnChange;
} else if (message.type == 'error') {
++errorChange;
}
});
}
}
}
if (change.removed.length > 0) {
/* Removal */
change.removed.forEach(function(plugin){
if (plugin.isActive) {
--activeChange;
}
if (plugin.isDirty) {
--dirtyChange;
}
if (plugin.messages) {
totalChange -= plugin.messages.length;
plugin.messages.forEach(function(message) {
if (message.type == 'warn') {
--warnChange;
} else if (message.type == 'error') {
--errorChange;
}
});
}
});
}
document.getElementById('filterTotalMessageNo').textContent = parseInt(document.getElementById('filterTotalMessageNo').textContent, 10) + totalChange;
document.getElementById('totalMessageNo').textContent = document.getElementById('filterTotalMessageNo').textContent;
document.getElementById('totalWarningNo').textContent = parseInt(document.getElementById('totalWarningNo').textContent, 10) + warnReduction;
document.getElementById('totalErrorNo').textContent = parseInt(document.getElementById('totalErrorNo').textContent, 10) + errorReduction;
document.getElementById('filterTotalPluginNo').textContent = parseInt(document.getElementById('filterTotalPluginNo').textContent, 10) + change.addedCount - change.removed.length;
document.getElementById('totalPluginNo').textContent = document.getElementById('filterTotalPluginNo').textContent;
document.getElementById('activePluginNo').textContent = parseInt(document.getElementById('activePluginNo').textContent, 10) + activeReduction;
document.getElementById('dirtyPluginNo').textContent = parseInt(document.getElementById('dirtyPluginNo').textContent, 10) + dirtyReduction;
});
}
},
/* Call whenever installedGames is changed or game menu is rewritten. */
updateEnabledGames: function() {
/* Update the disabled games in the game menu. */
var gameMenuItems = document.getElementById('gameMenu').firstElementChild.children;
for (var i = 0; i < gameMenuItems.length; ++i) {
if (this.installedGames.indexOf(gameMenuItems[i].getAttribute('data-folder')) == -1) {
gameMenuItems[i].classList.toggle('disabled', true);
gameMenuItems[i].title = 'No install detected';
gameMenuItems[i].removeEventListener('click', changeGame, false);
gameMenuItems[i].addEventListener('mouseenter', showHoverText, false);
gameMenuItems[i].addEventListener('mouseleave', hideHoverText, false);
} else {
gameMenuItems[i].classList.toggle('disabled', false);
gameMenuItems[i].title = '';
gameMenuItems[i].addEventListener('click', changeGame, false);
gameMenuItems[i].removeEventListener('mouseenter', showHoverText, false);
gameMenuItems[i].removeEventListener('mouseleave', hideHoverText, false);
}
}
},
/* Call whenever settings are changed. */
updateSettingsUI: function() {
var gameSelect = document.getElementById('defaultGameSelect');
var gameMenu = document.getElementById('gameMenu').firstElementChild;
var gameTable = document.getElementById('gameTable');
/* First make sure game listing elements don't have any existing entries. */
while (gameSelect.children.length > 1) {
gameSelect.removeChild(gameSelect.lastElementChild);
}
while (gameMenu.firstElementChild) {
gameMenu.firstElementChild.removeEventListener('click', changeGame, false);
gameMenu.firstElementChild.removeEventListener('mouseenter', showHoverText, false);
gameMenu.firstElementChild.removeEventListener('mouseleave', hideHoverText, false);
gameMenu.removeChild(gameMenu.firstElementChild);
}
gameTable.clear();
/* Now fill with new values. */
this.settings.games.forEach(function(game){
var option = document.createElement('option');
option.value = game.folder;
option.textContent = game.name;
gameSelect.appendChild(option);
var li = document.createElement('li');
li.setAttribute('data-folder', game.folder);
var icon = document.createElement('span');
icon.className = 'fa fa-fw';
li.appendChild(icon);
var text = document.createElement('span');
text.textContent = game.name;
li.appendChild(text);
gameMenu.appendChild(li);
var row = gameTable.addRow(game);
gameTable.setReadOnly(row, ['name','folder','type']);
});
gameSelect.value = this.settings.game;
document.getElementById('languageSelect').value = this.settings.language;
document.getElementById('enableDebugLogging').checked = this.settings.enableDebugLogging;
document.getElementById('updateMasterlist').checked = this.settings.updateMasterlist;
document.getElementById('autoRefresh').checked = this.settings.autoRefresh;
this.updateEnabledGames();
this.game.updateSelectedGame();
},
/* Observer for loot members. */
observer: function(changes) {
changes.forEach(function(change){
if (change.name == 'installedGames') {
change.object.updateEnabledGames();
} else if (change.name == 'settings') {
change.object.updateSettingsUI();
}
});
},
/* Observer for loot.game members. */
gameObserver: function(changes) {
changes.forEach(function(change){
if (change.name == 'folder') {
change.object.updateSelectedGame();
change.object.updateRedatePluginsButtonState();
} else if (change.name == 'masterlist') {
if (change.object[change.name] && change.object[change.name].revision) {
document.getElementById('masterlistRevision').textContent = change.object[change.name].revision;
} else {
document.getElementById('masterlistRevision').textContent = 'N/A';
}
if (change.object[change.name] && change.object[change.name].date) {
document.getElementById('masterlistDate').textContent = change.object[change.name].date;
} else {
document.getElementById('masterlistDate').textContent = 'N/A';
}
} else if (change.name == 'globalMessages') {
/* For the messages, they don't have a JS 'class' so need to everything
here. Count up the global message types that exist, then remove them
and add the new ones, counting their types, then update the message
counts. I tried using an observer for this, but it wouldn't fire for
some reason. */
var oldTotal = change.oldValue.length;
var newTotal = 0;
var oldWarn = 0;
var newWarn = 0;
var oldErr = 0;
var newErr = 0;
/* Count up old warnings and errors. */
change.oldValue.forEach(function(message){
if (message.type == 'warn') {
++oldWarn;
} else if (message.type == 'error') {
++oldErr;
}
});
/* Remove old messages from UI. */
var generalMessagesList = document.getElementById('generalMessages').getElementsByTagName('ul')[0];
while (generalMessagesList.firstElementChild) {
generalMessagesList.removeChild(generalMessagesList.firstElementChild);
}
/* Add new messages. */
if (change.object[change.name]) {
newTotal = change.object[change.name].length;
change.object[change.name].forEach(function(message){
var li = document.createElement('li');
li.className = message.type;
/* Use the Marked library for Markdown formatting support. */
li.innerHTML = marked(message.content[0].str);
generalMessagesList.appendChild(li);
if (li.className == 'warn') {
++newWarn;
} else if (li.className == 'error') {
++newErr;
}
});
}
/* Update message counts in UI. */
document.getElementById('filterTotalMessageNo').textContent = parseInt(document.getElementById('filterTotalMessageNo').textContent, 10) + newTotal - oldTotal;
document.getElementById('totalMessageNo').textContent = document.getElementById('filterTotalMessageNo').textContent;
document.getElementById('totalWarningNo').textContent = parseInt(document.getElementById('totalWarningNo').textContent, 10) + newWarn - oldWarn;
document.getElementById('totalErrorNo').textContent = parseInt(document.getElementById('totalErrorNo').textContent, 10) + newErr - oldErr;
} else if (change.name == 'plugins') {
/* Update plugin and message counts. Unlike for global messages
it's not worth calculating the count differences, just count
from zero. */
var totalMessageNo = 0;
var warnMessageNo = 0;
var errorMessageNo = 0;
var activePluginNo = 0;
var dirtyPluginNo = 0;
if (change.object.globalMessages) {
totalMessageNo = change.object.globalMessages.length;
change.object.globalMessages.forEach(function(message){
if (message.type == 'warn') {
++warnMessageNo;
} else if (message.type == 'error') {
++errorMessageNo;
}
});
}
change.object[change.name].forEach(function(plugin) {
if (plugin.isActive) {
++activePluginNo;
}
if (plugin.isDirty) {
++dirtyPluginNo;
}
if (plugin.messages) {
totalMessageNo += plugin.messages.length;
plugin.messages.forEach(function(message) {
if (message.type == 'warn') {
++warnMessageNo;
} else if (message.type == 'error') {
++errorMessageNo;
}
});
}
});
document.getElementById('filterTotalMessageNo').textContent = totalMessageNo;
document.getElementById('totalMessageNo').textContent = totalMessageNo;
document.getElementById('totalWarningNo').textContent = warnMessageNo;
document.getElementById('totalErrorNo').textContent = errorMessageNo;
document.getElementById('filterTotalPluginNo').textContent = change.object[change.name].length;
document.getElementById('totalPluginNo').textContent = change.object[change.name].length;
document.getElementById('activePluginNo').textContent = activePluginNo;
document.getElementById('dirtyPluginNo').textContent = dirtyPluginNo;
/* Register a new array observer. */
Array.observe(change.object[change.name], change.object.pluginsObserver);
}
});
},
/* Returns a cefQuery as a Promise. */
query: function(request) {
return new Promise(function(resolve, reject) {
window.cefQuery({
request: request,
persistent: false,
onSuccess: resolve,
onFailure: function(errorCode, errorMessage) {
reject(Error('Error code: ' + errorCode + '; ' + errorMessage))
}
});
});
}
};
Object.observe(loot, loot.observer);
/* Register game object observer. */
Object.observe(loot.game, loot.gameObserver);
function processCefError(err) {
/* Error.stack seems to be Chromium-specific. It gives a lot more useful
info than just the error message. */
console.log(err.stack);
closeProgressDialog();
showMessageBox('error', 'Error', err.message);
}
function saveFilterState(evt) {
if (evt.currentTarget.checked) {
if (!loot.settings.filters) {
loot.settings.filters = {};
}
loot.settings.filters[evt.currentTarget.id] = true;
} else {
if (loot.settings.filters) {
delete loot.settings.filters[evt.currentTarget.id];
}
}
var request = JSON.stringify({
name: 'saveFilterState',
args: [
evt.currentTarget.id,
evt.currentTarget.checked
]
});
loot.query(request).catch(processCefError);
}
function applySavedFilters() {
if (loot.settings.filters) {
for (var key in loot.settings.filters) {
var elem = document.getElementById(key);
if (elem) {
elem.dispatchEvent(new MouseEvent('click'));
}
}
}
}
function isVisible(element) {
return (element.className.indexOf('hidden') == -1);
}
function showElement(element) {
if (element != null) {
element.classList.toggle('hidden', false);
}
}
function hideElement(element) {
if (element != null) {
element.classList.toggle('hidden', true);
}
}
function toggleDisplayCSS(evt) {
var e = document.getElementsByClassName(evt.target.getAttribute('data-class'));
if (evt.target.checked) {
for (var i = 0, z = e.length; i < z; i++) {
e[i].classList.toggle('hidden', true);
}
} else {
for (var i = 0, z = e.length; i < z; i++) {
e[i].classList.toggle('hidden', false);
}
}
}
function getConflictingPluginsFromFilter() {
var conflictsPlugin = document.body.getAttribute('data-conflicts');
if (conflictsPlugin) {
/* Now get conflicts for the plugin. */
var request = JSON.stringify({
name: 'getConflictingPlugins',
args: [
conflictsPlugin
]
});
updateProgressDialog('Checking if plugins have been loaded...');
openProgressDialog();
return loot.query(request).then(JSON.parse).then(function(result){
if (result) {
/* Filter everything but the plugin itself if there are no
conflicts. */
var conflicts = [ conflictsPlugin ];
for (var key in result) {
if (result[key].conflicts) {
conflicts.push(key);
}
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == key) {
loot.game.plugins[i].crc = result[key].crc;
loot.game.plugins[i].isDummy = result[key].isDummy;
loot.game.plugins[i].messages = result[key].messages;
loot.game.plugins[i].tags = result[key].tags;
loot.game.plugins[i].isDirty = result[key].isDirty;
break;
}
}
}
closeProgressDialog();
return conflicts;
}
closeProgressDialog();
return [ conflictsPlugin ];
}).catch(processCefError);
}
return Promise.resolve([]);
}
function applyFilters(evt) {
var cards = document.getElementsByTagName('main')[0].getElementsByTagName('plugin-card');
var entries = document.getElementById('pluginsNav').children;
var hiddenPluginNo = 0;
var hiddenMessageNo = 0;
if (cards.length != entries.length) {
throw Error("Error: Number of plugins in sidebar doesn't match number of plugins in main area!");
}
/* The conflict filter, if enabled, executes C++ code, so needs to be
handled using a promise, so the rest of the function should wait until
it is completed.
*/
getConflictingPluginsFromFilter().then(function(conflicts) {
for (var i = 0; i < cards.length; ++i) {
var isConflictingPlugin = false;
var isMessageless = true;
var hasInactivePluginMessages = false;
var messages = cards[i].getElementsByTagName('ul')[0].getElementsByTagName('li');
if (cards[i].getAttribute('data-active') == 'false') {
hasInactivePluginMessages = true;
}
if (conflicts.indexOf(cards[i].getName()) != -1) {
isConflictingPlugin = true;
}
for (var j = 0; j < messages.length; ++j) {
var hasPluginMessages = false;
var hasNotes = false;
var hasDoNotCleanMessages = false;
if (messages[j].parentElement.parentElement.id != 'generalMessages') {
hasPluginMessages = true;
}
if (messages[j].className.indexOf('say') != -1) {
hasNotes = true;
}
if (messages[j].textContent.indexOf('Do not clean.') != -1) {
hasDoNotCleanMessages = true;
}
if ((document.getElementById('hideAllPluginMessages').checked && hasPluginMessages)
|| (document.getElementById('hideNotes').checked && hasNotes)
|| (document.getElementById('hideDoNotCleanMessages').checked && hasDoNotCleanMessages)
|| (document.getElementById('hideInactivePluginMessages').checked && hasInactivePluginMessages)) {
hideElement(messages[j]);
++hiddenMessageNo;
} else {
showElement(messages[j]);
}
if (messages[j].className.indexOf('hidden') == -1) {
isMessageless = false;
break;
}
}
if ((document.getElementById('hideMessagelessPlugins').checked && isMessageless)
|| conflicts.length > 0 && !isConflictingPlugin) {
hideElement(cards[i]);
hideElement(entries[i]);
++hiddenPluginNo;
} else {
showElement(cards[i]);
showElement(entries[i]);
}
}
document.getElementById('hiddenMessageNo').textContent = hiddenMessageNo;
document.getElementById('hiddenPluginNo').textContent = hiddenPluginNo;
});
}
function showMessageDialog(title, text, closeCallback) {
var dialog = new MessageDialog();
document.body.appendChild(dialog);
dialog.showModal('warn', title, text, closeCallback);
}
function showMessageBox(type, title, text) {
var dialog = new MessageDialog();
document.body.appendChild(dialog);
dialog.showModal(type, title, text);
}
function openLogLocation(evt) {
loot.query('openLogLocation').catch(processCefError);
}
function changeGame(evt) {
/* First store current game info in loot.games object.
This object may not exist, so initialise it if not. */
var index = undefined;
if (loot.games) {
for (var i = 0; i < loot.games.length; ++i) {
if (loot.games[i].folder == loot.game.folder) {
index = i;
break;
}
}
} else {
loot.games = [];
}
if (index == undefined) {
loot.games.push({
folder: loot.game.folder
});
index = loot.games.length - 1;
}
loot.games[index].globalMessages = loot.game.globalMessages;
loot.games[index].masterlist = loot.game.masterlist;
loot.games[index].plugins = loot.game.plugins;
updateProgressDialog('Loading game data...');
openProgressDialog();
/* Now send off a CEF query with the folder name of the new game. */
var request = JSON.stringify({
name: 'changeGame',
args: [
evt.currentTarget.getAttribute('data-folder')
]
});
loot.query(request).then(function(result){
/* Filters should be re-applied on game change, except the conflicts
filter. Don't need to deactivate the others beforehand. Strictly not
deactivating the conflicts filter either, just resetting it's value.
*/
document.body.removeAttribute('data-conflicts');
/* Clear the UI of all existing game-specific data. Also
clear the card and li variables for each plugin object. */
loot.games[index].plugins.forEach(function(plugin){
plugin.card.parentElement.removeChild(plugin.card);
plugin.card = undefined;
plugin.li.parentElement.removeChild(plugin.li);
plugin.li = undefined;
});
var globalMessages = document.getElementById('generalMessages').getElementsByTagName('ul')[0];
while (globalMessages.firstElementChild) {
globalMessages.removeChild(globalMessages.firstElementChild);
}
/* Parse the data sent from C++. */
try {
var gameInfo = JSON.parse(result, jsonToPlugin);
} catch (e) {
console.log(e);
console.log('changeGame response: ' + result);
}
/* This may not be the first time loading this game this instance of
LOOT. Restore cached data if it exists. */
index = undefined;
if (loot.games) {
for (var i = 0; i < loot.games.length; ++i) {
if (loot.games[i].folder == gameInfo.folder) {
index = i;
break;
}
}
}
if (index == undefined) {
/* Game data not cached, simply set what was sent. */
loot.game.folder = gameInfo.folder;
loot.game.masterlist = gameInfo.masterlist;
loot.game.globalMessages = gameInfo.globalMessages;
loot.game.plugins = gameInfo.plugins;
} else {
/* Game data cache exists. */
loot.game.folder = loot.games[i].folder;
loot.game.masterlist = loot.games[i].masterlist;
loot.game.globalMessages = loot.games[i].globalMessages;
loot.game.plugins = loot.games[i].plugins;
/* Now overwrite plugin data with the newly sent data. Also update
card and li vars as they were unset when the game was switched
from before. */
gameInfo.plugins.forEach(function(plugin){
var foundPlugin = false;
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == plugin.name) {
loot.game.plugins[i].isActive = plugin.isActive;
loot.game.plugins[i].isDummy = plugin.isDummy;
loot.game.plugins[i].loadsBSA = plugin.loadsBSA;
loot.game.plugins[i].crc = plugin.crc;
loot.game.plugins[i].version = plugin.version;
loot.game.plugins[i].modPriority = plugin.modPriority;
loot.game.plugins[i].isGlobalPriority = plugin.isGlobalPriority;
loot.game.plugins[i].messages = plugin.messages;
loot.game.plugins[i].tags = plugin.tags;
loot.game.plugins[i].isDirty = plugin.isDirty;
loot.game.plugins[i].card = plugin.card;
loot.game.plugins[i].li = plugin.li;
foundPlugin = true;
break;
}
}
if (!foundPlugin) {
/* A new plugin. */
loot.game.plugins.push(plugin);
}
});
}
/* Reapply previously active filters. */
applyFilters();
closeProgressDialog();
}).catch(processCefError);
}
function openReadme(evt) {
loot.query('openReadme').catch(processCefError);
}
function updateMasterlist(evt) {
updateProgressDialog('Updating masterlist...');
openProgressDialog();
return loot.query('updateMasterlist').then(JSON.parse).then(function(result){
if (!result) {
closeProgressDialog();
return;
}
/* Update JS variables. */
loot.game.masterlist = result.masterlist;
loot.game.globalMessages = result.globalMessages;
result.plugins.forEach(function(plugin){
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == plugin.name) {
loot.game.plugins[i].isDirty = plugin.isDirty;
loot.game.plugins[i].isGlobalPriority = plugin.isGlobalPriority;
loot.game.plugins[i].masterlist = plugin.masterlist;
loot.game.plugins[i].messages = plugin.messages;
loot.game.plugins[i].modPriority = plugin.modPriority;
loot.game.plugins[i].tags = plugin.tags;
break;
}
}
});
closeProgressDialog();
}).catch(processCefError);
}
function sortUIElements(pluginNames) {
/* pluginNames is an array of plugin names in their sorted order. Rearrange
the plugin cards and nav entries to match it. */
var main = document.getElementsByTagName('main')[0];
var pluginsNav = document.getElementById('pluginsNav');
var entries = pluginsNav.children;
if (main.getElementsByTagName('plugin-card').length != entries.length) {
throw Error("Error: Number of plugins in sidebar doesn't match number of plugins in main area!");
}
var lastCard;
var lastLi;
pluginNames.forEach(function(name, index){
/* Just removing and appending everything is the easiest way, but
redrawing the UI so much is expensive. Try to only move elements
that need to be moved.
The previous plugin in pluginNames has already been sorted, so check
if this plugin's card is after the previous plugin's card, and if so
do nothing.
The first plugin is a special case.
*/
var card = document.getElementById(name.replace(/\s+/g, ''));
var move = false;
if (index == 0) {
/* Special case. */
move = (card != card.parentElement.getElementsByTagName('plugin-card')[0]);
} else {
move = (card.previousElementSibling.getName() != pluginNames[index-1]);
}
if (move) {
/* Also need to move plugin sidebar entry. */
var li;
for (var i = 0; i < entries.length; ++i) {
if (entries[i].getElementsByTagName('a')[0].textContent == name) {
li = entries[i];
}
}
/* Easiest just to remove them and add them on at the end. */
main.removeChild(card);
if (lastCard) {
main.insertBefore(card, lastCard.nextElementSibling);
} else {
main.insertBefore(card, main.getElementsByTagName('plugin-card')[0]);
}
pluginsNav.removeChild(li);
if (lastLi) {
pluginsNav.insertBefore(li, lastLi.nextElementSibling);
} else {
pluginsNav.insertBefore(li, pluginsNav.firstElementChild);
}
lastLi = li;
} else {
lastLi = entries[index];
}
lastCard = card;
});
}
function openProgressDialog() {
var progressDialog = document.getElementById('progressDialog');
if (!progressDialog.open) {
progressDialog.showModal();
}
}
function updateProgressDialog(message) {
var progressDialog = document.getElementById('progressDialog');
progressDialog.getElementsByTagName('h1')[0].textContent = message;
}
function closeProgressDialog() {
var progressDialog = document.getElementById('progressDialog');
if (progressDialog.open) {
progressDialog.close();
}
}
function sortPlugins(evt) {
var mlistUpdate;
if (loot.settings.updateMasterlist) {
mlistUpdate = updateMasterlist(evt);
} else {
mlistUpdate = new Promise(function(resolve, reject){
resolve('');
});
}
mlistUpdate.then(function(){
updateProgressDialog('Sorting plugins...');
openProgressDialog();
loot.query('sortPlugins').then(JSON.parse).then(function(result){
if (result) {
var loadOrder = [];
result.forEach(function(plugin){
loadOrder.push(plugin.name);
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == plugin.name) {
loot.game.plugins[i].crc = plugin.crc;
loot.game.plugins[i].isDummy = plugin.isDummy;
loot.game.plugins[i].messages = plugin.messages;
loot.game.plugins[i].tags = plugin.tags;
loot.game.plugins[i].isDirty = plugin.isDirty;
break;
}
}
});
if (loot.settings.neverTellMeTheOdds) {
/* Array shuffler from <https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array-in-javascript> */
for(var j, x, i = loadOrder.length; i; j = Math.floor(Math.random() * i), x = loadOrder[--i], loadOrder[i] = loadOrder[j], loadOrder[j] = x);
}
/* Record the previous order in case the user cancels sorting. */
/* Start at 2 to skip summary and general messages. */
var cards = document.getElementsByTagName('main')[0].getElementsByTagName('plugin-card');
loot.newLoadOrder = loadOrder;
loot.lastLoadOrder = [];
for (var i = 0; i < cards.length; ++i) {
loot.lastLoadOrder.push(cards[i].getElementsByTagName('h1')[0].textContent);
}
/* Now update the UI for the new order. */
sortUIElements(loadOrder);
/* Now hide the masterlist update buttons, and display the accept and
cancel sort buttons. */
hideElement(document.getElementById('updateMasterlistButton'));
hideElement(document.getElementById('sortButton'));
showElement(document.getElementById('applySortButton'));
showElement(document.getElementById('cancelSortButton'));
closeProgressDialog();
}
}).catch(processCefError);
}).catch(processCefError);
}
function applySort(evt) {
var request = JSON.stringify({
name: 'applySort',
args: [
loot.newLoadOrder
]
});
return loot.query(request).then(function(result){
/* Remove old load order storage. */
delete loot.lastLoadOrder;
delete loot.newLoadOrder;
/* Now show the masterlist update buttons, and hide the accept and
cancel sort buttons. */
showElement(document.getElementById('updateMasterlistButton'));
showElement(document.getElementById('sortButton'));
hideElement(document.getElementById('applySortButton'));
hideElement(document.getElementById('cancelSortButton'));
}).catch(processCefError);
}
function cancelSort(evt) {
return loot.query('cancelSort').then(function(){
/* Sort UI elements again according to stored old load order. */
sortUIElements(loot.lastLoadOrder);
delete loot.lastLoadOrder;
delete loot.newLoadOrder;
/* Now show the masterlist update buttons, and hide the accept and
cancel sort buttons. */
showElement(document.getElementById('updateMasterlistButton'));
showElement(document.getElementById('sortButton'));
hideElement(document.getElementById('applySortButton'));
hideElement(document.getElementById('cancelSortButton'));
}).catch(processCefError);
}
function redatePlugins(evt) {
if (evt.target.classList.contains('disabled')) {
return;
}
showMessageDialog('Redate Plugins', 'This feature is provided so that modders using the Creation Kit may set the load order it uses. A side-effect is that any subscribed Steam Workshop mods will be re-downloaded by Steam. Do you wish to continue?', function(result){
if (result) {
loot.query('redatePlugins').then(function(response){
showMessageBox('info', 'Redate Plugins', 'Plugins were successfully redated.');
}).catch(processCefError);
}
});
//showMessageBox('info', 'Redate Plugins', 'Plugins were successfully redated.');
}
function clearAllMetadata(evt) {
showMessageDialog('Clear All Metadata', 'Are you sure you want to clear all existing user-added metadata from all plugins?', function(result){
if (result) {
loot.query('clearAllMetadata').then(JSON.parse).then(function(result){
if (result) {
/* Need to empty the UI-side user metadata. */
result.forEach(function(plugin){
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == plugin.name) {
loot.game.plugins[i].userlist = undefined;
loot.game.plugins[i].modPriority = plugin.modPriority;
loot.game.plugins[i].isGlobalPriority = plugin.isGlobalPriority;
loot.game.plugins[i].messages = plugin.messages;
loot.game.plugins[i].tags = plugin.tags;
loot.game.plugins[i].isDirty = plugin.isDirty;
break;
}
}
});
}
}).catch(processCefError);
}
});
}
function copyContent(evt) {
var messages = [];
var plugins = [];
if (loot.game) {
loot.game.globalMessages.forEach(function(message){
var m = {};
messages.push({
type: message.type,
content: message.content[0].str
});
});
loot.game.plugins.forEach(function(plugin){
plugins.push({
name: plugin.name,
crc: plugin.crc,
version: plugin.version,
isActive: plugin.isActive,
isDummy: plugin.isDummy,
loadsBSA: plugin.loadsBSA,
modPriority: plugin.modPriority,
isGlobalPriority: plugin.isGlobalPriority,
messages: plugin.messages,
tags: plugin.tags,
isDirty: plugin.isDirty
});
});
} else {
var message = document.getElementById('generalMessagesList').getElementsByTagName('ul')[0].firstElementChild;
if (message) {
messages.push({
type: 'error',
content: message.textContent
});
}
}
var request = JSON.stringify({
name: 'copyContent',
args: [{
messages: messages,
plugins: plugins
}]
});
loot.query(request).catch(processCefError);
}
function areSettingsValid() {
/* Validate inputs individually. */
var inputs = document.getElementById('settings').getElementsByTagName('input');
for (var i = 0; i < inputs.length; ++i) {
if (!inputs[i].checkValidity()) {
return false;
console.log(inputs[i]);
}
}
return true;
}
/* Close any open menus. */
function onBodyClick(evt) {
var toolbarMenus = document.querySelectorAll('header > ol > li > ol');
for (var i = 0; i < toolbarMenus.length; ++i) {
if (!evt.detail.newMenu || evt.detail.newMenu != toolbarMenus[i]) {
hideElement(toolbarMenus[i]);
}
}
var pluginMenus = document.getElementsByTagName('plugin-menu');
for (var i = 0; i < pluginMenus.length; ++i) {
if (!evt.detail.newMenu || evt.detail.newMenu != pluginMenus[i]) {
pluginMenus[i].parentElement.removeChild(pluginMenus[i]);
}
}
}
function openMenu(evt) {
if (!isVisible(evt.currentTarget.firstElementChild)) {
showElement(evt.currentTarget.firstElementChild)
/* To prevent the click event closing this menu just after it was
opened, stop the current event and send off a new one. */
evt.stopPropagation();
document.body.dispatchEvent(new CustomEvent('click', {
bubbles: true,
detail: {
newMenu: evt.currentTarget.firstElementChild
}
}));
}
}
function toggleFiltersList(evt) {
var filters = document.getElementById('filters');
var plugins = document.getElementById('pluginsNav');
if (isVisible(filters)) {
hideElement(filters);
showElement(plugins);
} else {
showElement(filters);
hideElement(plugins);
}
}
function closeAboutDialog(evt) {
evt.target.parentElement.close();
}
function showAboutDialog(evt) {
document.getElementById('about').showModal();
}
function closeSettingsDialog(evt) {
if (!areSettingsValid()) {
return;
}
var dialog = evt.target.parentElement.parentElement;
if (evt.target.classList.contains('accept')) {
/* Update the JS variable values. */
var settings = {
enableDebugLogging: document.getElementById('enableDebugLogging').checked,
game: document.getElementById('defaultGameSelect').value,
games: document.getElementById('gameTable').getRowsData(false),
language: document.getElementById('languageSelect').value,
lastGame: loot.settings.lastGame,
updateMasterlist: document.getElementById('updateMasterlist').checked,
filters: loot.settings.filters,
autoRefresh: document.getElementById('autoRefresh').checked,
};
/* There's no easy way to check if an event listener has been added
already, so just remove the window focus listener, and add it again
if auto-refresh is enabled. */
window.removeEventListener('focus', onFocus, false);
if (settings.autoRefresh) {
window.addEventListener('focus', onFocus, false);
}
/* Send the settings back to the C++ side. */
var request = JSON.stringify({
name: 'closeSettings',
args: [
settings
]
});
loot.query(request).then(function(result){
try {
loot.installedGames = JSON.parse(result);
} catch (e) {
console.log(e);
console.log('getInstalledGames response: ' + results[1]);
}
loot.settings = settings;
}).catch(processCefError);
} else {
/* Re-apply the existing settings to the settings dialog elements. */
loot.updateSettingsUI();
}
dialog.close();
}
function showSettingsDialog(evt) {
document.getElementById('settings').showModal();
}
function getDialogParent(element) {
var element = element.parentElement;
while (element) {
if (element.nodeName == 'DIALOG') {
return element;
}
element = element.parentElement;
}
return null;
}
function showHoverText(evt) {
hideHoverText(evt);
var hoverText = document.createElement('div');
hoverText.id = 'hoverText';
hoverText.textContent = evt.target.title;
if (!evt.target.id) {
evt.target.id = 'hoverTarget';
}
hoverText.setAttribute('data-target', evt.target.id);
var rect = evt.target.getBoundingClientRect();
hoverText.style.left = (rect.left + evt.target.offsetWidth/2) + 'px';
hoverText.style.top = (rect.bottom + 10) + 'px';
var dialog = getDialogParent(evt.target);
if (dialog) {
dialog.appendChild(hoverText);
} else {
document.body.appendChild(hoverText);
}
}
function hideHoverText(evt) {
var hoverText = document.getElementById('hoverText');
if (hoverText) {
var hoverTarget = document.getElementById('hoverTarget');
if (hoverTarget) {
hoverTarget.id = '';
}
hoverText.parentElement.removeChild(hoverText);
}
}
function startSearch(evt) {
if (evt.target.value == '') {
loot.query('cancelFind').then(function(result){
evt.target.focus();
}).catch(processCefError);
} else {
var request = JSON.stringify({
name: 'find',
args: [
evt.target.value
]
});
loot.query(request).then(function(result){
evt.target.focus();
}).catch(processCefError);
}
}
function focusSearch(evt) {
if (evt.ctrlKey && evt.keyCode == 70) { //'f'
document.getElementById('searchBox').focus();
}
}
function closeFirstRunDialog(evt) {
evt.target.parentElement.close();
}
function handleUnappliedChangesClose() {
showMessageDialog('Unapplied Sorting Changes', 'You have not yet applied or cancelled your sorted load order. Apply your load order before quitting?', function(result){
if (result) {
applySort().then(function(){
window.close();
});
} else {
cancelSort().then(function(){
window.close();
});
}
});
}
function setupEventHandlers() {
var elements;
/*Set up filter value and CSS setting storage read/write handlers.*/
elements = document.getElementById('filters').getElementsByTagName('input');
for (var i = 0; i < elements.length; ++i) {
elements[i].addEventListener('click', saveFilterState, false);
}
/*Set up handlers for filters.*/
document.getElementById('hideVersionNumbers').addEventListener('click', toggleDisplayCSS, false);
document.getElementById('hideCRCs').addEventListener('click', toggleDisplayCSS, false);
document.getElementById('hideBashTags').addEventListener('click', toggleDisplayCSS, false);
document.getElementById('hideNotes').addEventListener('click', applyFilters, false);
document.getElementById('hideDoNotCleanMessages').addEventListener('click', applyFilters, false);
document.getElementById('hideInactivePluginMessages').addEventListener('click', applyFilters, false);
document.getElementById('hideAllPluginMessages').addEventListener('click', applyFilters, false);
document.getElementById('hideMessagelessPlugins').addEventListener('click', applyFilters, false);
/* Set up handlers for buttons. */
document.getElementById('fileMenu').addEventListener('click', openMenu, false);
document.getElementById('redatePluginsButton').addEventListener('click', redatePlugins, false);
document.getElementById('openLogButton').addEventListener('click', openLogLocation, false);
document.getElementById('wipeUserlistButton').addEventListener('click', clearAllMetadata, false);
document.getElementById('copyContentButton').addEventListener('click', copyContent, false);
document.getElementById('refreshContentButton').addEventListener('click', onFocus, false);
document.getElementById('gameMenu').addEventListener('click', openMenu, false);
document.getElementById('settingsButton').addEventListener('click', showSettingsDialog, false);
document.getElementById('helpMenu').addEventListener('click', openMenu, false);
document.getElementById('helpButton').addEventListener('click', openReadme, false);
document.getElementById('aboutButton').addEventListener('click', showAboutDialog, false);
document.getElementById('updateMasterlistButton').addEventListener('click', updateMasterlist, false);
document.getElementById('sortButton').addEventListener('click', sortPlugins, false);
document.getElementById('applySortButton').addEventListener('click', applySort, false);
document.getElementById('cancelSortButton').addEventListener('click', cancelSort, false);
document.getElementById('filtersToggle').addEventListener('click', toggleFiltersList, false);
/* Set up event handlers for settings dialog. */
var settings = document.getElementById('settings');
settings.getElementsByClassName('accept')[0].addEventListener('click', closeSettingsDialog, false);
settings.getElementsByClassName('cancel')[0].addEventListener('click', closeSettingsDialog, false);
/* Set up about dialog handler. */
document.getElementById('about').getElementsByTagName('button')[0].addEventListener('click', closeAboutDialog, false);
/* Set up event handler for closing the first-run dialog. */
document.getElementById('firstRun').getElementsByTagName('button')[0].addEventListener('click', closeFirstRunDialog, false);
/* Set up event handler for hover text. */
var hoverTargets = document.querySelectorAll('[title]');
for (var i = 0; i < hoverTargets.length; ++i) {
hoverTargets[i].addEventListener('mouseenter', showHoverText, false);
hoverTargets[i].addEventListener('mouseleave', hideHoverText, false);
}
/* Set up event handlers for content search. */
var searchBox = document.getElementById('searchBox');
searchBox.addEventListener('input', startSearch, false);
searchBox.addEventListener('search', startSearch, false);
window.addEventListener('keyup', focusSearch, false);
/* Set up handler for closing menus. */
document.body.addEventListener('click', onBodyClick, false);
}
function initVars() {
loot.query('getVersion').then(function(result){
try {
loot.version = JSON.parse(result);
document.getElementById('LOOTVersion').textContent = loot.version;
document.getElementById('firstTimeLootVersion').textContent = loot.version;
} catch (e) {
console.log(e);
console.log('Response: ' + result);
}
}).catch(processCefError);
loot.query('getLanguages').then(function(result){
try {
loot.languages = JSON.parse(result);
/* Now fill in language options. */
var settingsLangSelect = document.getElementById('languageSelect');
var messageLangSelect = document.getElementById('messageRow').content.querySelector('.language');
for (var i = 0; i < loot.languages.length; ++i) {
var option = document.createElement('option');
option.value = loot.languages[i].locale;
option.textContent = loot.languages[i].name;
settingsLangSelect.appendChild(option);
messageLangSelect.appendChild(option.cloneNode(true));
}
} catch (e) {
console.log(e);
console.log('Response: ' + result);
}
}).catch(processCefError);
loot.query('getInitErrors').then(JSON.parse).then(function(result){
if (result) {
var generalMessagesList = document.getElementById('generalMessages').getElementsByTagName('ul')[0];
result.forEach(function(message){
var li = document.createElement('li');
li.className = 'error';
/* Use the Marked library for Markdown formatting support. */
li.innerHTML = marked(message);
generalMessagesList.appendChild(li);
});
document.getElementById('filterTotalMessageNo').textContent = result.length;
document.getElementById('totalMessageNo').textContent = result.length;
document.getElementById('totalErrorNo').textContent = result.length;
}
var parallelPromises = [
loot.query('getGameTypes'),
loot.query('getInstalledGames'),
loot.query('getSettings'),
];
if (!result) {
parallelPromises.push(loot.query('getGameData'));
}
updateProgressDialog('Initialising user interface...');
openProgressDialog();
Promise.all(parallelPromises).then(function(results) {
try {
loot.gameTypes = JSON.parse(results[0]);
} catch (e) {
console.log(e);
console.log('getGameTypes response: ' + results[0]);
}
/* Fill in game row template's game type options. */
var select = document.getElementById('gameRow').content.querySelector('select');
for (var j = 0; j < loot.gameTypes.length; ++j) {
var option = document.createElement('option');
option.value = loot.gameTypes[j];
option.textContent = loot.gameTypes[j];
select.appendChild(option);
}
try {
loot.installedGames = JSON.parse(results[1]);
} catch (e) {
console.log(e);
console.log('getInstalledGames response: ' + results[1]);
}
if (results.length > 3) {
try {
var game = JSON.parse(results[3], jsonToPlugin);
loot.game.folder = game.folder;
loot.game.masterlist = game.masterlist;
loot.game.globalMessages = game.globalMessages;
loot.game.plugins = game.plugins;
} catch (e) {
console.log(e);
console.log('getGameData response: ' + results[3]);
}
}
try {
loot.settings = JSON.parse(results[2]);
applySavedFilters();
} catch (e) {
console.log(e);
console.log('getSettings response: ' + results[2]);
}
if (results.length == 3) {
document.getElementById('settingsButton').click();
}
closeProgressDialog();
/* So much easier than trying to set up my own C++ message loop to listen
to window messages looking for a focus change. */
if (loot.settings.autoRefresh) {
window.addEventListener('focus', onFocus, false);
}
if (!loot.settings.lastVersion || loot.settings.lastVersion != loot.version) {
document.getElementById('firstRun').showModal();
}
}).catch(processCefError);
}).catch(processCefError);
}
function onFocus(evt) {
/* Send a query for updated load order and plugin header info. */
updateProgressDialog('Refreshing data...');
openProgressDialog();
loot.query('getGameData').then(function(result){
/* Parse the data sent from C++. */
try {
/* We don't want the plugin info creating cards, so don't convert
to plugin objects. */
var gameInfo = JSON.parse(result);
} catch (e) {
console.log(e);
console.log('getGameData response: ' + result);
}
/* Now overwrite plugin data with the newly sent data. Also update
card and li vars as they were unset when the game was switched
from before. */
var pluginNames = [];
gameInfo.plugins.forEach(function(plugin){
var foundPlugin = false;
for (var i = 0; i < loot.game.plugins.length; ++i) {
if (loot.game.plugins[i].name == plugin.name) {
loot.game.plugins[i].isActive = plugin.isActive;
loot.game.plugins[i].isDummy = plugin.isDummy;
loot.game.plugins[i].loadsBSA = plugin.loadsBSA;
loot.game.plugins[i].crc = plugin.crc;
loot.game.plugins[i].version = plugin.version;
loot.game.plugins[i].modPriority = plugin.modPriority;
loot.game.plugins[i].isGlobalPriority = plugin.isGlobalPriority;
loot.game.plugins[i].messages = plugin.messages;
loot.game.plugins[i].tags = plugin.tags;
loot.game.plugins[i].isDirty = plugin.isDirty;
foundPlugin = true;
break;
}
}
if (!foundPlugin) {
/* A new plugin. */
loot.game.plugins.push(new Plugin(plugin));
}
pluginNames.push(plugin.name);
});
for (var i = 0; i < loot.game.plugins.length;) {
var foundPlugin = false;
for (var j = 0; j < pluginNames.length; ++j) {
if (loot.game.plugins[i].name == pluginNames[j]) {
foundPlugin = true;
break;
}
}
if (!foundPlugin) {
/* Remove plugin. */
loot.game.plugins[i].card.parentElement.removeChild(loot.game.plugins[i].card);
loot.game.plugins[i].li.parentElement.removeChild(loot.game.plugins[i].li);
loot.game.plugins.splice(i, 1);
} else {
++i;
}
}
/* Now sort the cards to match the load order. */
sortUIElements(pluginNames);
/* Reapply filters. */
applyFilters();
closeProgressDialog();
}).catch(processCefError);
}
require.config({
baseUrl: "js",
});
require(['marked', 'order!custom', 'order!plugin'], function(response) {
marked = response;
/* Make sure settings are what I want. */
marked.setOptions({
gfm: true,
tables: true,
sanitize: true
});
setupEventHandlers();
initVars();
});