diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..a7b620b8 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,23 @@ +env: + es6: true + mocha: true + amd: true + node: true + browser: true + +ecmaFeatures: + modules: false + +globals: + loot: false + should: false + +extends: + - "eslint:recommended" + - "airbnb/base" + +rules: + strict: + - 2 + - global + guard-for-in: 0 diff --git a/.travis.yml b/.travis.yml index 4b10c77c..7e9dab43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - libssl-dev - gcc-5 - g++-5 + sauce_connect: true install: # Use GCC 5. @@ -27,6 +28,16 @@ install: # Run install step script to download and build dependencies. - chmod +x scripts/install-step.travis.sh - ./scripts/install-step.travis.sh + # Install the latest stable Node.js. The default Node.js version is too old + # for some of the JavaScript syntax used. + - npm install -g nvm + - nvm install node + # Install Node.js dependencies + - npm install + # Add local binary path to PATH + - export PATH="./node_modules/.bin:$PATH" + # Install runtime dependencies using Bower + - bower install before_script: # Build Google Test @@ -46,7 +57,9 @@ before_script: # Travis machines are 64 bit, and the dependencies use dynamic linking. - cmake .. -DPROJECT_ARCH=64 -DPROJECT_STATIC_RUNTIME=OFF -DBUILD_SHARED_LIBS=OFF -DGTEST_ROOT=../../googletest-release-1.7.0 -script: make tests && ./tests +script: + - npm test + - make tests && ./tests before_deploy: # Build the metadata validator @@ -69,3 +82,8 @@ notifications: - "chat.freenode.net#loot" use_notice: true skip_join: true + +env: + global: + - secure: OWgDkff/b+0eKk/P75rBnecaPDv1E/gMkCzsTUUtofmzj+sttpMUsh8VOVUvckGs5m3fRwRhc/t99gR4FuDtMOVE5JZ8CqZn7tcv2wNAspQ613pS0nWyIX/IUbyF+gjtPW2dWy81hKVtTP4yFvUn1Gk+JRjJLSyxM53Xt6oTOlY= + - secure: XpGbqizjWfaGiWDkrcW4UhSW8ijm00jgSo2qkthza3QmHQ3C0Vye3gQjjITCq+y6izQTPa46fxaI+Ozligyh4aQ1G8SpCa77tXMBfvo31led+s81FlAOSVGDJ/2eDOoawFuj5dTKTPR1zLniDYH8AqG8EDZuLoK6fhx7CchU2Yw= diff --git a/bower.json b/bower.json index 1a57f4da..6895c6a3 100644 --- a/bower.json +++ b/bower.json @@ -37,5 +37,22 @@ "jed-gettext-parser": "~1.0.0", "paper-toggle-button": "Polymer/paper-toggle-button#~0.5.4", "core-overlay": "WrinklyNinja/core-overlay#fix-focus-self-as-next" + }, + "resolutions": { + "polymer": "^0.5", + "core-icons": "^0.5", + "core-icon": "^0.5", + "core-collapse": "^0.5", + "core-focusable": "^0.5", + "core-icon-button": "^0.5", + "core-resizable": "^0.5", + "core-iconset-svg": "^0.5", + "core-dropdown": "null-dimensions-positions-fix", + "core-transition": "^0.5", + "core-overlay": "fix-focus-self-as-next", + "webcomponentsjs": "^0.7.18", + "core-iconset": "^0.5", + "core-selection": "^0.5", + "core-meta": "^0.5" } } diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 00000000..a4122449 --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,39 @@ +'use strict'; +module.exports = (grunt) => { + grunt.initConfig({ + connect: { + server: { + options: { + base: '', + port: 9999, + }, + }, + }, + 'saucelabs-mocha': { + all: { + options: { + urls: ['http://127.0.0.1:9999/src/tests/gui/html/js/test.html'], + build: process.env.TRAVIS_JOB_ID, + throttled: 3, + browsers: [{ + browserName: 'chrome', + platform: 'Windows 10', + version: '47', + }], + testname: 'LOOT UI JS Tests', + }, + }, + }, + watch: {}, + }); + + // Loading dependencies + for (const key in grunt.file.readJSON('package.json').devDependencies) { + if (key !== 'grunt' && key.indexOf('grunt') === 0) { + grunt.loadNpmTasks(key); + } + } + + grunt.registerTask('dev', ['connect', 'watch']); + grunt.registerTask('test', ['connect', 'saucelabs-mocha']); +}; diff --git a/package.json b/package.json index 2ee8b1f9..609731bb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,19 @@ "private": true, "devDependencies": { "bower": "^1.7.1", + "eslint": "^1.10.3", + "eslint-config-airbnb": "^2.0.0", "fs-extra": "^0.26.2", + "grunt": "^0.4.5", + "grunt-cli": "^0.1.13", + "grunt-contrib-connect": "^0.11.2", + "grunt-contrib-watch": "^0.6.1", + "grunt-saucelabs": "^8.6.2", + "mocha": "^2.3.4", + "should": "^8.0.1", "vulcanize": "^0.7.11" + }, + "scripts": { + "test": "grunt test" } } diff --git a/scripts/archive.js b/scripts/archive.js index 21b15bd8..1dadfee4 100644 --- a/scripts/archive.js +++ b/scripts/archive.js @@ -2,206 +2,203 @@ // Archive packaging script. Takes one argument, which is the path to the // repository's root. Requires 7-zip and Git to be installed, and Git to be // available on the system path. +'use strict'; +const childProcess = require('child_process'); +const path = require('path'); +const fs = require('fs-extra'); +const os = require('os'); +const helpers = require('./helpers'); -var child_process = require('child_process'); -var path = require('path'); -var fs = require('fs-extra'); -var os = require('os'); -var helpers = require('./helpers'); - -function vulcanize() { - return child_process.execFileSync('node', [ - 'scripts/vulcanize.js', - root_path - ]); +function vulcanize(rootPath) { + return childProcess.execFileSync('node', [ + 'scripts/vulcanize.js', + rootPath, + ]); } function getGitDescription() { - return String(child_process.execFileSync('git', [ - 'describe', - '--tags', - '--long' - ])).slice(0, -1); + return String(childProcess.execFileSync('git', [ + 'describe', + '--tags', + '--long', + ])).slice(0, -1); } -function compress(source_path, dest_path) { - var sevenzip_path = ''; - if (os.platform() == 'win32') { - sevenzip_path = path.join('C:\\', 'Program Files', '7-Zip', '7z.exe'); - } else { - sevenzip_path = '/usr/bin/7z'; +function compress(sourcePath, destPath) { + let sevenzipPath = ''; + if (os.platform() === 'win32') { + sevenzipPath = path.join('C:\\', 'Program Files', '7-Zip', '7z.exe'); + } else { + sevenzipPath = '/usr/bin/7z'; + } + + // First remove any existing archive. + fs.removeSync(destPath); + + // The last argument must have a leading dot for the subdirectory not to + // be present in the archive, but path.join removes it, so it's prefixed. + return childProcess.execFileSync(sevenzipPath, [ + 'a', + '-r', + destPath, + '.' + path.sep + path.join(sourcePath, '*'), + ]); +} + +function createAppArchive(rootPath, releasePath, tempPath, destPath) { + // Ensure that the output directory is empty. + fs.emptyDirSync(tempPath); + + // Copy LOOT exectuable and CEF files. + let binaries = []; + if (os.platform() === 'win32') { + binaries = [ + 'LOOT.exe', + 'd3dcompiler_47.dll', + 'libEGL.dll', + 'libGLESv2.dll', + 'libcef.dll', + 'natives_blob.bin', + 'snapshot_blob.bin', + 'cef.pak', + 'cef_100_percent.pak', + 'cef_200_percent.pak', + 'devtools_resources.pak', + 'icudtl.dat', + ]; + + if (helpers.fileExists(path.join(releasePath, 'wow_helper.exe'))) { + binaries.push('wow_helper.exe'); } + } else { + binaries = [ + 'LOOT', + 'chrome-sandbox', + 'libcef.so', + 'natives_blob.bin', + 'snapshot_blob.bin', + 'cef.pak', + 'cef_100_percent.pak', + 'cef_200_percent.pak', + 'devtools_resources.pak', + 'icudtl.dat', + ]; + } + binaries.forEach((file) => { + fs.copySync( + path.join(releasePath, file), + path.join(tempPath, file) + ); + }); - // First remove any existing archive. - fs.removeSync(dest_path); + // CEF locale file. + fs.mkdirsSync(path.join(tempPath, 'resources', 'l10n')); + fs.copySync( + path.join(releasePath, 'resources', 'l10n', 'en-US.pak'), + path.join(tempPath, 'resources', 'l10n', 'en-US.pak') + ); - // The last argument must have a leading dot for the subdirectory not to - // be present in the archive, but path.join removes it, so it's prefixed. - return child_process.execFileSync(sevenzip_path, [ - 'a', - '-r', - dest_path, - '.' + path.sep + path.join(source_path, '*') - ]); + // Translation files. + [ + 'es', 'ru', 'fr', 'zh_CN', 'pl', 'pt_BR', 'fi', 'de', 'da', 'ko', + ].forEach((lang) => { + fs.mkdirsSync(path.join(tempPath, 'resources', 'l10n', lang, 'LC_MESSAGES')); + fs.copySync( + path.join(rootPath, 'resources', 'l10n', lang, 'LC_MESSAGES', 'loot.mo'), + path.join(tempPath, 'resources', 'l10n', lang, 'LC_MESSAGES', 'loot.mo') + ); + }); + + // UI files. + fs.mkdirsSync(path.join(tempPath, 'resources', 'ui', 'css')); + fs.copySync( + path.join(releasePath, 'resources', 'ui', 'index.html'), + path.join(tempPath, 'resources', 'ui', 'index.html') + ); + fs.copySync( + path.join(rootPath, 'resources', 'ui', 'css', 'dark-theme.css'), + path.join(tempPath, 'resources', 'ui', 'css', 'dark-theme.css') + ); + fs.copySync( + path.join(rootPath, 'resources', 'ui', 'fonts'), + path.join(tempPath, 'resources', 'ui', 'fonts') + ); + + // Docs. + fs.mkdirsSync(path.join(tempPath, 'docs')); + [ + 'images', + 'licenses', + 'LOOT Metadata Syntax.html', + 'LOOT Readme.html', + ].forEach((item) => { + fs.copySync( + path.join(rootPath, 'docs', item), + path.join(tempPath, 'docs', item) + ); + }); + + // Now compress the folder to a 7-zip archive. + compress(tempPath, destPath); + + // Finally, delete the temporary folder. + fs.removeSync(tempPath); } -function createAppArchive(release_path, dest_path) { - // Ensure that the output directory is empty. - fs.emptyDirSync(temp_path); +function createApiArchive(rootPath, binaryPath, tempPath, destPath) { + // Ensure that the output directory is empty. + fs.emptyDirSync(tempPath); - // Copy LOOT exectuable and CEF files. - var binaries = []; - if (os.platform() == 'win32') { - binaries = [ - 'LOOT.exe', - 'd3dcompiler_47.dll', - 'libEGL.dll', - 'libGLESv2.dll', - 'libcef.dll', - 'natives_blob.bin', - 'snapshot_blob.bin', - 'cef.pak', - 'cef_100_percent.pak', - 'cef_200_percent.pak', - 'devtools_resources.pak', - 'icudtl.dat' - ]; + // API binary/binaries. + fs.copySync( + binaryPath, + path.join(tempPath, path.basename(binaryPath)) + ); - if (helpers.fileExists(path.join(release_path, 'wow_helper.exe'))) { - binaries.push('wow_helper.exe'); - } - } else { - binaries = [ - 'LOOT', - 'chrome-sandbox', - 'libcef.so', - 'natives_blob.bin', - 'snapshot_blob.bin', - 'cef.pak', - 'cef_100_percent.pak', - 'cef_200_percent.pak', - 'devtools_resources.pak', - 'icudtl.dat' - ]; - } - binaries.forEach(function(file){ - fs.copySync( - path.join(release_path, file), - path.join(temp_path, file) - ); - }); + // API header file. + fs.mkdirsSync(path.join(tempPath, 'include', 'loot')); + fs.copySync( + path.join(rootPath, 'include', 'loot', 'api.h'), + path.join(tempPath, 'include', 'loot', 'api.h') + ); - // CEF locale file. - fs.mkdirsSync(path.join(temp_path, 'resources', 'l10n')); - fs.copySync( - path.join(release_path, 'resources', 'l10n', 'en-US.pak'), - path.join(temp_path, 'resources', 'l10n', 'en-US.pak') - ); + // Docs. + fs.mkdirsSync(path.join(tempPath, 'docs')); + fs.copySync( + path.join(rootPath, 'docs', 'latex', 'refman.pdf'), + path.join(tempPath, 'docs', 'readme.pdf') + ); + fs.copySync( + path.join(rootPath, 'docs', 'licenses'), + path.join(tempPath, 'docs', 'licenses') + ); - // Translation files. - [ - 'es', 'ru', 'fr', 'zh_CN', 'pl', - 'pt_BR', 'fi', 'de', 'da', 'ko' - ].forEach(function(lang){ - fs.mkdirsSync(path.join(temp_path, 'resources', 'l10n', lang, 'LC_MESSAGES')); - fs.copySync( - path.join(root_path, 'resources', 'l10n', lang, 'LC_MESSAGES', 'loot.mo'), - path.join(temp_path, 'resources', 'l10n', lang, 'LC_MESSAGES', 'loot.mo') - ); - }); + // Now compress the folder to a 7-zip archive. + compress(tempPath, destPath); - // UI files. - fs.mkdirsSync(path.join(temp_path, 'resources', 'ui', 'css')); - fs.copySync( - path.join(release_path, 'resources', 'ui', 'index.html'), - path.join(temp_path, 'resources', 'ui', 'index.html') - ); - fs.copySync( - path.join(root_path, 'resources', 'ui', 'css', 'dark-theme.css'), - path.join(temp_path, 'resources', 'ui', 'css', 'dark-theme.css') - ); - fs.copySync( - path.join(root_path, 'resources', 'ui', 'fonts'), - path.join(temp_path, 'resources', 'ui', 'fonts') - ); - - // Docs. - fs.mkdirsSync(path.join(temp_path, 'docs')); - [ - 'images', - 'licenses', - 'LOOT Metadata Syntax.html', - 'LOOT Readme.html' - ].forEach(function(item){ - fs.copySync( - path.join(root_path, 'docs', item), - path.join(temp_path, 'docs', item) - ); - }); - - // Now compress the folder to a 7-zip archive. - compress(temp_path, dest_path); - - // Finally, delete the temporary folder. - fs.removeSync(temp_path); + // Finally, delete the temporary folder. + fs.removeSync(tempPath); } -function createApiArchive(binary_path, dest_path) { - // Ensure that the output directory is empty. - fs.emptyDirSync(temp_path); +let rootPath = '.'; +if (process.argv.length > 2) { + rootPath = process.argv[2]; +} +const tempPath = path.join(rootPath, 'build', 'archive.tmp'); - // API binary/binaries. - fs.copySync( - binary_path, - path.join(temp_path, path.basename(binary_path)) - ); +const gitDesc = getGitDescription(); +vulcanize(rootPath); - // API header file. - fs.mkdirsSync(path.join(temp_path, 'include', 'loot')); - fs.copySync( - path.join(root_path, 'include', 'loot', 'api.h'), - path.join(temp_path, 'include', 'loot', 'api.h') - ); - - // Docs. - fs.mkdirsSync(path.join(temp_path, 'docs')); - fs.copySync( - path.join(root_path, 'docs', 'latex', 'refman.pdf'), - path.join(temp_path, 'docs', 'readme.pdf') - ); - fs.copySync( - path.join(root_path, 'docs', 'licenses'), - path.join(temp_path, 'docs', 'licenses') - ); - - - // Now compress the folder to a 7-zip archive. - compress(temp_path, dest_path); - - // Finally, delete the temporary folder. - fs.removeSync(temp_path); +const releasePaths = helpers.getAppReleasePaths(rootPath); +for (let i = 0; i < releasePaths.length; ++i) { + if (releasePaths[i].label) { + createAppArchive(rootPath, releasePaths[i].path, tempPath, path.join(rootPath, 'build', 'LOOT ' + gitDesc + ' (' + releasePaths[i].label + ').7z')); + } else { + createAppArchive(releasePaths[i].path, path.join(rootPath, 'build', 'LOOT ' + gitDesc + '.7z')); + } } -if (process.argv.length < 3) { - var root_path = '.'; -} else { - var root_path = process.argv[2]; -} -var temp_path = path.join(root_path, 'build', 'archive.tmp'); - -var git_desc = getGitDescription(); -vulcanize(); - -var release_paths = helpers.getAppReleasePaths(root_path); -for (var i = 0; i < release_paths.length; ++i) { - if (release_paths[i].label) { - createAppArchive(release_paths[i].path, path.join(root_path, 'build', 'LOOT ' + git_desc + ' (' + release_paths[i].label + ').7z')); - } else { - createAppArchive(release_paths[i].path, path.join(root_path, 'build', 'LOOT ' + git_desc + '.7z')); - } -} - -var binary_paths = helpers.getApiBinaryPaths(root_path); -for (var i = 0; i < binary_paths.length; ++i) { - createApiArchive(binary_paths[i].path, path.join(root_path, 'build', 'LOOT API ' + git_desc + ' (' + binary_paths[i].label + ').7z')); +const binaryPaths = helpers.getApiBinaryPaths(rootPath); +for (let i = 0; i < binaryPaths.length; ++i) { + createApiArchive(rootPath, binaryPaths[i].path, tempPath, path.join(rootPath, 'build', 'LOOT API ' + gitDesc + ' (' + binaryPaths[i].label + ').7z')); } diff --git a/scripts/helpers.js b/scripts/helpers.js index 8f4a531c..45dcba71 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1,113 +1,116 @@ // Helper functions shared across scripts. -var path = require('path'); -var fs = require('fs'); -var os = require('os'); +'use strict'; +const path = require('path'); +const fs = require('fs'); +const os = require('os'); -function fileExists(file_path) { - try { - // Query the entry - stats = fs.lstatSync(file_path); +function fileExists(filePath) { + try { + // Query the entry + const stats = fs.lstatSync(filePath); - // Is it a directory? - if (stats.isFile()) { - return true; - } - } catch (e) {} + // Is it a directory? + if (stats.isFile()) { + return true; + } + } catch (e) { + /* Don't do anything, it's not an error. */ + } - return false; + return false; } -function getAppReleasePaths(root_path) { - var paths = []; - var file = 'LOOT'; - var paths_to_try = [ - { - path: path.join(root_path, 'build'), - label: null - }, - { - path: path.join(root_path, 'build', '32'), - label: '32 bit' - }, - { - path: path.join(root_path, 'build', '64'), - label: '64 bit' - } - ]; +function getAppReleasePaths(rootPath) { + const paths = []; + const pathsToTry = [ + { + path: path.join(rootPath, 'build'), + label: null, + }, + { + path: path.join(rootPath, 'build', '32'), + label: '32 bit', + }, + { + path: path.join(rootPath, 'build', '64'), + label: '64 bit', + }, + ]; - if (os.platform() == 'win32') { - file += '.exe'; + let file = 'LOOT'; + if (os.platform() === 'win32') { + file += '.exe'; + } + + for (let i = 0; i < pathsToTry.length; ++i) { + if (os.platform() === 'win32') { + pathsToTry[i].path = path.join(pathsToTry[i].path, 'Release'); } - for (var i = 0; i < paths_to_try.length; ++i) { - if (os.platform() == 'win32') { - paths_to_try[i].path = path.join(paths_to_try[i].path, 'Release'); - } - - if (fileExists(path.join(paths_to_try[i].path, file))) { - paths.push(paths_to_try[i]); - } + if (fileExists(path.join(pathsToTry[i].path, file))) { + paths.push(pathsToTry[i]); } + } - return paths; + return paths; } -function getApiBinaryPaths(root_path) { - var paths = []; - var files = []; - var paths_to_try = [ +function getApiBinaryPaths(rootPath) { + const paths = []; + const pathsToTry = [ + { + path: path.join(rootPath, 'build'), + label: null, + }, + { + path: path.join(rootPath, 'build', '32'), + label: '32 bit', + }, + { + path: path.join(rootPath, 'build', '64'), + label: '64 bit', + }, + ]; + + for (let i = 0; i < pathsToTry.length; ++i) { + let files = []; + if (os.platform() === 'win32') { + pathsToTry[i].path = path.join(pathsToTry[i].path, 'Release'); + + files = [ { - path: path.join(root_path, 'build'), - label: null + name: 'loot32.dll', + label: '32 bit', }, { - path: path.join(root_path, 'build', '32'), - label: '32 bit' + name: 'loot64.dll', + label: '64 bit', + }, + ]; + } else { + files = [ + { + name: 'libloot32.so', + label: '32 bit', }, { - path: path.join(root_path, 'build', '64'), - label: '64 bit' - } - ]; - - for (var i = 0; i < paths_to_try.length; ++i) { - if (os.platform() == 'win32') { - paths_to_try[i].path = path.join(paths_to_try[i].path, 'Release'); - - files = [ - { - name: 'loot32.dll', - label: '32 bit' - }, - { - name: 'loot64.dll', - label: '64 bit' - } - ]; - } else { - files = [ - { - name: 'libloot32.so', - label: '32 bit' - }, - { - name: 'libloot64.so', - label: '64 bit' - } - ]; - } - - for (var j = 0; j < files.length; ++j) { - if (fileExists(path.join(paths_to_try[i].path, files[j].name))) { - paths_to_try[i].path = path.join(paths_to_try[i].path, files[j].name); - paths_to_try[i].label = files[j].label; - paths.push(paths_to_try[i]); - break; - } - } + name: 'libloot64.so', + label: '64 bit', + }, + ]; } - return paths; + for (let j = 0; j < files.length; ++j) { + if (fileExists(path.join(pathsToTry[i].path, files[j].name))) { + pathsToTry[i].path = path.join(pathsToTry[i].path, files[j].name); + pathsToTry[i].label = files[j].label; + paths.push(pathsToTry[i]); + break; + } + } + } + + return paths; } module.exports.fileExists = fileExists; diff --git a/scripts/vulcanize.config.json b/scripts/vulcanize.config.json deleted file mode 100644 index b7f85720..00000000 --- a/scripts/vulcanize.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "excludes": { - "styles": [ "css/theme.css" ] - } -} \ No newline at end of file diff --git a/scripts/vulcanize.js b/scripts/vulcanize.js index bf53b4a3..af00c399 100755 --- a/scripts/vulcanize.js +++ b/scripts/vulcanize.js @@ -1,44 +1,43 @@ #!/usr/bin/env node // Build the UI's index.html file. Takes one argument, which is the path to the // repository's root. -var child_process = require('child_process'); -var path = require('path'); -var fs = require('fs'); -var os = require('os'); -var helpers = require('./helpers'); +'use strict'; +const path = require('path'); +const fs = require('fs'); +const helpers = require('./helpers'); +const vulcanize = require('vulcanize'); -if (process.argv.length < 3) { - var root_path = '.'; -} else { - var root_path = process.argv[2]; +function runVulcanize(err) { + if (err) { + console.error(err); + process.exit(1); + } + vulcanize.processDocument(); } -var release_paths = helpers.getAppReleasePaths(root_path); - -for (var i = 0; i < release_paths.length; ++i) { - var output_path = path.join(release_paths[i].path, 'resources', 'ui'); - - // Makes sure output directory exists first. - try { - fs.mkdirSync(output_path); - } catch (e) { - if (e.code != 'EEXIST') { - console.log(e); - } - } - - var vulcanize = path.join(root_path, 'node_modules', '.bin', 'vulcanize'); - if (os.platform() == 'win32') { - vulcanize += '.cmd'; - } - - child_process.execFileSync(vulcanize, [ - '--inline', - '--strip', - '--config', - path.join(root_path, 'scripts', 'vulcanize.config.json'), - '-o', - path.join(output_path, 'index.html'), - path.join(root_path, 'src', 'gui', 'html', 'index.html') - ]); +let rootPath = '.'; +if (process.argv.length > 2) { + rootPath = process.argv[2]; } + +helpers.getAppReleasePaths(rootPath).forEach((releasePath) => { + const outputPath = path.join(releasePath.path, 'resources', 'ui'); + + // Makes sure output directory exists first. + try { + fs.mkdirSync(outputPath); + } catch (e) { + if (e.code !== 'EEXIST') { + console.log(e); + } + } + + vulcanize.setOptions({ + inline: true, + excludes: { + styles: ['css/theme.css'], + }, + output: path.join(outputPath, 'index.html'), + input: path.join(rootPath, 'src', 'gui', 'html', 'index.html'), + }, runVulcanize); +}) diff --git a/src/gui/handler.cpp b/src/gui/handler.cpp index 8e592da0..f62eb811 100644 --- a/src/gui/handler.cpp +++ b/src/gui/handler.cpp @@ -488,12 +488,12 @@ namespace loot { // First sort out the priority value. This is only given if it was changed. BOOST_LOG_TRIVIAL(trace) << "Calculating userlist metadata priority value from Javascript variables."; - if (pluginMetadata["modPriority"] && pluginMetadata["isGlobalPriority"]) { + if (pluginMetadata["priority"] && pluginMetadata["isPriorityGlobal"]) { BOOST_LOG_TRIVIAL(trace) << "Priority value was changed, recalculating..."; // Priority value was changed, so add it to the userlist data. - newUserlistEntry.Priority(pluginMetadata["modPriority"].as()); + newUserlistEntry.Priority(pluginMetadata["priority"].as()); newUserlistEntry.SetPriorityExplicit(true); - newUserlistEntry.SetPriorityGlobal(pluginMetadata["isGlobalPriority"].as()); + newUserlistEntry.SetPriorityGlobal(pluginMetadata["isPriorityGlobal"].as()); } else { // Priority value wasn't changed, use the existing userlist value. @@ -1056,8 +1056,8 @@ namespace loot { // Now add to pluginNode. YAML::Node pluginNode; pluginNode["name"] = tempPlugin.Name(); - pluginNode["modPriority"] = tempPlugin.Priority(); - pluginNode["isGlobalPriority"] = tempPlugin.IsPriorityGlobal(); + pluginNode["priority"] = tempPlugin.Priority(); + pluginNode["isPriorityGlobal"] = tempPlugin.IsPriorityGlobal(); pluginNode["messages"] = tempPlugin.Messages(); pluginNode["tags"] = tempPlugin.Tags(); pluginNode["isDirty"] = isDirty; @@ -1108,6 +1108,6 @@ namespace loot { void Handler::SendProgressUpdate(CefRefPtr frame, const std::string& message) { BOOST_LOG_TRIVIAL(trace) << "Sending progress update: " << message; - frame->ExecuteJavaScript("showProgress('" + message + "');", frame->GetURL(), 0); + frame->ExecuteJavaScript("loot.Dialog.showProgress('" + message + "');", frame->GetURL(), 0); } } diff --git a/src/gui/html/css/style.css b/src/gui/html/css/style.css index b2875018..17f2b0b7 100644 --- a/src/gui/html/css/style.css +++ b/src/gui/html/css/style.css @@ -35,12 +35,9 @@ html /deep/ ::-webkit-scrollbar-track { html /deep/ ::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.26); } -paper-button[autofocus] { +html /deep/ paper-button[autofocus] { color: #64B5F6; } -.hidden { - display:none; -} a { color: #2196F3; text-decoration: none; diff --git a/src/gui/html/elements/loot-message-dialog.html b/src/gui/html/elements/loot-message-dialog.html index afe1ecf5..e6596124 100644 --- a/src/gui/html/elements/loot-message-dialog.html +++ b/src/gui/html/elements/loot-message-dialog.html @@ -16,74 +16,62 @@ was pressed. - + + + + + + + - - - - + + diff --git a/src/gui/html/js/dialog.js b/src/gui/html/js/dialog.js new file mode 100644 index 00000000..e83f2243 --- /dev/null +++ b/src/gui/html/js/dialog.js @@ -0,0 +1,48 @@ +'use strict'; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.Dialog = factory(); + } +}(this, () => { + return class Dialog { + static showProgress(text) { + const progressDialog = document.getElementById('progressDialog'); + progressDialog.getElementsByTagName('p')[0].textContent = text; + if (!progressDialog.opened) { + progressDialog.showModal(); + } + } + + static closeProgress() { + const progressDialog = document.getElementById('progressDialog'); + if (progressDialog.opened) { + progressDialog.close(); + } + } + + static showMessage(title, text) { + const dialog = document.createElement('loot-message-dialog'); + dialog.setDismissable(false); + dialog.showModal(title, text); + document.body.appendChild(dialog); + } + + static askQuestion(title, text, confirmText, closeCallback) { + const dialog = document.createElement('loot-message-dialog'); + dialog.setConfirmText(confirmText); + dialog.showModal(title, text, closeCallback); + document.body.appendChild(dialog); + } + + static showNotification(text) { + const toast = document.getElementById('toast'); + toast.text = text; + toast.show(); + } + }; +})); diff --git a/src/gui/html/js/dom.js b/src/gui/html/js/dom.js new file mode 100644 index 00000000..7e754b30 --- /dev/null +++ b/src/gui/html/js/dom.js @@ -0,0 +1,89 @@ +'use strict'; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.dom = factory(); + } +}(this, () => { + return { + getElementInTableRowTemplate(rowTemplateId, elementClass) { + const select = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (select) { + return select.import.querySelector('#' + rowTemplateId).content.querySelector('.' + elementClass); + } + return document.querySelector('#' + rowTemplateId).content.querySelector('.' + elementClass); + }, + + show(elementId) { + document.getElementById(elementId).hidden = false; + }, + + hide(elementId) { + document.getElementById(elementId).hidden = true; + }, + + updateSelectedGame(gameFolder) { + document.getElementById('gameMenu').value = gameFolder; + + /* Also disable deletion of the game's row in the settings dialog. */ + const table = document.getElementById('gameTable'); + for (let i = 0; i < table.tBodies[0].rows.length; ++i) { + const folderElements = table.tBodies[0].rows[i].getElementsByClassName('folder'); + if (folderElements.length === 1) { + table.setReadOnly(table.tBodies[0].rows[i], ['delete'], folderElements[0].value === gameFolder); + } + } + }, + + updateEnabledGames(installedGames) { + const gameMenuItems = document.getElementById('gameMenu').children; + for (let i = 0; i < gameMenuItems.length; ++i) { + if (installedGames.indexOf(gameMenuItems[i].getAttribute('value')) === -1) { + gameMenuItems[i].setAttribute('disabled', true); + } else { + gameMenuItems[i].removeAttribute('disabled'); + } + } + }, + + updateSettingsDialog(settings, installedGames, gameFolder) { + const gameSelect = document.getElementById('defaultGameSelect'); + const gameMenu = document.getElementById('gameMenu'); + const 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.removeChild(gameMenu.firstElementChild); + } + gameTable.clear(); + + /* Now fill with new values. */ + settings.games.forEach((game) => { + const menuItem = document.createElement('paper-item'); + menuItem.setAttribute('value', game.folder); + menuItem.setAttribute('noink', ''); + menuItem.textContent = game.name; + gameMenu.appendChild(menuItem); + gameSelect.appendChild(menuItem.cloneNode(true)); + + const row = gameTable.addRow(game); + gameTable.setReadOnly(row, ['name', 'folder', 'type']); + }); + + gameSelect.value = settings.game; + document.getElementById('languageSelect').value = settings.language; + document.getElementById('enableDebugLogging').checked = settings.enableDebugLogging; + document.getElementById('updateMasterlist').checked = settings.updateMasterlist; + + this.updateEnabledGames(installedGames); + this.updateSelectedGame(gameFolder); + }, + }; +})); diff --git a/src/gui/html/js/events.js b/src/gui/html/js/events.js index 2eab2f20..f96f163c 100644 --- a/src/gui/html/js/events.js +++ b/src/gui/html/js/events.js @@ -1,763 +1,599 @@ +'use strict'; function saveFilterState(evt) { - var request = JSON.stringify({ - name: 'saveFilterState', - args: [ - evt.target.id, - evt.target.checked, - ] - }); - loot.query(request).catch(processCefError); + loot.query('saveFilterState', evt.target.id, evt.target.checked).catch(handlePromiseError); } function onToggleDisplayCSS(evt) { - var attr = 'data-hide-' + evt.target.getAttribute('data-class'); - if (evt.target.checked) { - document.getElementById('main').setAttribute(attr, true); - } else { - document.getElementById('main').removeAttribute(attr); - } + saveFilterState(evt); + const attr = 'data-hide-' + evt.target.getAttribute('data-class'); + if (evt.target.checked) { + document.getElementById('main').setAttribute(attr, true); + } else { + document.getElementById('main').removeAttribute(attr); + } - if (evt.target.id != 'hideBashTags') { - /* Now perform search again. If there is no current search, this won't - do anything. */ - document.getElementById('searchBar').search(); - } -} -function onToggleBashTags(evt) { - onToggleDisplayCSS(evt); + if (evt.target.id === 'hideBashTags') { document.getElementById('main').lastElementChild.updateSize(); - /* Now perform search again. If there is no current search, this won't - do anything. */ - document.getElementById('searchBar').search(); + } + /* Now perform search again. If there is no current search, this won't + do anything. */ + document.getElementById('searchBar').search(); } -function onOpenLogLocation(evt) { - loot.query('openLogLocation').catch(processCefError); +function onSidebarFilterToggle(evt) { + if (evt.target.id !== 'contentFilter') { + loot.filters[evt.target.id] = evt.target.checked; + } else { + loot.filters.contentSearchString = evt.target.value; + } + saveFilterState(evt); + filterPluginData(loot.game.plugins, loot.filters); +} + +function onJumpToGeneralInfo() { + window.location.hash = ''; + document.getElementById('main').scrollTop = 0; } function onChangeGame(evt) { - /* Check that the selected game isn't the current one. */ - if (evt.target.className.indexOf('core-selected') != -1) { - return; + /* Check that the selected game isn't the current one. */ + if (!evt.detail.isSelected) { + return; + } + + /* Send off a CEF query with the folder name of the new game. */ + loot.Dialog.showProgress(loot.l10n.translate('Loading game data...')); + loot.query('changeGame', evt.detail.item.getAttribute('value')).then((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. */ + const globalMessages = document.getElementById('summary').getElementsByTagName('ul')[0]; + while (globalMessages.firstElementChild) { + globalMessages.removeChild(globalMessages.firstElementChild); } - /* Send off a CEF query with the folder name of the new game. */ - showProgress(l10n.jed.translate('Loading game data...').fetch()); - var request = JSON.stringify({ - name: 'changeGame', - args: [ - evt.currentTarget.getAttribute('value') - ] - }); - 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'); + /* Parse the data sent from C++. */ + const gameInfo = JSON.parse(result, loot.Plugin.fromJson); + loot.game = new loot.Game(gameInfo, loot.l10n); - /* Clear the UI of all existing game-specific data. Also - clear the card and li variables for each plugin object. */ - var globalMessages = document.getElementById('summary').getElementsByTagName('ul')[0]; - while (globalMessages.firstElementChild) { - globalMessages.removeChild(globalMessages.firstElementChild); - } + /* Reset virtual list positions. */ + document.getElementById('cardsNav').scrollToItem(0); + document.getElementById('main').lastElementChild.scrollToItem(0); - /* Parse the data sent from C++. */ - try { - var gameInfo = JSON.parse(result, jsonToPlugin); - loot.game.folder = gameInfo.folder; - loot.game.masterlist = gameInfo.masterlist; - loot.game.globalMessages = gameInfo.globalMessages; - loot.game.plugins = gameInfo.plugins; + /* Now update virtual lists. */ + filterPluginData(loot.game.plugins, loot.filters); - /* Reset virtual list positions. */ - document.getElementById('cardsNav').scrollToItem(0); - document.getElementById('main').lastElementChild.scrollToItem(0); - - /* Now update virtual lists. */ - setFilteredUIData(); - } catch (e) { - console.log(e); - console.log('changeGame response: ' + result); - } - - closeProgressDialog(); - }).catch(processCefError); -} -function onOpenReadme(evt) { - loot.query('openReadme').catch(processCefError); + loot.Dialog.closeProgress(); + }).catch(handlePromiseError); } /* Masterlist update process, minus progress dialog. */ function updateMasterlistNoProgress() { - return loot.query('updateMasterlist').then(JSON.parse).then(function(result){ - if (result) { - /* Update JS variables. */ - loot.game.masterlist = result.masterlist; - loot.game.globalMessages = result.globalMessages; + return loot.query('updateMasterlist').then(JSON.parse).then((result) => { + if (result) { + /* 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; - } - } - }); - /* Hack to stop cards overlapping. */ - document.getElementById('main').lastElementChild.updateSize(); - - toast(l10n.jed.translate('Masterlist updated to revision %s.').fetch(loot.game.masterlist.revision)); - } else { - toast(l10n.jed.translate('No masterlist update was necessary.').fetch()); - } - }).catch(processCefError); -} -function onUpdateMasterlist(evt) { - showProgress(l10n.jed.translate('Updating masterlist...').fetch()); - updateMasterlistNoProgress().then(function(result){ - closeProgressDialog(); - }).catch(processCefError); -} -function onSortPlugins(evt) { - if (document.body.hasAttribute('data-conflicts')) { - /* Deactivate any existing plugin conflict filter. */ - for (var i = 0; i < loot.game.plugins.length; ++i) { - loot.game.plugins[i].isConflictFilterChecked = false; - } - /* Un-highlight any existing filter plugin. */ - var cards = document.getElementById('main').getElementsByTagName('loot-plugin-card'); - for (var i = 0; i < cards.length; ++i) { - cards[i].classList.toggle('highlight', false); - } - document.body.removeAttribute('data-conflicts'); - } - - var promise = Promise.resolve(''); - if (loot.settings.updateMasterlist) { - promise = promise.then(updateMasterlistNoProgress()); - } - promise.then(function(){ - showProgress(l10n.jed.translate('Sorting plugins...').fetch()); - loot.query('sortPlugins').then(JSON.parse).then(function(result){ - if (result) { - loot.game.oldLoadOrder = loot.game.plugins; - loot.game.loadOrder = []; - result.forEach(function(plugin){ - var found = false; - 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].isEmpty = plugin.isEmpty; - - loot.game.plugins[i].messages = plugin.messages; - loot.game.plugins[i].tags = plugin.tags; - loot.game.plugins[i].isDirty = plugin.isDirty; - - loot.game.loadOrder.push(loot.game.plugins[i]); - - found = true; - break; - } - } - if (!found) { - loot.game.plugins.push(new Plugin(plugin)); - loot.game.loadOrder.push(loot.game.plugins[loot.game.plugins.length - 1]); - } - }); - - if (loot.settings.neverTellMeTheOdds) { - /* Array shuffler from */ - for(var j, x, i = loot.game.loadOrder.length; i; j = Math.floor(Math.random() * i), x = loadOrder[--i], loot.game.loadOrder[i] = loot.game.loadOrder[j], loot.game.loadOrder[j] = x); - } - - /* Now update the UI for the new order. */ - loot.game.plugins = loot.game.loadOrder; - setFilteredUIData(); - - /* 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')); - - /* Disable changing game. */ - document.getElementById('gameMenu').setAttribute('disabled', ''); - closeProgressDialog(); - } - }).catch(processCefError); - }).catch(processCefError); -} -function onApplySort(evt) { - var loadOrder = []; - loot.game.plugins.forEach(function(plugin){ - loadOrder.push(plugin.name); - }); - var request = JSON.stringify({ - name: 'applySort', - args: [ - loadOrder - ] - }); - return loot.query(request).then(function(result){ - /* Remove old load order storage. */ - delete loot.game.loadOrder; - delete loot.game.oldLoadOrder; - - /* 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')); - - /* Enable changing game. */ - document.getElementById('gameMenu').removeAttribute('disabled'); - }).catch(processCefError); -} -function onCancelSort(evt) { - return loot.query('cancelSort').then(function(){ - /* Sort UI elements again according to stored old load order. */ - loot.game.plugins = loot.game.oldLoadOrder; - setFilteredUIData(); - delete loot.game.loadOrder; - delete loot.game.oldLoadOrder; - - /* 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')); - - /* Enable changing game. */ - document.getElementById('gameMenu').removeAttribute('disabled'); - }).catch(processCefError); -} -function onRedatePlugins(evt) { - if (evt.target.hasAttribute('disabled')) { - return; - } - - showMessageDialog(l10n.jed.translate('Redate Plugins?').fetch(), l10n.jed.translate('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?').fetch(), l10n.jed.translate('Redate').fetch(), function(result){ - if (result) { - loot.query('redatePlugins').then(function(response){ - toast('Plugins were successfully redated.'); - }).catch(processCefError); - } - }); -} -function onClearAllMetadata(evt) { - showMessageDialog('', l10n.jed.translate('Are you sure you want to clear all existing user-added metadata from all plugins?').fetch(), l10n.jed.translate('Clear').fetch(), 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].editor = 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; - } - } - }); - - toast(l10n.jed.translate('All user-added metadata has been cleared.').fetch()); - } - }).catch(processCefError); - } - }); -} -function onCopyContent(evt) { - var messages = []; - var plugins = []; - - if (loot.game) { - if (loot.game.globalMessages) { - loot.game.globalMessages.forEach(function(message){ - var m = {}; - messages.push({ - type: message.type, - content: message.content[0].str - }); - }); - } - if (loot.game.plugins) { - loot.game.plugins.forEach(function(plugin){ - plugins.push({ - name: plugin.name, - crc: plugin.crc, - version: plugin.version, - isActive: plugin.isActive, - isEmpty: plugin.isEmpty, - loadsArchive: plugin.loadsArchive, - - modPriority: plugin.modPriority, - isGlobalPriority: plugin.isGlobalPriority, - messages: plugin.messages, - tags: plugin.tags, - isDirty: plugin.isDirty - }); - }); - } - } else { - var message = document.getElementById('summary').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).then(function(){ - toast(l10n.jed.translate("LOOT's content has been copied to the clipboard.").fetch()); - }).catch(processCefError); -} -function onCopyLoadOrder(evt) { - var plugins = []; - - if (loot.game) { - if (loot.game.plugins) { - loot.game.plugins.forEach(function(plugin){ - plugins.push(plugin.name); - }); - } - } - - var request = JSON.stringify({ - name: 'copyLoadOrder', - args: [ - plugins - ] - }); - - loot.query(request).then(function(){ - toast(l10n.jed.translate("The load order has been copied to the clipboard.").fetch()); - }).catch(processCefError); -} -function onSwitchSidebarTab(evt) { - if (evt.detail.isSelected) { - document.getElementById(evt.target.selected).parentElement.selected = evt.target.selected; - } -} -function onShowAboutDialog(evt) { - document.getElementById('about').showModal(); -} -function areSettingsValid() { - /* Validate inputs individually. */ - var inputs = document.getElementById('settingsDialog').getElementsByTagName('loot-validated-input'); - for (var i = 0; i < inputs.length; ++i) { - if (!inputs[i].checkValidity()) { - return false; - } - } - return true; -} -function onCloseSettingsDialog(evt) { - if (evt.target.classList.contains('accept')) { - if (!areSettingsValid()) { - return; - } - - /* 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, - }; - - /* Send the settings back to the C++ side. */ - var request = JSON.stringify({ - name: 'closeSettings', - args: [ - settings - ] + result.plugins.forEach((resultPlugin) => { + const existingPlugin = loot.game.plugins.find((plugin) => { + return plugin.name === resultPlugin.name; }); - 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(); - } - evt.target.parentElement.close(); -} -function onShowSettingsDialog(evt) { - document.getElementById('settingsDialog').showModal(); -} -function onFocusSearch(evt) { - if (evt.ctrlKey && evt.keyCode == 70) { //'f' - document.getElementById('mainToolbar').classList.add('search'); - document.getElementById('searchBar').focusInput(); - } -} -function onEditorOpen(evt) { - /* Set up drag 'n' drop event handlers. */ - var elements = document.getElementById('cardsNav').getElementsByTagName('loot-plugin-item'); - for (var i = 0; i < elements.length; ++i) { - elements[i].draggable = true; - elements[i].addEventListener('dragstart', elements[i].onDragStart, false); - } - - /* Now show editor. */ - evt.target.classList.toggle('flip'); - - /* Enable priority hover in plugins list and enable header - buttons if this is the only editor instance. */ - var numEditors = 0; - if (document.body.hasAttribute('data-editors')) { - numEditors = parseInt(document.body.getAttribute('data-editors'), 10); - } - ++numEditors; - - if (numEditors == 1) { - /* Set the edit mode toggle attribute. */ - document.getElementById('cardsNav').setAttribute('data-editModeToggle', ''); - /* Disable the toolbar elements. */ - document.getElementById('wipeUserlistButton').setAttribute('disabled', ''); - document.getElementById('copyContentButton').setAttribute('disabled', ''); - document.getElementById('refreshContentButton').setAttribute('disabled', ''); - document.getElementById('settingsButton').setAttribute('disabled', ''); - document.getElementById('gameMenu').setAttribute('disabled', ''); - document.getElementById('updateMasterlistButton').setAttribute('disabled', ''); - document.getElementById('sortButton').setAttribute('disabled', ''); - } - document.body.setAttribute('data-editors', numEditors); - document.getElementById('cardsNav').updateSize(); - - return loot.query('editorOpened').catch(processCefError); -} -function onEditorClose(evt) { - /* evt.detail is true if the apply button was pressed. */ - var promise; - if (evt.detail) { - /* Need to record the editor control values and work out what's - changed, and update any UI elements necessary. Offload the - majority of the work to the C++ side of things. */ - - var edits = evt.target.readFromEditor(evt.target.data); - - var request = JSON.stringify({ - name: 'editorClosed', - args: [ - edits - ] - }); - promise = loot.query(request).then(JSON.parse).then(function(result){ - if (result) { - evt.target.data.modPriority = result.modPriority; - evt.target.data.isGlobalPriority = result.isGlobalPriority; - evt.target.data.messages = result.messages; - evt.target.data.tags = result.tags; - evt.target.data.isDirty = result.isDirty; - - evt.target.data.userlist = edits.userlist; - - /* Now perform search again. If there is no current search, this won't - do anything. */ - document.getElementById('searchBar').search(); - } - }); - } else { - /* Don't need to record changes, but still need to notify C++ side that - the editor has been closed. */ - promise = loot.query('editorClosed'); - } - promise.then(function(){ - delete evt.target.data.editor; - - /* Now hide editor. */ - evt.target.classList.toggle('flip'); - evt.target.data.isEditorOpen = false; - - /* Remove drag 'n' drop event handlers. */ - var elements = document.getElementById('cardsNav').getElementsByTagName('loot-plugin-item'); - for (var i = 0; i < elements.length; ++i) { - elements[i].removeAttribute('draggable'); - elements[i].removeEventListener('dragstart', elements[i].onDragStart, false); + if (existingPlugin) { + existingPlugin.isDirty = resultPlugin.isDirty; + existingPlugin.isPriorityGlobal = resultPlugin.isPriorityGlobal; + existingPlugin.masterlist = resultPlugin.masterlist; + existingPlugin.messages = resultPlugin.messages; + existingPlugin.priority = resultPlugin.priority; + existingPlugin.tags = resultPlugin.tags; } + }); + /* Hack to stop cards overlapping. */ + document.getElementById('main').lastElementChild.updateSize(); - /* Disable priority hover in plugins list and enable header - buttons if this is the only editor instance. */ - var numEditors = parseInt(document.body.getAttribute('data-editors'), 10); - --numEditors; - - if (numEditors == 0) { - document.body.removeAttribute('data-editors'); - /* Set the edit mode toggle attribute. */ - document.getElementById('cardsNav').setAttribute('data-editModeToggle', ''); - /* Re-enable toolbar elements. */ - document.getElementById('wipeUserlistButton').removeAttribute('disabled'); - document.getElementById('copyContentButton').removeAttribute('disabled'); - document.getElementById('refreshContentButton').removeAttribute('disabled'); - document.getElementById('settingsButton').removeAttribute('disabled'); - document.getElementById('gameMenu').removeAttribute('disabled'); - document.getElementById('updateMasterlistButton').removeAttribute('disabled'); - document.getElementById('sortButton').removeAttribute('disabled'); - } else { - document.body.setAttribute('data-editors', numEditors); - } - document.getElementById('cardsNav').updateSize(); - }).catch(processCefError); + loot.Dialog.showNotification(loot.l10n.translate('Masterlist updated to revision %s.', loot.game.masterlist.revision)); + } else { + loot.Dialog.showNotification(loot.l10n.translate('No masterlist update was necessary.')); + } + }).catch(handlePromiseError); } -function onConflictsFilter(evt) { +function onUpdateMasterlist() { + loot.Dialog.showProgress(loot.l10n.translate('Updating masterlist...')); + updateMasterlistNoProgress().then(() => { + loot.Dialog.closeProgress(); + }).catch(handlePromiseError); +} +function onSortPlugins() { + if (document.body.hasAttribute('data-conflicts')) { /* Deactivate any existing plugin conflict filter. */ - for (var i = 0; i < loot.game.plugins.length; ++i) { - if (loot.game.plugins[i].id != evt.target.id) { - loot.game.plugins[i].isConflictFilterChecked = false; - } - } + loot.game.plugins.forEach((plugin) => { + plugin.isConflictFilterChecked = false; + }); /* Un-highlight any existing filter plugin. */ - var cards = document.getElementById('main').getElementsByTagName('loot-plugin-card'); - for (var i = 0; i < cards.length; ++i) { - cards[i].classList.toggle('highlight', false); + const cards = document.getElementById('main').getElementsByTagName('loot-plugin-card'); + for (let i = 0; i < cards.length; ++i) { + cards[i].classList.toggle('highlight', false); } - /* evt.detail is true if the filter has been activated. */ - if (evt.detail) { - document.body.setAttribute('data-conflicts', evt.target.getName()); - evt.target.classList.toggle('highlight', true); - } else { - document.body.removeAttribute('data-conflicts'); + document.body.removeAttribute('data-conflicts'); + } + + let promise = Promise.resolve(); + if (loot.settings.updateMasterlist) { + promise = promise.then(updateMasterlistNoProgress); + } + promise.then(() => { + loot.Dialog.showProgress(loot.l10n.translate('Sorting plugins...')); + return loot.query('sortPlugins').then(JSON.parse); + }).then((result) => { + if (!result) { + return; } - setFilteredUIData(evt); -} -function onCopyMetadata(evt) { - /* evt.detail is the name of the plugin. */ - var request = JSON.stringify({ - name: 'copyMetadata', - args: [ - evt.target.getName(), - ] + loot.game.oldLoadOrder = loot.game.plugins; + loot.game.loadOrder = []; + result.forEach((plugin) => { + let existingPlugin = loot.game.plugins.find((item) => { + return item.name === plugin.name; + }); + if (existingPlugin) { + existingPlugin.crc = plugin.crc; + existingPlugin.isEmpty = plugin.isEmpty; + + existingPlugin.messages = plugin.messages; + existingPlugin.tags = plugin.tags; + existingPlugin.isDirty = plugin.isDirty; + } else { + existingPlugin = new loot.Plugin(plugin); + } + loot.game.loadOrder.push(existingPlugin); }); - loot.query(request).then(function(){ - toast(l10n.jed.translate('The metadata for "%s" has been copied to the clipboard.').fetch(evt.target.getName())); - }).catch(processCefError); + /* Now update the UI for the new order. */ + loot.game.plugins = loot.game.loadOrder; + filterPluginData(loot.game.plugins, loot.filters); + + /* Now hide the masterlist update buttons, and display the accept and + cancel sort buttons. */ + loot.dom.hide('updateMasterlistButton'); + loot.dom.hide('sortButton'); + loot.dom.show('applySortButton'); + loot.dom.show('cancelSortButton'); + + /* Disable changing game. */ + document.getElementById('gameMenu').setAttribute('disabled', ''); + loot.Dialog.closeProgress(); + }).catch(handlePromiseError); } -function onClearMetadata(evt) { - showMessageDialog('', l10n.jed.translate('Are you sure you want to clear all existing user-added metadata from "%s"?').fetch(evt.target.getName()), l10n.jed.translate('Clear').fetch(), function(result){ - if (result) { - var request = JSON.stringify({ - name: 'clearPluginMetadata', - args: [ - evt.target.getName() - ] - }); +function onApplySort() { + const loadOrder = loot.game.plugins.map((plugin) => { + return plugin.name; + }); + return loot.query('applySort', loadOrder).then(() => { + /* Remove old load order storage. */ + delete loot.game.loadOrder; + delete loot.game.oldLoadOrder; - loot.query(request).then(JSON.parse).then(function(result){ - if (result) { - /* Need to empty the UI-side user metadata. */ - for (var i = 0; i < loot.game.plugins.length; ++i) { - if (loot.game.plugins[i].id == evt.target.id) { - loot.game.plugins[i].userlist = undefined; - loot.game.plugins[i].editor = undefined; + /* Now show the masterlist update buttons, and hide the accept and + cancel sort buttons. */ + loot.dom.show('updateMasterlistButton'); + loot.dom.show('sortButton'); + loot.dom.hide('applySortButton'); + loot.dom.hide('cancelSortButton'); - loot.game.plugins[i].modPriority = result.modPriority; - loot.game.plugins[i].isGlobalPriority = result.isGlobalPriority; - loot.game.plugins[i].messages = result.messages; - loot.game.plugins[i].tags = result.tags; - loot.game.plugins[i].isDirty = result.isDirty; + /* Enable changing game. */ + document.getElementById('gameMenu').removeAttribute('disabled'); + }).catch(handlePromiseError); +} +function onCancelSort() { + return loot.query('cancelSort').then(() => { + /* Sort UI elements again according to stored old load order. */ + loot.game.plugins = loot.game.oldLoadOrder; + filterPluginData(loot.game.plugins, loot.filters); + delete loot.game.loadOrder; + delete loot.game.oldLoadOrder; - break; - } - } - toast(l10n.jed.translate('The user-added metadata for "%s" has been cleared.').fetch(evt.target.getName())); - /* Now perform search again. If there is no current search, this won't - do anything. */ - document.getElementById('searchBar').search(); - } - }).catch(processCefError); + /* Now show the masterlist update buttons, and hide the accept and + cancel sort buttons. */ + loot.dom.show('updateMasterlistButton'); + loot.dom.show('sortButton'); + loot.dom.hide('applySortButton'); + loot.dom.hide('cancelSortButton'); + + /* Enable changing game. */ + document.getElementById('gameMenu').removeAttribute('disabled'); + }).catch(handlePromiseError); +} + +function onRedatePlugins(evt) { + if (evt.target.hasAttribute('disabled')) { + return; + } + + loot.Dialog.askQuestion(loot.l10n.translate('Redate Plugins?'), loot.l10n.translate('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?'), loot.l10n.translate('Redate'), (result) => { + if (result) { + loot.query('redatePlugins').then(() => { + loot.Dialog.showNotification('Plugins were successfully redated.'); + }).catch(handlePromiseError); + } + }); +} +function onClearAllMetadata() { + loot.Dialog.askQuestion('', loot.l10n.translate('Are you sure you want to clear all existing user-added metadata from all plugins?'), loot.l10n.translate('Clear'), (result) => { + if (!result) { + return; + } + loot.query('clearAllMetadata').then(JSON.parse).then((plugins) => { + if (!plugins) { + return; + } + /* Need to empty the UI-side user metadata. */ + plugins.forEach((plugin) => { + const existingPlugin = loot.game.plugins.find((item) => { + return item.name === plugin.name; + }); + if (existingPlugin) { + existingPlugin.userlist = undefined; + existingPlugin.editor = undefined; + + existingPlugin.priority = plugin.priority; + existingPlugin.isPriorityGlobal = plugin.isPriorityGlobal; + existingPlugin.messages = plugin.messages; + existingPlugin.tags = plugin.tags; + existingPlugin.isDirty = plugin.isDirty; } + }); + + loot.Dialog.showNotification(loot.l10n.translate('All user-added metadata has been cleared.')); + }).catch(handlePromiseError); + }); +} +function onCopyContent() { + let messages = []; + let plugins = []; + + if (loot.game) { + if (loot.game.globalMessages) { + messages = loot.game.globalMessages.map((message) => { + return { + type: message.type, + content: message.content[0].str, + }; + }); + } + if (loot.game.plugins) { + plugins = loot.game.plugins.map((plugin) => { + return { + name: plugin.name, + crc: plugin.crc, + version: plugin.version, + isActive: plugin.isActive, + isEmpty: plugin.isEmpty, + loadsArchive: plugin.loadsArchive, + + priority: plugin.priority, + isPriorityGlobal: plugin.isPriorityGlobal, + messages: plugin.messages, + tags: plugin.tags, + isDirty: plugin.isDirty, + }; + }); + } + } else { + const message = document.getElementById('summary').getElementsByTagName('ul')[0].firstElementChild; + if (message) { + messages.push({ + type: 'error', + content: message.textContent, + }); + } + } + + loot.query('copyContent', { + messages, + plugins, + }).then(() => { + loot.Dialog.showNotification(loot.l10n.translate("LOOT's content has been copied to the clipboard.")); + }).catch(handlePromiseError); +} +function onCopyLoadOrder() { + let plugins = []; + + if (loot.game && loot.game.plugins) { + plugins = loot.game.plugins.map((plugin) => { + return plugin.name; }); + } + + loot.query('copyLoadOrder', plugins).then(() => { + loot.Dialog.showNotification(loot.l10n.translate('The load order has been copied to the clipboard.')); + }).catch(handlePromiseError); +} +function onContentRefresh() { + /* Send a query for updated load order and plugin header info. */ + loot.Dialog.showProgress(loot.l10n.translate('Refreshing data...')); + loot.query('getGameData').then((result) => { + /* Parse the data sent from C++. */ + const game = JSON.parse(result, loot.Plugin.fromJson); + loot.game = new loot.Game(game, loot.l10n); + + /* Reapply filters. */ + filterPluginData(loot.game.plugins, loot.filters); + + loot.Dialog.closeProgress(); + }).catch(handlePromiseError); +} + +function onOpenReadme() { + loot.query('openReadme').catch(handlePromiseError); +} +function onOpenLogLocation() { + loot.query('openLogLocation').catch(handlePromiseError); +} +function onShowAboutDialog() { + document.getElementById('about').showModal(); +} +function handleUnappliedChangesClose(change) { + loot.Dialog.askQuestion('', loot.l10n.translate('You have not yet applied or cancelled your %s. Are you sure you want to quit?', change), loot.l10n.translate('Quit'), (result) => { + if (!result) { + return; + } + /* Cancel any sorting and close any editors. Cheat by sending a + cancelSort query for as many times as necessary. */ + const queries = []; + let numQueries = 0; + if (!document.getElementById('applySortButton').hidden) { + numQueries += 1; + } + numQueries += document.body.getAttribute('data-editors'); + for (let i = 0; i < numQueries; ++i) { + queries.push(loot.query('cancelSort')); + } + Promise.all(queries).then(() => { + window.close(); + }).catch(handlePromiseError); + }); +} +function onQuit() { + if (!document.getElementById('applySortButton').hidden) { + handleUnappliedChangesClose(loot.l10n.translate('sorted load order')); + } else if (document.body.hasAttribute('data-editors')) { + handleUnappliedChangesClose(loot.l10n.translate('metadata edits')); + } else { + window.close(); + } +} + +function onSwitchSidebarTab(evt) { + if (evt.detail.isSelected) { + document.getElementById(evt.target.selected).parentElement.selected = evt.target.selected; + } } function onSidebarClick(evt) { - if (evt.target.hasAttribute('data-index')) { - document.getElementById('main').lastElementChild.scrollToItem(evt.target.getAttribute('data-index')); + if (evt.target.hasAttribute('data-index')) { + document.getElementById('main').lastElementChild.scrollToItem(evt.target.getAttribute('data-index')); - if (evt.type == 'dblclick') { - var card = document.getElementById(evt.target.getAttribute('data-id')); - if (!card.classList.contains('flip')) { - document.getElementById(evt.target.getAttribute('data-id')).onShowEditor(); - } - } + if (evt.type === 'dblclick') { + const card = document.getElementById(evt.target.getAttribute('data-id')); + if (!card.classList.contains('flip')) { + document.getElementById(evt.target.getAttribute('data-id')).onShowEditor(); + } } + } } -function onQuit(evt) { - if (!document.getElementById('applySortButton').classList.contains('hidden')) { - handleUnappliedChangesClose(l10n.jed.translate('sorted load order').fetch()); - } else if (document.body.hasAttribute('data-editors')) { - handleUnappliedChangesClose(l10n.jed.translate('metadata edits').fetch()); + +function areSettingsValid() { + /* Validate inputs individually. */ + const inputs = document.getElementById('settingsDialog').getElementsByTagName('loot-validated-input'); + for (let i = 0; i < inputs.length; ++i) { + if (!inputs[i].checkValidity()) { + return false; + } + } + return true; +} +function onCloseSettingsDialog(evt) { + if (evt.target.classList.contains('accept')) { + if (!areSettingsValid()) { + return; + } + + /* Update the JS variable values. */ + const 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, + }; + + /* Send the settings back to the C++ side. */ + loot.query('closeSettings', settings).then(JSON.parse).then((installedGames) => { + loot.installedGames = installedGames; + loot.dom.updateEnabledGames(installedGames); + }).catch(handlePromiseError).then(() => { + loot.settings = settings; + loot.dom.updateSettingsDialog(loot.settings, loot.installedGames, loot.game.folder); + }).catch(handlePromiseError); + } else { + /* Re-apply the existing settings to the settings dialog elements. */ + loot.dom.updateSettingsDialog(loot.settings, loot.installedGames, loot.game.folder); + } + evt.target.parentElement.close(); +} +function onShowSettingsDialog() { + document.getElementById('settingsDialog').showModal(); +} + +function onEditorOpen(evt) { + /* Set up drag 'n' drop event handlers. */ + const elements = document.getElementById('cardsNav').getElementsByTagName('loot-plugin-item'); + for (let i = 0; i < elements.length; ++i) { + elements[i].draggable = true; + elements[i].addEventListener('dragstart', elements[i].onDragStart); + } + + /* Now show editor. */ + evt.target.classList.toggle('flip'); + + /* Enable priority hover in plugins list and enable header + buttons if this is the only editor instance. */ + let numEditors = 0; + if (document.body.hasAttribute('data-editors')) { + numEditors = parseInt(document.body.getAttribute('data-editors'), 10); + } + ++numEditors; + + if (numEditors === 1) { + /* Set the edit mode toggle attribute. */ + document.getElementById('cardsNav').setAttribute('data-editModeToggle', ''); + /* Disable the toolbar elements. */ + document.getElementById('wipeUserlistButton').setAttribute('disabled', ''); + document.getElementById('copyContentButton').setAttribute('disabled', ''); + document.getElementById('refreshContentButton').setAttribute('disabled', ''); + document.getElementById('settingsButton').setAttribute('disabled', ''); + document.getElementById('gameMenu').setAttribute('disabled', ''); + document.getElementById('updateMasterlistButton').setAttribute('disabled', ''); + document.getElementById('sortButton').setAttribute('disabled', ''); + } + document.body.setAttribute('data-editors', numEditors); + document.getElementById('cardsNav').updateSize(); + + return loot.query('editorOpened').catch(handlePromiseError); +} +function onEditorClose(evt) { + /* evt.detail is true if the apply button was pressed. */ + let promise; + if (evt.detail) { + /* Need to record the editor control values and work out what's + changed, and update any UI elements necessary. Offload the + majority of the work to the C++ side of things. */ + const edits = evt.target.readFromEditor(evt.target.data); + promise = loot.query('editorClosed', edits).then(JSON.parse).then((result) => { + if (result) { + evt.target.data.priority = result.priority; + evt.target.data.isPriorityGlobal = result.isPriorityGlobal; + evt.target.data.messages = result.messages; + evt.target.data.tags = result.tags; + evt.target.data.isDirty = result.isDirty; + + evt.target.data.userlist = edits.userlist; + + /* Now perform search again. If there is no current search, this won't + do anything. */ + document.getElementById('searchBar').search(); + } + }); + } else { + /* Don't need to record changes, but still need to notify C++ side that + the editor has been closed. */ + promise = loot.query('editorClosed'); + } + promise.then(() => { + delete evt.target.data.editor; + + /* Now hide editor. */ + evt.target.classList.toggle('flip'); + evt.target.data.isEditorOpen = false; + + /* Remove drag 'n' drop event handlers. */ + const elements = document.getElementById('cardsNav').getElementsByTagName('loot-plugin-item'); + for (let i = 0; i < elements.length; ++i) { + elements[i].removeAttribute('draggable'); + elements[i].removeEventListener('dragstart', elements[i].onDragStart); + } + + /* Disable priority hover in plugins list and enable header + buttons if this is the only editor instance. */ + let numEditors = parseInt(document.body.getAttribute('data-editors'), 10); + --numEditors; + + if (numEditors === 0) { + document.body.removeAttribute('data-editors'); + /* Set the edit mode toggle attribute. */ + document.getElementById('cardsNav').setAttribute('data-editModeToggle', ''); + /* Re-enable toolbar elements. */ + document.getElementById('wipeUserlistButton').removeAttribute('disabled'); + document.getElementById('copyContentButton').removeAttribute('disabled'); + document.getElementById('refreshContentButton').removeAttribute('disabled'); + document.getElementById('settingsButton').removeAttribute('disabled'); + document.getElementById('gameMenu').removeAttribute('disabled'); + document.getElementById('updateMasterlistButton').removeAttribute('disabled'); + document.getElementById('sortButton').removeAttribute('disabled'); } else { - window.close(); + document.body.setAttribute('data-editors', numEditors); } + document.getElementById('cardsNav').updateSize(); + }).catch(handlePromiseError); } -function onJumpToGeneralInfo(evt) { - window.location.hash = ''; - document.getElementById('main').scrollTop = 0; +function onConflictsFilter(evt) { + /* Deactivate any existing plugin conflict filter. */ + loot.game.plugins.forEach((plugin) => { + if (plugin.id !== evt.target.id) { + plugin.isConflictFilterChecked = false; + } + }); + /* Un-highlight any existing filter plugin. */ + const cards = document.getElementById('main').getElementsByTagName('loot-plugin-card'); + for (let i = 0; i < cards.length; ++i) { + cards[i].classList.toggle('highlight', false); + } + /* evt.detail is true if the filter has been activated. */ + if (evt.detail) { + document.body.setAttribute('data-conflicts', evt.target.getName()); + evt.target.classList.toggle('highlight', true); + } else { + document.body.removeAttribute('data-conflicts'); + } + filterPluginData(loot.game.plugins, loot.filters); } -function onContentRefresh(evt) { - /* Send a query for updated load order and plugin header info. */ - showProgress(l10n.jed.translate('Refreshing data...').fetch()); - 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].isEmpty = plugin.isEmpty; - loot.game.plugins[i].loadsArchive = plugin.loadsArchive; - 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.splice(i, 1); - } else { - ++i; - } - } - - /* Reapply filters. */ - setFilteredUIData(); - - closeProgressDialog(); - }).catch(processCefError); +function onCopyMetadata(evt) { + loot.query('copyMetadata', evt.target.getName()).then(() => { + loot.Dialog.showNotification(loot.l10n.translate('The metadata for "%s" has been copied to the clipboard.', evt.target.getName())); + }).catch(handlePromiseError); } -function onSearchOpen(evt) { +function onClearMetadata(evt) { + loot.Dialog.askQuestion('', loot.l10n.translate('Are you sure you want to clear all existing user-added metadata from "%s"?', evt.target.getName()), loot.l10n.translate('Clear'), (result) => { + if (!result) { + return; + } + loot.query('clearPluginMetadata', evt.target.getName()).then(JSON.parse).then((plugin) => { + if (!result) { + return; + } + /* Need to empty the UI-side user metadata. */ + const existingPlugin = loot.game.plugins.find((item) => { + return item.id === evt.target.id; + }); + if (existingPlugin) { + existingPlugin.userlist = undefined; + existingPlugin.editor = undefined; + + existingPlugin.priority = plugin.priority; + existingPlugin.isPriorityGlobal = plugin.isPriorityGlobal; + existingPlugin.messages = plugin.messages; + existingPlugin.tags = plugin.tags; + existingPlugin.isDirty = plugin.isDirty; + } + loot.Dialog.showNotification(loot.l10n.translate('The user-added metadata for "%s" has been cleared.', evt.target.getName())); + /* Now perform search again. If there is no current search, this won't + do anything. */ + document.getElementById('searchBar').search(); + }).catch(handlePromiseError); + }); +} + +function onFocusSearch(evt) { + if (evt.ctrlKey && evt.keyCode === 70) { // 'f' document.getElementById('mainToolbar').classList.add('search'); document.getElementById('searchBar').focusInput(); + } } -function onSearchClose(evt) { - document.getElementById('mainToolbar').classList.remove('search'); +function onSearchOpen() { + document.getElementById('mainToolbar').classList.add('search'); + document.getElementById('searchBar').focusInput(); } -function setupEventHandlers() { - /*Set up handlers for filters.*/ - document.getElementById('hideVersionNumbers').addEventListener('change', onToggleDisplayCSS, false); - document.getElementById('hideVersionNumbers').addEventListener('change', saveFilterState, false); - document.getElementById('hideCRCs').addEventListener('change', onToggleDisplayCSS, false); - document.getElementById('hideCRCs').addEventListener('change', saveFilterState, false); - document.getElementById('hideBashTags').addEventListener('change', onToggleBashTags, false); - document.getElementById('hideBashTags').addEventListener('change', saveFilterState, false); - document.getElementById('hideNotes').addEventListener('change', setFilteredUIData, false); - document.getElementById('hideNotes').addEventListener('change', saveFilterState, false); - document.getElementById('hideDoNotCleanMessages').addEventListener('change', setFilteredUIData, false); - document.getElementById('hideDoNotCleanMessages').addEventListener('change', saveFilterState, false); - document.getElementById('hideInactivePlugins').addEventListener('change', setFilteredUIData, false); - document.getElementById('hideInactivePlugins').addEventListener('change', saveFilterState, false); - document.getElementById('hideAllPluginMessages').addEventListener('change', setFilteredUIData, false); - document.getElementById('hideAllPluginMessages').addEventListener('change', saveFilterState, false); - document.getElementById('hideMessagelessPlugins').addEventListener('change', setFilteredUIData, false); - document.getElementById('hideMessagelessPlugins').addEventListener('change', saveFilterState, false); - document.body.addEventListener('loot-filter-conflicts', onConflictsFilter, false); - - /* Set up event handlers for content filter. */ - document.getElementById('contentFilter').addEventListener('change', setFilteredUIData, false); - - /* Set up handlers for buttons. */ - document.getElementById('redatePluginsButton').addEventListener('click', onRedatePlugins, false); - document.getElementById('openLogButton').addEventListener('click', onOpenLogLocation, false); - document.getElementById('wipeUserlistButton').addEventListener('click', onClearAllMetadata, false); - document.getElementById('copyLoadOrderButton').addEventListener('click', onCopyLoadOrder, false); - document.getElementById('copyContentButton').addEventListener('click', onCopyContent, false); - document.getElementById('refreshContentButton').addEventListener('click', onContentRefresh, false); - document.getElementById('settingsButton').addEventListener('click', onShowSettingsDialog, false); - document.getElementById('helpButton').addEventListener('click', onOpenReadme, false); - document.getElementById('aboutButton').addEventListener('click', onShowAboutDialog, false); - document.getElementById('quitButton').addEventListener('click', onQuit, false); - document.getElementById('updateMasterlistButton').addEventListener('click', onUpdateMasterlist, false); - document.getElementById('sortButton').addEventListener('click', onSortPlugins, false); - document.getElementById('applySortButton').addEventListener('click', onApplySort, false); - document.getElementById('cancelSortButton').addEventListener('click', onCancelSort, false); - document.getElementById('sidebarTabs').addEventListener('core-select', onSwitchSidebarTab, false); - document.getElementById('jumpToGeneralInfo').addEventListener('click', onJumpToGeneralInfo, false); - - /* Set up search event handlers. */ - document.getElementById('showSearch').addEventListener('click', onSearchOpen, false); - document.getElementById('searchBar').addEventListener('loot-search-close', onSearchClose, false); - window.addEventListener('keyup', onFocusSearch, false); - - /* Set up event handlers for settings dialog. */ - var settings = document.getElementById('settingsDialog'); - settings.getElementsByClassName('accept')[0].addEventListener('click', onCloseSettingsDialog, false); - settings.getElementsByClassName('cancel')[0].addEventListener('click', onCloseSettingsDialog, false); - - /* Set up handler for opening and closing editors. */ - document.body.addEventListener('loot-editor-open', onEditorOpen, false); - document.body.addEventListener('loot-editor-close', onEditorClose, false); - document.body.addEventListener('loot-copy-metadata', onCopyMetadata, false); - document.body.addEventListener('loot-clear-metadata', onClearMetadata, false); - - document.getElementById('cardsNav').addEventListener('click', onSidebarClick, false); - document.getElementById('cardsNav').addEventListener('dblclick', onSidebarClick, false); +function onSearchClose() { + document.getElementById('mainToolbar').classList.remove('search'); } diff --git a/src/gui/html/js/filters.js b/src/gui/html/js/filters.js index 33bde7ac..7f229173 100644 --- a/src/gui/html/js/filters.js +++ b/src/gui/html/js/filters.js @@ -1,250 +1,67 @@ -var filters = { - /* Filter functions return true if the given plugin passes the filter and - should be displayed, otherwise false. */ +'use strict'; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.Filters = factory(); + } +}(this, () => { + return class Filters { + constructor(l10n) { + /* Plugin filters */ + this.hideMessagelessPlugins = false; + this.hideInactivePlugins = false; + this.conflictingPluginNames = []; + this.contentSearchString = ''; - hiddenPluginNo: 0, - hiddenMessageNo: 0, - conflicts: [], + /* Plugin content filters */ + this.hideVersionNumbers = false; + this.hideCRCs = false; + this.hideBashTags = false; + this.hideAllPluginMessages = false; + this.hideNotes = false; + this.hideDoNotCleanMessages = false; - searchFilter: function(plugin, needle) { - if (needle.length == 0) { - return true; + this._doNotCleanString = l10n.translate('Do not clean').toLowerCase(); + } + + pluginFilter(plugin) { + if (this.hideInactivePlugins && !plugin.isActive) { + return false; } - if (plugin.name.toLowerCase().indexOf(needle) != -1 - || plugin.getCrcString().toLowerCase().indexOf(needle) != -1 - || plugin.version.toLowerCase().indexOf(needle) != -1) { - - return true; + if (this.conflictingPluginNames.length !== 0 && this.conflictingPluginNames.indexOf(plugin.name) === -1) { + return false; } - var tags = plugin.getTagStrings(); - if (tags.added.toLowerCase().indexOf(needle) != -1 - || tags.removed.toLowerCase().indexOf(needle) != -1) { - - return true; + if (this.hideMessagelessPlugins && plugin.getCardContent(this).messages.length === 0) { + return false; } - for (var i = 0; i < plugin.messages.length; ++i) { - if (plugin.messages[i].content[0].str.toLowerCase().indexOf(needle) != -1) { - return true; - } - } - }, - - messagelessFilter: function(plugin) { - /* This function could be further optimised to perform fewer checks, - but it's also responsible for setting the hidden message count, so - has to go through everything. */ - - var hasMessages = false; - /* If any messages exist, check if they are hidden or not. Note - that the messages may not be present as elements, so the check - is actually if they would be hidden according to the message - filters. */ - if (this.allMessageFilter()) { - plugin.messages.forEach(function(message){ - if (this.noteFilter(message) - && this.doNotCleanFilter(message)) { - - hasMessages = true; - return; - } - ++hiddenMessageNo; - }, this); - } else { - hiddenMessageNo += plugin.messages.length; + if (this.contentSearchString.length !== 0 && !plugin.getCardContent(this).containsText(this.contentSearchString)) { + return false; } - if (document.getElementById('hideMessagelessPlugins').checked) { - return hasMessages; - } else { - return true; - } - }, + return true; + } - inactiveFilter: function(plugin) { - if (document.getElementById('hideInactivePlugins').checked) { - return plugin.isActive; - } else { - return true; - } - }, - - conflictsFilter: function(plugin) { - if (this.conflicts.length > 0) { - return this.conflicts.indexOf(plugin.name) != -1; - } else { - return true; - } - }, - - applyPluginFilters: function(plugins) { - var search = document.getElementById('contentFilter').value.toLowerCase(); - hiddenPluginNo = 0; - hiddenMessageNo = 0; - var filteredPlugins = []; - - plugins.forEach(function(plugin){ - /* Messageless filter needs to run first. */ - if (this.messagelessFilter(plugin) - && this.inactiveFilter(plugin) - && this.conflictsFilter(plugin) - && this.searchFilter(plugin, search)) { - - filteredPlugins.push(plugin); - return; - } - ++hiddenPluginNo; - }, this); - - document.getElementById('hiddenPluginNo').textContent = hiddenPluginNo; - document.getElementById('hiddenMessageNo').textContent = hiddenMessageNo; - - return filteredPlugins; - }, - - /* Message filter functions are run from within plugin cards, when the card's - messages are to be added as elements. Each filter should return true if the - message is to be displayed. */ - - noteFilter: function(message) { - if (document.getElementById('hideNotes').checked) { - return message.type != 'say'; - } else { - return true; - } - }, - - doNotCleanFilter: function(message) { - if (document.getElementById('hideDoNotCleanMessages').checked) { - return message.content[0].str.indexOf(l10n.jed.translate("Do not clean").fetch()) == -1; - } else { - return true; - } - }, - - allMessageFilter: function() { - return !document.getElementById('hideAllPluginMessages').checked; - }, - - applyMessageFilters: function(messages) { - var filteredMessages = []; - - if (this.allMessageFilter()) { - messages.forEach(function(message){ - if (this.noteFilter(message) - && this.doNotCleanFilter(message)) { - - filteredMessages.push(message); - return; - } - }, this); + messageFilter(message) { + if (this.hideAllPluginMessages) { + return false; } - return filteredMessages; - }, -}; - -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 - ] - }); - - showProgress(l10n.jed.translate('Checking if plugins have been loaded...').fetch()); - - 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].isEmpty = result[key].isEmpty; - - 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 setFilteredUIData() { - /* 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) { - filters.conflicts = conflicts; - var filtered = filters.applyPluginFilters(loot.game.plugins); - document.getElementById('cardsNav').data = filtered; - document.getElementById('main').lastElementChild.data = filtered; - - /* Also run message filters on the filtered plugins. */ - filtered.forEach(function(plugin){ - plugin.computed.messages = plugin.getUIMessages(); - }); - - /* Now perform search again. If there is no current search, this won't - do anything. */ - document.getElementById('searchBar').search(); - }); -} -function restoreFilterStates() { - if (loot.settings.filters) { - document.getElementById('hideMessagelessPlugins').checked = loot.settings.filters.hideMessagelessPlugins; - document.getElementById('hideInactivePlugins').checked = loot.settings.filters.hideInactivePlugins; - document.getElementById('hideNotes').checked = loot.settings.filters.hideNotes; - document.getElementById('hideDoNotCleanMessages').checked = loot.settings.filters.hideDoNotCleanMessages; - document.getElementById('hideAllPluginMessages').checked = loot.settings.filters.hideAllPluginMessages; - document.getElementById('hideVersionNumbers').checked = loot.settings.filters.hideVersionNumbers; - document.getElementById('hideCRCs').checked = loot.settings.filters.hideCRCs; - document.getElementById('hideBashTags').checked = loot.settings.filters.hideBashTags; - } -} - -function applyEnabledFilters() { - if (loot.settings.filters) { - if (loot.settings.filters.hideMessagelessPlugins - || loot.settings.filters.hideInactivePlugins - || loot.settings.filters.hideNotes - || loot.settings.filters.hideDoNotCleanMessages - || loot.settings.filters.hideAllPluginMessages) { - setFilteredUIData(); + if (this.hideNotes && message.type === 'say') { + return false; } - if (loot.settings.filters.hideVersionNumbers) { - document.getElementById('hideVersionNumbers').dispatchEvent(new Event('change')); + if (this.hideDoNotCleanMessages && message.content.toLowerCase().indexOf(this._doNotCleanString) !== -1) { + return false; } - if (loot.settings.filters.hideCRCs) { - document.getElementById('hideCRCs').dispatchEvent(new Event('change')); - } - - if (loot.settings.filters.hideBashTags) { - document.getElementById('hideBashTags').dispatchEvent(new Event('change')); - } - } -} + return true; + } + }; +})); diff --git a/src/gui/html/js/game.js b/src/gui/html/js/game.js new file mode 100644 index 00000000..a95ea157 --- /dev/null +++ b/src/gui/html/js/game.js @@ -0,0 +1,321 @@ +'use strict'; + +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.Game = factory(root.marked); + } +}(this, (marked) => { + return class Game { + constructor(obj, l10n) { + this.folder = obj.folder || ''; + this.globalMessages = obj.globalMessages || []; + this.masterlist = obj.masterlist || {}; + this.plugins = obj.plugins || []; + + this.loadOrder = undefined; + this.oldLoadOrder = undefined; + + this._notApplicableString = l10n.translate('N/A'); + } + + get folder() { + return this._folder; + } + + set folder(folder) { + if (folder !== this._folder) { + document.dispatchEvent(new CustomEvent('loot-game-folder-change', { + detail: { folder }, + })); + } + + this._folder = folder; + } + + get globalMessages() { + return this._globalMessages; + } + + set globalMessages(globalMessages) { + /* Update the message counts. */ + let oldTotal = 0; + let newTotal = 0; + let oldWarns = 0; + let newWarns = 0; + let oldErrs = 0; + let newErrs = 0; + + if (this._globalMessages) { + oldTotal = this._globalMessages.length; + this._globalMessages.forEach((message) => { + if (message.type === 'warn') { + ++oldWarns; + } else if (message.type === 'error') { + ++oldErrs; + } + }); + } + + if (globalMessages) { + newTotal = globalMessages.length; + + globalMessages.forEach((message) => { + if (message.type === 'warn') { + ++newWarns; + } else if (message.type === 'error') { + ++newErrs; + } + }); + } + + if (newTotal !== oldTotal || newWarns !== oldWarns || newErrs !== oldErrs) { + document.dispatchEvent(new CustomEvent('loot-game-global-messages-change', { + detail: { + totalDiff: newTotal - oldTotal, + warningDiff: newWarns - oldWarns, + errorDiff: newErrs - oldErrs, + messages: globalMessages, + }, + })); + } + + this._globalMessages = globalMessages; + } + + get masterlist() { + return this._masterlist; + } + + set masterlist(masterlist) { + if (masterlist !== this._masterlist + && (masterlist === undefined || this._masterlist === undefined + || masterlist.revision !== this._masterlist.revision + || masterlist.date !== this._masterlist.date)) { + let revision = this._notApplicableString; + let date = this._notApplicableString; + if (masterlist && masterlist.revision) { + revision = masterlist.revision; + } + if (masterlist && masterlist.date) { + date = masterlist.date; + } + + document.dispatchEvent(new CustomEvent('loot-game-masterlist-change', { + detail: { + revision, + date, + }, + })); + } + + this._masterlist = masterlist; + } + + get plugins() { + return this._plugins; + } + + set plugins(plugins) { + /* Update plugin and message counts. Unlike for global messages + it's not worth calculating the count differences, just count + from zero. */ + let totalMessageNo = 0; + let warnMessageNo = 0; + let errorMessageNo = 0; + let activePluginNo = 0; + let dirtyPluginNo = 0; + + /* Include global messages in the count. */ + if (this.globalMessages) { + totalMessageNo = this.globalMessages.length; + this.globalMessages.forEach((message) => { + if (message.type === 'warn') { + ++warnMessageNo; + } else if (message.type === 'error') { + ++errorMessageNo; + } + }); + } + + plugins.forEach((plugin) => { + if (plugin.isActive) { + ++activePluginNo; + } + if (plugin.isDirty) { + ++dirtyPluginNo; + } + if (plugin.messages) { + totalMessageNo += plugin.messages.length; + plugin.messages.forEach((message) => { + if (message.type === 'warn') { + ++warnMessageNo; + } else if (message.type === 'error') { + ++errorMessageNo; + } + }); + } + }); + + document.dispatchEvent(new CustomEvent('loot-game-plugins-change', { + detail: { + valuesAreTotals: true, + totalMessageNo, + warnMessageNo, + errorMessageNo, + totalPluginNo: plugins.length, + activePluginNo, + dirtyPluginNo, + }, + })); + + this._plugins = plugins; + } + + appendPlugin(plugin) { + let totalChange = 0; + let warnChange = 0; + let errorChange = 0; + let activeChange = 0; + let dirtyChange = 0; + + if (plugin.isActive) { + ++activeChange; + } + if (plugin.isDirty) { + ++dirtyChange; + } + if (plugin.messages) { + totalChange += plugin.messages.length; + plugin.messages.forEach((message) => { + if (message.type === 'warn') { + ++warnChange; + } else if (message.type === 'error') { + ++errorChange; + } + }); + } + + document.dispatchEvent(new CustomEvent('loot-game-plugins-change', { + detail: { + valuesAreTotals: false, + totalMessageNo: totalChange, + warnMessageNo: warnChange, + errorMessageNo: errorChange, + totalPluginNo: 1, + activePluginNo: activeChange, + dirtyPluginNo: dirtyChange, + }, + })); + + this._plugins.push(plugin); + } + + removePluginAtIndex(index) { + let totalChange = 0; + let warnChange = 0; + let errorChange = 0; + let activeChange = 0; + let dirtyChange = 0; + + if (this._plugins[index].isActive) { + --activeChange; + } + if (this._plugins[index].isDirty) { + --dirtyChange; + } + if (this._plugins[index].messages) { + totalChange -= this._plugins[index].messages.length; + this._plugins[index].messages.forEach((message) => { + if (message.type === 'warn') { + --warnChange; + } else if (message.type === 'error') { + --errorChange; + } + }); + } + + document.dispatchEvent(new CustomEvent('loot-game-plugins-change', { + detail: { + valuesAreTotals: false, + totalMessageNo: totalChange, + warnMessageNo: warnChange, + errorMessageNo: errorChange, + totalPluginNo: -1, + activePluginNo: activeChange, + dirtyPluginNo: dirtyChange, + }, + })); + + this._plugins.splice(index, 1); + } + + static onPluginsChange(evt) { + if (!evt.detail.valuesAreTotals) { + evt.detail.totalMessageNo += parseInt(document.getElementById('totalMessageNo').textContent, 10); + evt.detail.warnMessageNo += parseInt(document.getElementById('totalWarningNo').textContent, 10); + evt.detail.errorMessageNo += parseInt(document.getElementById('totalErrorNo').textContent, 10); + evt.detail.totalPluginNo += parseInt(document.getElementById('totalPluginNo').textContent, 10); + evt.detail.activePluginNo += parseInt(document.getElementById('activePluginNo').textContent, 10); + evt.detail.dirtyPluginNo += parseInt(document.getElementById('dirtyPluginNo').textContent, 10); + } + + document.getElementById('filterTotalMessageNo').textContent = evt.detail.totalMessageNo; + document.getElementById('totalMessageNo').textContent = evt.detail.totalMessageNo; + document.getElementById('totalWarningNo').textContent = evt.detail.warnMessageNo; + document.getElementById('totalErrorNo').textContent = evt.detail.errorMessageNo; + + document.getElementById('filterTotalPluginNo').textContent = evt.detail.totalPluginNo; + document.getElementById('totalPluginNo').textContent = evt.detail.totalPluginNo; + document.getElementById('activePluginNo').textContent = evt.detail.activePluginNo; + document.getElementById('dirtyPluginNo').textContent = evt.detail.dirtyPluginNo; + } + static onGlobalMessagesChange(evt) { + document.getElementById('filterTotalMessageNo').textContent = parseInt(document.getElementById('filterTotalMessageNo').textContent, 10) + evt.detail.totalDiff; + document.getElementById('totalMessageNo').textContent = parseInt(document.getElementById('totalMessageNo').textContent, 10) + evt.detail.totalDiff; + document.getElementById('totalWarningNo').textContent = parseInt(document.getElementById('totalWarningNo').textContent, 10) + evt.detail.warningDiff; + document.getElementById('totalErrorNo').textContent = parseInt(document.getElementById('totalErrorNo').textContent, 10) + evt.detail.errorDiff; + + /* Remove old messages from UI. */ + const generalMessagesList = document.getElementById('summary').getElementsByTagName('ul')[0]; + while (generalMessagesList.firstElementChild) { + generalMessagesList.removeChild(generalMessagesList.firstElementChild); + } + + /* Add new messages. */ + if (evt.detail.messages) { + evt.detail.messages.forEach((message) => { + const 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); + }); + } + } + static onMasterlistChange(evt) { + document.getElementById('masterlistRevision').textContent = evt.detail.revision; + document.getElementById('masterlistDate').textContent = evt.detail.date; + } + static onFolderChange(evt) { + loot.dom.updateSelectedGame(evt.detail.folder); + /* Enable/disable the redate plugins option. */ + let gameSettings = undefined; + if (loot.settings && loot.settings.games) { + gameSettings = loot.settings.games.find((game) => { + return game.folder === evt.detail.folder; + }); + } + const redateButton = document.getElementById('redatePluginsButton'); + if (gameSettings && gameSettings.type === 'Skyrim') { + redateButton.removeAttribute('disabled'); + } else { + redateButton.setAttribute('disabled', true); + } + } + }; +})); diff --git a/src/gui/html/js/helpers.js b/src/gui/html/js/helpers.js index d5231983..fd92f52c 100644 --- a/src/gui/html/js/helpers.js +++ b/src/gui/html/js/helpers.js @@ -1,73 +1,71 @@ -function processCefError(err) { - /* Error.stack seems to be Chromium-specific. It gives a lot more useful - info than just the error message. Also, this can be used to catch any - promise errors, not just CEF errors. */ - console.log(err.stack); - closeProgressDialog(); - showMessageBox(l10n.jed.translate('Error').fetch(), err.message); +'use strict'; +function handlePromiseError(err) { + /* Error.stack seems to be Chromium-specific. */ + console.log(err.stack); + loot.Dialog.closeProgress(); + loot.Dialog.showMessage(loot.l10n.translate('Error'), err.message); } +function getConflictingPlugins(pluginName) { + if (!pluginName) { + return Promise.resolve([]); + } -function showElement(element) { - if (element != null) { - element.classList.toggle('hidden', false); - } -} -function hideElement(element) { - if (element != null) { - element.classList.toggle('hidden', true); - } -} -function toast(text) { - var toast = document.getElementById('toast'); - toast.text = text; - toast.show(); -} -function showMessageDialog(title, text, positiveText, closeCallback) { - var dialog = document.createElement('loot-message-dialog'); - dialog.setButtonText(positiveText, l10n.jed.translate('Cancel').fetch()); - dialog.showModal(title, text, closeCallback); - document.body.appendChild(dialog); -} -function showMessageBox(title, text) { - var dialog = document.createElement('loot-message-dialog'); - dialog.setButtonText(l10n.jed.translate('OK').fetch()); - dialog.showModal(title, text); - document.body.appendChild(dialog); -} + /* Now get conflicts for the plugin. */ + loot.Dialog.showProgress(loot.l10n.translate('Checking if plugins have been loaded...')); -function showProgress(message) { - var progressDialog = document.getElementById('progressDialog'); - if (message) { - progressDialog.getElementsByTagName('p')[0].textContent = message; - } - if (!progressDialog.opened) { - progressDialog.showModal(); - } -} -function closeProgressDialog() { - var progressDialog = document.getElementById('progressDialog'); - if (progressDialog.opened) { - progressDialog.close(); - } -} -function handleUnappliedChangesClose(change) { - showMessageDialog('', l10n.jed.translate('You have not yet applied or cancelled your %s. Are you sure you want to quit?').fetch(change), l10n.jed.translate('Quit').fetch(), function(result){ - if (result) { - /* Cancel any sorting and close any editors. Cheat by sending a - cancelSort query for as many times as necessary. */ - var queries = []; - var numQueries = 0; - if (!document.getElementById('applySortButton').classList.contains('hidden')) { - numQueries += 1; - } - numQueries += document.body.getAttribute('data-editors'); - for (var i = 0; i < numQueries; ++i) { - queries.push(loot.query('cancelSort')); - } - Promise.all(queries).then(function(){ - window.close(); - }).catch(processCefError); + return loot.query('getConflictingPlugins', pluginName).then(JSON.parse).then((result) => { + const conflicts = [pluginName]; + if (result) { + /* Filter everything but the plugin itself if there are no + conflicts. */ + for (const key in result) { + if (result[key].conflicts) { + conflicts.push(key); } - }); -} + const plugin = loot.game.plugins.find((item) => { + return item.name === key; + }); + if (plugin) { + plugin.crc = result[key].crc; + plugin.isEmpty = result[key].isEmpty; + plugin.messages = result[key].messages; + plugin.tags = result[key].tags; + plugin.isDirty = result[key].isDirty; + } + } + } + loot.Dialog.closeProgress(); + return conflicts; + }).catch(handlePromiseError); +} +function filterPluginData(plugins, filters) { + getConflictingPlugins(filters.conflictTargetPluginName).then((conflictingPluginNames) => { + filters.conflictingPluginNames = conflictingPluginNames; + return plugins.filter(filters.pluginFilter, filters); + }).then((filteredPlugins) => { + document.getElementById('cardsNav').data = filteredPlugins; + document.getElementById('pluginCardList').data = filteredPlugins; + + filteredPlugins.forEach((plugin) => { + const element = document.getElementById(plugin.id); + if (element) { + element.onMessagesChange(); + } + }); + document.getElementById('cardsNav').updateSize(); + document.getElementById('pluginCardList').updateSize(); + + /* Now perform search again. If there is no current search, this won't + do anything. */ + document.getElementById('searchBar').search(); + + /* Re-count all hidden plugins and messages. */ + document.getElementById('hiddenPluginNo').textContent = plugins.length - filteredPlugins.length; + let hiddenMessageNo = 0; + plugins.forEach((plugin) => { + hiddenMessageNo += plugin.messages.length - plugin.getCardContent(filters).messages.length; + }); + document.getElementById('hiddenMessageNo').textContent = hiddenMessageNo; + }).catch(handlePromiseError); +} diff --git a/src/gui/html/js/init.js b/src/gui/html/js/init.js index 8c011628..68a5179a 100644 --- a/src/gui/html/js/init.js +++ b/src/gui/html/js/init.js @@ -22,178 +22,262 @@ . */ 'use strict'; -var marked; -var l10n; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.initialise = factory(root.loot.Dialog, + root.loot.dom, + root.loot.Filters, + root.loot.Game, + root.loot.translateStaticText, + root.loot.Plugin, + root.loot.query, + root.loot.Translator); + } +}(this, (Dialog, dom, Filters, Game, translateStaticText, Plugin, query, Translator) => { + function setupEventHandlers() { + /* Set up handlers for filters. */ + document.getElementById('hideVersionNumbers').addEventListener('change', onToggleDisplayCSS); + document.getElementById('hideCRCs').addEventListener('change', onToggleDisplayCSS); + document.getElementById('hideBashTags').addEventListener('change', onToggleDisplayCSS); + document.getElementById('hideNotes').addEventListener('change', onSidebarFilterToggle); + document.getElementById('hideDoNotCleanMessages').addEventListener('change', onSidebarFilterToggle); + document.getElementById('hideInactivePlugins').addEventListener('change', onSidebarFilterToggle); + document.getElementById('hideAllPluginMessages').addEventListener('change', onSidebarFilterToggle); + document.getElementById('hideMessagelessPlugins').addEventListener('change', onSidebarFilterToggle); + document.body.addEventListener('loot-filter-conflicts', onConflictsFilter); -function initVars() { - loot.query('getVersion').then(function(result){ - try { - loot.version = JSON.parse(result); + /* Set up event handlers for content filter. */ + document.getElementById('contentFilter').addEventListener('change', onSidebarFilterToggle); - /* The fourth part of the version string is the build number. Trim it. */ - var pos = loot.version.lastIndexOf('.'); - if (loot.version.length > pos + 1) { - document.getElementById('LOOTBuild').textContent = loot.version.substring(pos + 1); - } else { - document.getElementById('LOOTBuild').textContent = l10n.jed.translate('unknown').fetch(); - } + /* Set up handlers for buttons. */ + document.getElementById('redatePluginsButton').addEventListener('click', onRedatePlugins); + document.getElementById('openLogButton').addEventListener('click', onOpenLogLocation); + document.getElementById('wipeUserlistButton').addEventListener('click', onClearAllMetadata); + document.getElementById('copyLoadOrderButton').addEventListener('click', onCopyLoadOrder); + document.getElementById('copyContentButton').addEventListener('click', onCopyContent); + document.getElementById('refreshContentButton').addEventListener('click', onContentRefresh); + document.getElementById('settingsButton').addEventListener('click', onShowSettingsDialog); + document.getElementById('helpButton').addEventListener('click', onOpenReadme); + document.getElementById('aboutButton').addEventListener('click', onShowAboutDialog); + document.getElementById('quitButton').addEventListener('click', onQuit); + document.getElementById('gameMenu').addEventListener('core-select', onChangeGame); + document.getElementById('updateMasterlistButton').addEventListener('click', onUpdateMasterlist); + document.getElementById('sortButton').addEventListener('click', onSortPlugins); + document.getElementById('applySortButton').addEventListener('click', onApplySort); + document.getElementById('cancelSortButton').addEventListener('click', onCancelSort); + document.getElementById('sidebarTabs').addEventListener('core-select', onSwitchSidebarTab); + document.getElementById('jumpToGeneralInfo').addEventListener('click', onJumpToGeneralInfo); - loot.version = loot.version.substring(0, pos); - document.getElementById('LOOTVersion').textContent = loot.version; - document.getElementById('firstTimeLootVersion').textContent = loot.version; - } catch (e) { - console.log(e); - console.log('Response: ' + result); - } - }).catch(processCefError); + /* Set up search event handlers. */ + document.getElementById('showSearch').addEventListener('click', onSearchOpen); + document.getElementById('searchBar').addEventListener('loot-search-close', onSearchClose); + window.addEventListener('keyup', onFocusSearch); - loot.query('getLanguages').then(function(result){ - try { - loot.languages = JSON.parse(result); + /* Set up event handlers for settings dialog. */ + const settings = document.getElementById('settingsDialog'); + settings.getElementsByClassName('accept')[0].addEventListener('click', onCloseSettingsDialog); + settings.getElementsByClassName('cancel')[0].addEventListener('click', onCloseSettingsDialog); - /* Now fill in language options. */ - var settingsLangSelect = document.getElementById('languageSelect'); - var messageLangSelect = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (messageLangSelect) { - messageLangSelect = messageLangSelect.import.querySelector('#messageRow').content.querySelector('.language'); - } else { - messageLangSelect = document.querySelector('#messageRow').content.querySelector('.language'); - } + /* Set up handler for opening and closing editors. */ + document.body.addEventListener('loot-editor-open', onEditorOpen); + document.body.addEventListener('loot-editor-close', onEditorClose); + document.body.addEventListener('loot-copy-metadata', onCopyMetadata); + document.body.addEventListener('loot-clear-metadata', onClearMetadata); - for (var i = 0; i < loot.languages.length; ++i) { - var settingsItem = document.createElement('paper-item'); - settingsItem.setAttribute('value', loot.languages[i].locale); - settingsItem.setAttribute('noink', ''); - settingsItem.textContent = loot.languages[i].name; - settingsLangSelect.appendChild(settingsItem); - messageLangSelect.appendChild(settingsItem.cloneNode(true)); - } + document.getElementById('cardsNav').addEventListener('click', onSidebarClick); + document.getElementById('cardsNav').addEventListener('dblclick', onSidebarClick); - messageLangSelect.setAttribute('value', messageLangSelect.firstElementChild.getAttribute('value')); - } catch (e) { - console.log(e); - console.log('Response: ' + result); - } - }).catch(processCefError); + /* Set up handler for plugin message and dirty info changes. */ + document.addEventListener('loot-plugin-message-change', Plugin.onMessageChange); + document.addEventListener('loot-plugin-isdirty-change', Plugin.onIsDirtyChange); - loot.query('getInitErrors').then(JSON.parse).then(function(result){ - if (result) { - var generalMessagesList = document.getElementById('summary').getElementsByTagName('ul')[0]; + /* Set up event handlers for game member variable changes. */ + document.addEventListener('loot-game-folder-change', Game.onFolderChange); + document.addEventListener('loot-game-masterlist-change', Game.onMasterlistChange); + document.addEventListener('loot-game-global-messages-change', Game.onGlobalMessagesChange); + document.addEventListener('loot-game-plugins-change', Game.onPluginsChange); + } - 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); - }); + function applyEnabledFilters(filters, settings, plugins) { + if (!filters) { + return; + } - document.getElementById('filterTotalMessageNo').textContent = result.length; - document.getElementById('totalMessageNo').textContent = result.length; - document.getElementById('totalErrorNo').textContent = result.length; - } + if (settings.filters) { + for (const filter in settings.filters) { + filters[filter] = settings.filters[filter]; + document.getElementById(filter).checked = filters[filter]; + } + } - var parallelPromises = [ - loot.query('getGameTypes'), - loot.query('getInstalledGames'), - loot.query('getSettings'), - ]; + if (filters.hideMessagelessPlugins + || filters.hideInactivePlugins + || filters.hideNotes + || filters.hideDoNotCleanMessages + || filters.hideAllPluginMessages) { + filterPluginData(plugins, filters); + } - showProgress('Initialising user interface...'); - Promise.all(parallelPromises).then(function(results) { - try { - loot.gameTypes = JSON.parse(results[0]); - } catch (e) { - console.log(e); - console.log('getGameTypes response: ' + results[0]); - } + if (filters.hideVersionNumbers) { + document.getElementById('hideVersionNumbers').dispatchEvent(new Event('change')); + } - /* Fill in game row template's game type options. */ - var select = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (select) { - select = select.import.querySelector('#gameRow').content.querySelector('.type') - } else { - select = document.querySelector('#gameRow').content.querySelector('.type'); - } - for (var j = 0; j < loot.gameTypes.length; ++j) { - var item = document.createElement('paper-item'); - item.setAttribute('value', loot.gameTypes[j]); - item.setAttribute('noink', ''); - item.textContent = loot.gameTypes[j]; - select.appendChild(item); - } - select.setAttribute('value', select.firstElementChild.getAttribute('value')); + if (filters.hideCRCs) { + document.getElementById('hideCRCs').dispatchEvent(new Event('change')); + } - try { - loot.installedGames = JSON.parse(results[1]); - } catch (e) { - console.log(e); - console.log('getInstalledGames response: ' + results[1]); - } + if (filters.hideBashTags) { + document.getElementById('hideBashTags').dispatchEvent(new Event('change')); + } + } - try { - loot.settings = JSON.parse(results[2]); - restoreFilterStates(); - } catch (e) { - console.log(e); - console.log('getSettings response: ' + results[2]); - } - }).then(function(){ - return l10n.getJedInstance(loot.settings.language).then(function(jed){ - l10n.translateStaticText(jed); - l10n.jed = jed; + function setVersion(appData) { + return query('getVersion').then(JSON.parse).then((result) => { + /* The fourth part of the version string is the build number. Trim it. */ + const pos = result.lastIndexOf('.'); + appData.version = result.substring(0, pos); + document.getElementById('LOOTVersion').textContent = appData.version; + document.getElementById('firstTimeLootVersion').textContent = appData.version; + document.getElementById('LOOTBuild').textContent = result.substring(pos + 1); + }); + } - /* Also need to update the settings UI. */ - loot.updateSettingsUI(); - }).catch(processCefError); - }).then(function(){ - if (result) { - return new Promise(function(resolve, reject){ - closeProgressDialog(); - document.getElementById('settingsButton').click(); - resolve(''); - }); - } else { - return loot.query('getGameData').then(function(result){ - var game = JSON.parse(result, jsonToPlugin); - loot.game.folder = game.folder; - loot.game.masterlist = game.masterlist; - loot.game.globalMessages = game.globalMessages; - loot.game.plugins = game.plugins; - document.getElementById('cardsNav').data = loot.game.plugins; - document.getElementById('main').lastElementChild.data = loot.game.plugins; - applyEnabledFilters(); + function setLanguages() { + return query('getLanguages').then(JSON.parse).then((result) => { + /* Now fill in language options. */ + const settingsLangSelect = document.getElementById('languageSelect'); + const messageLangSelect = dom.getElementInTableRowTemplate('messageRow', 'language'); - setTimeout(function() { - document.getElementById('cardsNav').updateSize(); - closeProgressDialog(); - }, 100); + result.forEach((language) => { + const settingsItem = document.createElement('paper-item'); + settingsItem.setAttribute('value', language.locale); + settingsItem.setAttribute('noink', ''); + settingsItem.textContent = language.name; + settingsLangSelect.appendChild(settingsItem); + messageLangSelect.appendChild(settingsItem.cloneNode(true)); + }); - return ''; - }).catch(processCefError); - } - }).then(function(){ - if (!loot.settings.lastVersion || loot.settings.lastVersion != loot.version) { - document.getElementById('firstRun').showModal(); - } - }).catch(processCefError); - }).catch(processCefError); -} + messageLangSelect.setAttribute('value', messageLangSelect.firstElementChild.getAttribute('value')); + }); + } -window.addEventListener('polymer-ready', function(e) { + function displayInitErrors() { + return query('getInitErrors').then(JSON.parse).then((result) => { + if (!result) { + return result; + } + const generalMessagesList = document.getElementById('summary').getElementsByTagName('ul')[0]; + + result.forEach((message) => { + const li = document.createElement('li'); + li.className = 'error'; + /* Use the Marked library for Markdown formatting support. */ + li.innerHTML = window.marked(message); + generalMessagesList.appendChild(li); + }); + + document.getElementById('filterTotalMessageNo').textContent = result.length; + document.getElementById('totalMessageNo').textContent = result.length; + document.getElementById('totalErrorNo').textContent = result.length; + + return result; + }); + } + + function setGameTypes() { + return query('getGameTypes').then(JSON.parse).then((result) => { + /* Fill in game row template's game type options. */ + const select = dom.getElementInTableRowTemplate('gameRow', 'type'); + result.forEach((gameType) => { + const item = document.createElement('paper-item'); + item.setAttribute('value', gameType); + item.setAttribute('noink', ''); + item.textContent = gameType; + select.appendChild(item); + }); + select.setAttribute('value', select.firstElementChild.getAttribute('value')); + }); + } + + function setInstalledGames(appData) { + return query('getInstalledGames').then(JSON.parse).then((installedGames) => { + appData.installedGames = installedGames; + dom.updateEnabledGames(installedGames); + }); + } + + function setSettings(appData) { + return query('getSettings').then(JSON.parse).then((result) => { + appData.settings = result; + dom.updateSettingsDialog(appData.settings, appData.installedGames, appData.game.folder); + }); + } + + function setGameData(appData) { + return query('getGameData').then((result) => { + const game = JSON.parse(result, Plugin.fromJson); + appData.game = new Game(game, appData.l10n); + document.getElementById('cardsNav').data = appData.game.plugins; + document.getElementById('main').lastElementChild.data = appData.game.plugins; + applyEnabledFilters(appData.filters, appData.settings, appData.game.plugins); + Dialog.closeProgress(); + }); + } + + return () => { + Dialog.showProgress('Initialising user interface...'); /* Set the plugin list's scroll target to its parent. */ - document.getElementById('main').lastElementChild.scrollTarget = document.getElementById('main'); - - /* Register object observers. */ - Object.observe(loot, loot.observer); - Object.observe(loot.game, loot.gameObserver); + document.getElementById('pluginCardList').scrollTarget = document.getElementById('main'); /* Make sure settings are what I want. */ - marked.setOptions({ - gfm: true, - tables: true, - sanitize: true + window.marked.setOptions({ + gfm: true, + tables: true, + sanitize: true, }); setupEventHandlers(); - initVars(); -}, false); + + loot.version = ''; + loot.settings = {}; + loot.l10n = new Translator(); + loot.game = new Game({}, loot.l10n); + loot.filters = new Filters(loot.l10n); + + Promise.all([ + setLanguages(), + setGameTypes(), + setInstalledGames(loot), + setVersion(loot), + setSettings(loot), + ]).then(() => { + /* Translate static text. */ + loot.l10n = new Translator(loot.settings.language); + return loot.l10n.load(); + }).then(() => { + loot.filters = new Filters(loot.l10n); + translateStaticText(loot.l10n); + /* Also need to update the settings UI. */ + dom.updateSettingsDialog(loot.settings, loot.installedGames, loot.game.folder); + }).then(() => { + return displayInitErrors(); + }).then((result) => { + if (result) { + Dialog.closeProgress(); + document.getElementById('settingsButton').click(); + return Promise.resolve(); + } + return setGameData(loot); + }).then(() => { + if (!loot.settings.lastVersion || loot.settings.lastVersion !== loot.version) { + document.getElementById('firstRun').showModal(); + } + }).catch(handlePromiseError); + }; +})); + +window.addEventListener('polymer-ready', loot.initialise); diff --git a/src/gui/html/js/l10n.js b/src/gui/html/js/l10n.js index 65aa32dc..ca3ff8f2 100644 --- a/src/gui/html/js/l10n.js +++ b/src/gui/html/js/l10n.js @@ -1,334 +1,344 @@ 'use strict'; -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['bower_components/Jed/jed', 'bower_components/jed-gettext-parser/jedGettextParser'], factory); +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.translateStaticText = factory(); + } +}(this, () => { + function translatePluginCardTemplate(l10n) { + /* Plugin card template. */ + let pluginCard = document.querySelector('link[rel="import"][href$="loot-plugin-card.html"]'); + if (pluginCard) { + pluginCard = pluginCard.import.querySelector('template').content; } else { - // Browser globals - root.l10n = factory(root.Jed, root.jedGettextParser); + pluginCard = document.querySelector('polymer-element[name="loot-plugin-card"]').querySelector('template').content; } -}(this, function (jed, jedGettextParser) { - var defaultData = { - "messages": { - "": { - "domain" : "messages", - "lang" : "en", - "plural_forms" : "nplurals=2; plural=(n != 1);" - } - } - }; + pluginCard.getElementById('activeTick').setAttribute('label', l10n.translate('Active Plugin')); + pluginCard.getElementById('isMaster').setAttribute('label', l10n.translate('Master File')); + pluginCard.getElementById('emptyPlugin').setAttribute('label', l10n.translate('Empty Plugin')); + pluginCard.getElementById('loadsArchive').setAttribute('label', l10n.translate('Loads Archive')); + pluginCard.getElementById('hasUserEdits').setAttribute('label', l10n.translate('Has User Metadata')); - return { + pluginCard.getElementById('showOnlyConflicts').previousElementSibling.textContent = l10n.translate('Show Only Conflicts'); + pluginCard.getElementById('editMetadata').lastChild.textContent = l10n.translate('Edit Metadata'); + pluginCard.getElementById('copyMetadata').lastChild.textContent = l10n.translate('Copy Metadata'); + pluginCard.getElementById('clearMetadata').lastChild.textContent = l10n.translate('Clear User Metadata'); + } - loadLocaleData: function(locale) { - if (locale == 'en') { - return new Promise(function(resolve, reject){ - resolve(defaultData); - }); - } + function translatePluginEditorTemplate(l10n) { + /* Plugin editor template. */ + let pluginEditor = document.querySelector('link[rel="import"][href$="loot-plugin-card.html"]'); + if (pluginEditor) { + pluginEditor = pluginEditor.import.querySelector('link[rel="import"][href$="loot-plugin-editor.html"]').import.querySelector('template').content; + } else { + pluginEditor = document.querySelector('polymer-element[name="loot-plugin-editor"]').querySelector('template').content; + } - var url = 'loot://l10n/' + locale + '/LC_MESSAGES/loot.mo'; + pluginEditor.getElementById('activeTick').setAttribute('label', l10n.translate('Active Plugin')); + pluginEditor.getElementById('isMaster').setAttribute('label', l10n.translate('Master File')); + pluginEditor.getElementById('emptyPlugin').setAttribute('label', l10n.translate('Empty Plugin')); + pluginEditor.getElementById('loadsArchive').setAttribute('label', l10n.translate('Loads Archive')); - return new Promise(function(resolve, reject){ - var xhr = new XMLHttpRequest(); - xhr.open('GET', url); - xhr.responseType = 'arraybuffer'; - xhr.addEventListener('readystatechange', function(evt){ - if (evt.target.readyState == 4) { - /* Status is 0 for local file URL loading. */ - if (evt.target.status >= 200 && evt.target.status < 400) { - resolve(jedGettextParser.mo.parse(evt.target.response)); - } else { - reject(new Error('XHR Error')); - } - } - }, false); - xhr.send(); - }); - }, + pluginEditor.getElementById('enableEdits').previousElementSibling.textContent = l10n.translate('Enable Edits'); + pluginEditor.getElementById('globalPriority').parentElement.parentElement.setAttribute('label', l10n.translate('Global priorities are compared against all other plugins. Normal priorities are compared against only conflicting plugins.')); + pluginEditor.getElementById('globalPriority').previousElementSibling.textContent = l10n.translate('Global Priority'); + pluginEditor.getElementById('priorityValue').parentElement.previousElementSibling.textContent = l10n.translate('Priority Value'); - translateStaticText: function(l10n) { - /* Plugin card template. */ - var pluginCard = document.querySelector('link[rel="import"][href$="loot-plugin-card.html"]'); - if (pluginCard) { - pluginCard = pluginCard.import.querySelector('template').content; - } else { - pluginCard = document.querySelector('polymer-element[name="loot-plugin-card"]').querySelector('template').content; - } + pluginEditor.getElementById('tableTabs').querySelector('[data-for=main]').textContent = l10n.translate('Main'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=loadAfter]').textContent = l10n.translate('Load After'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=req]').textContent = l10n.translate('Requirements'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=inc]').textContent = l10n.translate('Incompatibilities'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=message]').textContent = l10n.translate('Messages'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=tags]').textContent = l10n.translate('Bash Tags'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=dirty]').textContent = l10n.translate('Dirty Info'); + pluginEditor.getElementById('tableTabs').querySelector('[data-for=locations]').textContent = l10n.translate('Locations'); - pluginCard.getElementById('activeTick').setAttribute('label', l10n.translate("Active Plugin").fetch()); - pluginCard.getElementById('isMaster').setAttribute('label', l10n.translate("Master File").fetch()); - pluginCard.getElementById('emptyPlugin').setAttribute('label', l10n.translate("Empty Plugin").fetch()); - pluginCard.getElementById('loadsArchive').setAttribute('label', l10n.translate("Loads Archive").fetch()); - pluginCard.getElementById('hasUserEdits').setAttribute('label', l10n.translate("Has User Metadata").fetch()); + pluginEditor.getElementById('loadAfter').querySelector('th:first-child').textContent = l10n.translate('Filename'); + pluginEditor.getElementById('loadAfter').querySelector('th:nth-child(2)').textContent = l10n.translate('Display Name'); + pluginEditor.getElementById('loadAfter').querySelector('th:nth-child(3)').textContent = l10n.translate('Condition'); - pluginCard.getElementById('showOnlyConflicts').previousElementSibling.textContent = l10n.translate("Show Only Conflicts").fetch(); - pluginCard.getElementById('editMetadata').lastChild.textContent = l10n.translate("Edit Metadata").fetch(); - pluginCard.getElementById('copyMetadata').lastChild.textContent = l10n.translate("Copy Metadata").fetch(); - pluginCard.getElementById('clearMetadata').lastChild.textContent = l10n.translate("Clear User Metadata").fetch(); + pluginEditor.getElementById('req').querySelector('th:first-child').textContent = l10n.translate('Filename'); + pluginEditor.getElementById('req').querySelector('th:nth-child(2)').textContent = l10n.translate('Display Name'); + pluginEditor.getElementById('req').querySelector('th:nth-child(3)').textContent = l10n.translate('Condition'); - /* Plugin editor template. */ - var pluginEditor = document.querySelector('link[rel="import"][href$="loot-plugin-card.html"]'); - if (pluginEditor) { - pluginEditor = pluginEditor.import.querySelector('link[rel="import"][href$="loot-plugin-editor.html"]').import.querySelector('template').content; - } else { - pluginEditor = document.querySelector('polymer-element[name="loot-plugin-editor"]').querySelector('template').content; - } + pluginEditor.getElementById('inc').querySelector('th:first-child').textContent = l10n.translate('Filename'); + pluginEditor.getElementById('inc').querySelector('th:nth-child(2)').textContent = l10n.translate('Display Name'); + pluginEditor.getElementById('inc').querySelector('th:nth-child(3)').textContent = l10n.translate('Condition'); - pluginEditor.getElementById('activeTick').setAttribute('label', l10n.translate("Active Plugin").fetch()); - pluginEditor.getElementById('isMaster').setAttribute('label', l10n.translate("Master File").fetch()); - pluginEditor.getElementById('emptyPlugin').setAttribute('label', l10n.translate("Empty Plugin").fetch()); - pluginEditor.getElementById('loadsArchive').setAttribute('label', l10n.translate("Loads Archive").fetch()); + pluginEditor.getElementById('message').querySelector('th:first-child').textContent = l10n.translate('Type'); + pluginEditor.getElementById('message').querySelector('th:nth-child(2)').textContent = l10n.translate('Content'); + pluginEditor.getElementById('message').querySelector('th:nth-child(3)').textContent = l10n.translate('Condition'); + pluginEditor.getElementById('message').querySelector('th:nth-child(4)').textContent = l10n.translate('Language'); - pluginEditor.getElementById('enableEdits').previousElementSibling.textContent = l10n.translate("Enable Edits").fetch(); - pluginEditor.getElementById('globalPriority').parentElement.parentElement.setAttribute('label', l10n.translate("Global priorities are compared against all other plugins. Normal priorities are compared against only conflicting plugins.").fetch()); - pluginEditor.getElementById('globalPriority').previousElementSibling.textContent = l10n.translate("Global Priority").fetch(); - pluginEditor.getElementById('priorityValue').parentElement.previousElementSibling.textContent = l10n.translate("Priority Value").fetch(); + pluginEditor.getElementById('tags').querySelector('th:first-child').textContent = l10n.translate('Add/Remove'); + pluginEditor.getElementById('tags').querySelector('th:nth-child(2)').textContent = l10n.translate('Bash Tag'); + pluginEditor.getElementById('tags').querySelector('th:nth-child(3)').textContent = l10n.translate('Condition'); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=main]').textContent = l10n.translate("Main").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=loadAfter]').textContent = l10n.translate("Load After").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=req]').textContent = l10n.translate("Requirements").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=inc]').textContent = l10n.translate("Incompatibilities").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=message]').textContent = l10n.translate("Messages").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=tags]').textContent = l10n.translate("Bash Tags").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=dirty]').textContent = l10n.translate("Dirty Info").fetch(); - pluginEditor.getElementById('tableTabs').querySelector('[data-for=locations]').textContent = l10n.translate("Locations").fetch(); + pluginEditor.getElementById('dirty').querySelector('th:first-child').textContent = l10n.translate('CRC'); + pluginEditor.getElementById('dirty').querySelector('th:nth-child(2)').textContent = l10n.translate('ITM Count'); + pluginEditor.getElementById('dirty').querySelector('th:nth-child(3)').textContent = l10n.translate('Deleted References'); + pluginEditor.getElementById('dirty').querySelector('th:nth-child(4)').textContent = l10n.translate('Deleted Navmeshes'); + pluginEditor.getElementById('dirty').querySelector('th:nth-child(5)').textContent = l10n.translate('Cleaning Utility'); - pluginEditor.getElementById('loadAfter').querySelector('th:first-child').textContent = l10n.translate("Filename").fetch(); - pluginEditor.getElementById('loadAfter').querySelector('th:nth-child(2)').textContent = l10n.translate("Display Name").fetch(); - pluginEditor.getElementById('loadAfter').querySelector('th:nth-child(3)').textContent = l10n.translate("Condition").fetch(); + pluginEditor.getElementById('locations').querySelector('th:first-child').textContent = l10n.translate('URL'); + pluginEditor.getElementById('locations').querySelector('th:nth-child(2)').textContent = l10n.translate('Name'); - pluginEditor.getElementById('req').querySelector('th:first-child').textContent = l10n.translate("Filename").fetch(); - pluginEditor.getElementById('req').querySelector('th:nth-child(2)').textContent = l10n.translate("Display Name").fetch(); - pluginEditor.getElementById('req').querySelector('th:nth-child(3)').textContent = l10n.translate("Condition").fetch(); + pluginEditor.getElementById('accept').parentElement.setAttribute('label', l10n.translate('Apply')); + pluginEditor.getElementById('cancel').parentElement.setAttribute('label', l10n.translate('Cancel')); + } - pluginEditor.getElementById('inc').querySelector('th:first-child').textContent = l10n.translate("Filename").fetch(); - pluginEditor.getElementById('inc').querySelector('th:nth-child(2)').textContent = l10n.translate("Display Name").fetch(); - pluginEditor.getElementById('inc').querySelector('th:nth-child(3)').textContent = l10n.translate("Condition").fetch(); + function translatePluginListItemTemplate(l10n) { + /* Plugin List Item Template */ + let pluginItem = document.querySelector('link[rel="import"][href$="loot-plugin-item.html"]'); + if (pluginItem) { + pluginItem = pluginItem.import.querySelector('template').content; + } else { + pluginItem = document.querySelector('polymer-element[name="loot-plugin-item"]').querySelector('template').content; + } + pluginItem.querySelector('#secondary core-tooltip').setAttribute('label', l10n.translate('Global Priority')); + pluginItem.getElementById('hasUserEditsTooltip').textContent = l10n.translate('Has User Metadata'); + pluginItem.getElementById('editorIsOpenTooltip').textContent = l10n.translate('Editor Is Open'); + } - pluginEditor.getElementById('message').querySelector('th:first-child').textContent = l10n.translate("Type").fetch(); - pluginEditor.getElementById('message').querySelector('th:nth-child(2)').textContent = l10n.translate("Content").fetch(); - pluginEditor.getElementById('message').querySelector('th:nth-child(3)').textContent = l10n.translate("Condition").fetch(); - pluginEditor.getElementById('message').querySelector('th:nth-child(4)').textContent = l10n.translate("Language").fetch(); + function translateMessageDialogTemplate(l10n) { + /* Plugin List Item Template */ + let messageDialog = document.querySelector('link[rel="import"][href$="loot-message-dialog.html"]'); + if (messageDialog) { + messageDialog = messageDialog.import.querySelector('template').content; + } else { + messageDialog = document.querySelector('polymer-element[name="loot-message-dialog"]').querySelector('template').content; + } + messageDialog.getElementById('confirm').textContent = l10n.translate('OK'); + messageDialog.getElementById('dismiss').textContent = l10n.translate('Cancel'); + } - pluginEditor.getElementById('tags').querySelector('th:first-child').textContent = l10n.translate("Add/Remove").fetch(); - pluginEditor.getElementById('tags').querySelector('th:nth-child(2)').textContent = l10n.translate("Bash Tag").fetch(); - pluginEditor.getElementById('tags').querySelector('th:nth-child(3)').textContent = l10n.translate("Condition").fetch(); + function translateFileRowTemplate(l10n) { + /* File row template */ + let fileRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (fileRow) { + fileRow = fileRow.import.querySelector('#fileRow').content; + } else { + fileRow = document.querySelector('#fileRow').content; + } + fileRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate('A filename is required.')); + fileRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - pluginEditor.getElementById('dirty').querySelector('th:first-child').textContent = l10n.translate("CRC").fetch(); - pluginEditor.getElementById('dirty').querySelector('th:nth-child(2)').textContent = l10n.translate("ITM Count").fetch(); - pluginEditor.getElementById('dirty').querySelector('th:nth-child(3)').textContent = l10n.translate("Deleted References").fetch(); - pluginEditor.getElementById('dirty').querySelector('th:nth-child(4)').textContent = l10n.translate("Deleted Navmeshes").fetch(); - pluginEditor.getElementById('dirty').querySelector('th:nth-child(5)').textContent = l10n.translate("Cleaning Utility").fetch(); + function translateMessageRowTemplate(l10n) { + /* Message row template */ + let messageRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (messageRow) { + messageRow = messageRow.import.querySelector('#messageRow').content; + } else { + messageRow = document.querySelector('#messageRow').content; + } + messageRow.querySelector('.type').children[0].textContent = l10n.translate('Note'); + messageRow.querySelector('.type').children[1].textContent = l10n.translate('Warning'); + messageRow.querySelector('.type').children[2].textContent = l10n.translate('Error'); + messageRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate('A content string is required.')); + messageRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - pluginEditor.getElementById('locations').querySelector('th:first-child').textContent = l10n.translate("URL").fetch(); - pluginEditor.getElementById('locations').querySelector('th:nth-child(2)').textContent = l10n.translate("Name").fetch(); + function translateTagRowTemplate(l10n) { + /* Tag row template */ + let tagRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (tagRow) { + tagRow = tagRow.import.querySelector('#tagRow').content; + } else { + tagRow = document.querySelector('#tagRow').content; + } + tagRow.querySelector('.type').children[0].textContent = l10n.translate('Add'); + tagRow.querySelector('.type').children[1].textContent = l10n.translate('Remove'); + tagRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate('A name is required.')); + tagRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - pluginEditor.getElementById('accept').parentElement.setAttribute('label', l10n.translate("Apply").fetch()); - pluginEditor.getElementById('cancel').parentElement.setAttribute('label', l10n.translate("Cancel").fetch()); + function translateDirtyInfoRowTemplate(l10n) { + /* Dirty Info row template */ + let dirtyInfoRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (dirtyInfoRow) { + dirtyInfoRow = dirtyInfoRow.import.querySelector('#dirtyInfoRow').content; + } else { + dirtyInfoRow = document.querySelector('#dirtyInfoRow').content; + } - /* Plugin List Item Template */ - var pluginItem = document.querySelector('link[rel="import"][href$="loot-plugin-item.html"]'); - if (pluginItem) { - pluginItem = pluginItem.import.querySelector('template').content; - } else { - pluginItem = document.querySelector('polymer-element[name="loot-plugin-item"]').querySelector('template').content; - } - pluginItem.querySelector('#secondary core-tooltip').setAttribute('label', l10n.translate("Global Priority").fetch()); - pluginItem.getElementById('hasUserEditsTooltip').textContent = l10n.translate("Has User Metadata").fetch(); - pluginItem.getElementById('editorIsOpenTooltip').textContent = l10n.translate("Editor Is Open").fetch(); + dirtyInfoRow.querySelector('loot-validated-input.crc').setAttribute('error', l10n.translate('A CRC is required.')); + dirtyInfoRow.querySelector('loot-validated-input.itm').setAttribute('error', l10n.translate('Values must be integers.')); + dirtyInfoRow.querySelector('loot-validated-input.udr').setAttribute('error', l10n.translate('Values must be integers.')); + dirtyInfoRow.querySelector('loot-validated-input.nav').setAttribute('error', l10n.translate('Values must be integers.')); + dirtyInfoRow.querySelector('loot-validated-input.util').setAttribute('error', l10n.translate('A utility name is required.')); + dirtyInfoRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - /* File row template */ - var fileRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (fileRow) { - fileRow = fileRow.import.querySelector('#fileRow').content; - } else { - fileRow = document.querySelector('#fileRow').content; - } - fileRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate("A filename is required.").fetch()); - fileRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + function translateLocationRowTemplate(l10n) { + /* Location row template */ + let locationRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (locationRow) { + locationRow = locationRow.import.querySelector('#locationRow').content; + } else { + locationRow = document.querySelector('#locationRow').content; + } + locationRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate('A link is required.')); + locationRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - /* Message row template */ - var messageRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (messageRow) { - messageRow = messageRow.import.querySelector('#messageRow').content; - } else { - messageRow = document.querySelector('#messageRow').content; - } - messageRow.querySelector('.type').children[0].textContent = l10n.translate("Note").fetch(); - messageRow.querySelector('.type').children[1].textContent = l10n.translate("Warning").fetch(); - messageRow.querySelector('.type').children[2].textContent = l10n.translate("Error").fetch(); - messageRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate("A content string is required.").fetch()); - messageRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + function translateGameRowTemplate(l10n) { + /* Game row template */ + let gameRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (gameRow) { + gameRow = gameRow.import.querySelector('#gameRow').content; + } else { + gameRow = document.querySelector('#gameRow').content; + } + gameRow.querySelector('loot-validated-input.name').setAttribute('error', l10n.translate('A name is required.')); + gameRow.querySelector('loot-validated-input.folder').setAttribute('error', l10n.translate('A folder is required.')); + gameRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Delete Row')); + } - /* Tag row template */ - var tagRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (tagRow) { - tagRow = tagRow.import.querySelector('#tagRow').content; - } else { - tagRow = document.querySelector('#tagRow').content; - } - tagRow.querySelector('.type').children[0].textContent = l10n.translate("Add").fetch(); - tagRow.querySelector('.type').children[1].textContent = l10n.translate("Remove").fetch(); - tagRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate("A name is required.").fetch()); - tagRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + function translateNewRowTemplate(l10n) { + /* New row template */ + let newRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); + if (newRow) { + newRow = newRow.import.querySelector('#newRow').content; + } else { + newRow = document.querySelector('#newRow').content; + } + newRow.querySelector('core-tooltip').setAttribute('label', l10n.translate('Add New Row')); + } - /* Dirty Info row template */ - var dirtyInfoRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (dirtyInfoRow) { - dirtyInfoRow = dirtyInfoRow.import.querySelector('#dirtyInfoRow').content; - } else { - dirtyInfoRow = document.querySelector('#dirtyInfoRow').content; - } - dirtyInfoRow.querySelector('loot-validated-input.crc').setAttribute('error', l10n.translate("A CRC is required.").fetch()); - dirtyInfoRow.querySelector('loot-validated-input.itm').setAttribute('error', l10n.translate("Values must be integers.").fetch()); - dirtyInfoRow.querySelector('loot-validated-input.udr').setAttribute('error', l10n.translate("Values must be integers.").fetch()); - dirtyInfoRow.querySelector('loot-validated-input.nav').setAttribute('error', l10n.translate("Values must be integers.").fetch()); - dirtyInfoRow.querySelector('loot-validated-input.util').setAttribute('error', l10n.translate("A utility name is required.").fetch()); - dirtyInfoRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + function translateMainToolbar(l10n) { + /* Main toolbar */ + document.getElementById('jumpToGeneralInfo').parentElement.label = l10n.translate('Jump To General Information'); + document.getElementById('sortButton').parentElement.label = l10n.translate('Sort Plugins'); + document.getElementById('updateMasterlistButton').parentElement.label = l10n.translate('Update Masterlist'); + document.getElementById('applySortButton').textContent = l10n.translate('Apply'); + document.getElementById('cancelSortButton').textContent = l10n.translate('Cancel'); + document.getElementById('showSearch').parentElement.label = l10n.translate('Search Cards'); - /* Location row template */ - var locationRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (locationRow) { - locationRow = locationRow.import.querySelector('#locationRow').content; - } else { - locationRow = document.querySelector('#locationRow').content; - } - locationRow.querySelector('loot-validated-input').setAttribute('error', l10n.translate("A link is required.").fetch()); - locationRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + /* Toolbar menu */ + document.getElementById('redatePluginsButton').lastElementChild.textContent = l10n.translate('Redate Plugins'); + document.getElementById('openLogButton').lastElementChild.textContent = l10n.translate('Open Debug Log Location'); + document.getElementById('wipeUserlistButton').lastElementChild.textContent = l10n.translate('Clear All User Metadata'); + document.getElementById('copyLoadOrderButton').lastElementChild.textContent = l10n.translate('Copy Load Order'); + document.getElementById('copyContentButton').lastElementChild.textContent = l10n.translate('Copy Content'); + document.getElementById('refreshContentButton').lastElementChild.textContent = l10n.translate('Refresh Content'); + document.getElementById('helpButton').lastElementChild.textContent = l10n.translate('View Documentation'); + document.getElementById('aboutButton').lastElementChild.textContent = l10n.translate('About'); + document.getElementById('settingsButton').lastElementChild.textContent = l10n.translate('Settings'); + document.getElementById('quitButton').lastElementChild.textContent = l10n.translate('Quit'); - /* Game row template */ - var gameRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (gameRow) { - gameRow = gameRow.import.querySelector('#gameRow').content; - } else { - gameRow = document.querySelector('#gameRow').content; - } - gameRow.querySelector('loot-validated-input.name').setAttribute('error', l10n.translate("A name is required.").fetch()); - gameRow.querySelector('loot-validated-input.folder').setAttribute('error', l10n.translate("A folder is required.").fetch()); - gameRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Delete Row").fetch()); + /* Search bar */ + document.getElementById('searchBar').shadowRoot.getElementById('search').label = l10n.translate('Search cards'); + } - /* New row template */ - var newRow = document.querySelector('link[rel="import"][href$="editable-table.html"]'); - if (newRow) { - newRow = newRow.import.querySelector('#newRow').content; - } else { - newRow = document.querySelector('#newRow').content; - } - newRow.querySelector('core-tooltip').setAttribute('label', l10n.translate("Add New Row").fetch()); + function translateSidebar(l10n) { + /* Nav items */ + document.getElementById('sidebarTabs').firstElementChild.textContent = l10n.translate('Plugins'); + document.getElementById('sidebarTabs').firstElementChild.nextElementSibling.textContent = l10n.translate('Filters'); + document.getElementById('contentFilter').parentElement.label = l10n.translate('Press Enter or click outside the input to set the filter.'); + document.getElementById('contentFilter').label = l10n.translate('Filter content'); - /* Main toolbar */ - document.getElementById('jumpToGeneralInfo').parentElement.label = l10n.translate("Jump To General Information").fetch(); - document.getElementById('sortButton').parentElement.label = l10n.translate("Sort Plugins").fetch(); - document.getElementById('updateMasterlistButton').parentElement.label = l10n.translate("Update Masterlist").fetch(); - document.getElementById('applySortButton').textContent = l10n.translate("Apply").fetch(); - document.getElementById('cancelSortButton').textContent = l10n.translate("Cancel").fetch(); - document.getElementById('showSearch').parentElement.label = l10n.translate("Search Cards").fetch(); + /* Filters */ + document.getElementById('hideVersionNumbers').label = l10n.translate('Hide version numbers'); + document.getElementById('hideCRCs').label = l10n.translate('Hide CRCs'); + document.getElementById('hideBashTags').label = l10n.translate('Hide Bash Tags'); + document.getElementById('hideNotes').label = l10n.translate('Hide notes'); + document.getElementById('hideDoNotCleanMessages').label = l10n.translate('Hide \'Do not clean\' messages'); + document.getElementById('hideAllPluginMessages').label = l10n.translate('Hide all plugin messages'); + document.getElementById('hideInactivePlugins').label = l10n.translate('Hide inactive plugins'); + document.getElementById('hideMessagelessPlugins').label = l10n.translate('Hide messageless plugins'); + document.getElementById('hiddenPluginsTxt').textContent = l10n.translate('Hidden plugins:'); + document.getElementById('hiddenMessagesTxt').textContent = l10n.translate('Hidden messages:'); + } - /* Toolbar menu */ - document.getElementById('redatePluginsButton').lastElementChild.textContent = l10n.translate("Redate Plugins").fetch(); - document.getElementById('openLogButton').lastElementChild.textContent = l10n.translate("Open Debug Log Location").fetch(); - document.getElementById('wipeUserlistButton').lastElementChild.textContent = l10n.translate("Clear All User Metadata").fetch(); - document.getElementById('copyLoadOrderButton').lastElementChild.textContent = l10n.translate("Copy Load Order").fetch(); - document.getElementById('copyContentButton').lastElementChild.textContent = l10n.translate("Copy Content").fetch(); - document.getElementById('refreshContentButton').lastElementChild.textContent = l10n.translate("Refresh Content").fetch(); - document.getElementById('helpButton').lastElementChild.textContent = l10n.translate("View Documentation").fetch(); - document.getElementById('aboutButton').lastElementChild.textContent = l10n.translate("About").fetch(); - document.getElementById('settingsButton').lastElementChild.textContent = l10n.translate("Settings").fetch(); - document.getElementById('quitButton').lastElementChild.textContent = l10n.translate("Quit").fetch(); + function translateSummaryCard(l10n) { + /* Summary */ + document.getElementById('summary').firstElementChild.textContent = l10n.translate('General Information'); + document.getElementById('masterlistRevision').previousElementSibling.textContent = l10n.translate('Masterlist Revision'); + document.getElementById('masterlistDate').previousElementSibling.textContent = l10n.translate('Masterlist Date'); + document.getElementById('totalWarningNo').previousElementSibling.textContent = l10n.translate('Warnings'); + document.getElementById('totalErrorNo').previousElementSibling.textContent = l10n.translate('Errors'); + document.getElementById('totalMessageNo').previousElementSibling.textContent = l10n.translate('Total Messages'); + document.getElementById('activePluginNo').previousElementSibling.textContent = l10n.translate('Active Plugins'); + document.getElementById('dirtyPluginNo').previousElementSibling.textContent = l10n.translate('Dirty Plugins'); + document.getElementById('totalPluginNo').previousElementSibling.textContent = l10n.translate('Total Plugins'); + } - /* Search bar */ - document.getElementById('searchBar').shadowRoot.getElementById('search').label = l10n.translate("Search cards").fetch(); + function translateSettingsDialog(l10n) { + /* Settings dialog */ + document.getElementById('settingsDialog').heading = l10n.translate('Settings'); - /* Nav items */ - document.getElementById('sidebarTabs').firstElementChild.textContent = l10n.translate("Plugins").fetch(); - document.getElementById('sidebarTabs').firstElementChild.nextElementSibling.textContent = l10n.translate("Filters").fetch(); - document.getElementById('contentFilter').parentElement.label = l10n.translate("Press Enter or click outside the input to set the filter.").fetch(); - document.getElementById('contentFilter').label = l10n.translate("Filter content").fetch(); + const defaultGameSelect = document.getElementById('defaultGameSelect'); + defaultGameSelect.previousElementSibling.textContent = l10n.translate('Default Game'); + defaultGameSelect.firstElementChild.textContent = l10n.translate('Autodetect'); + /* The selected text doesn't update, so force that translation. */ + defaultGameSelect.shadowRoot.querySelector('paper-dropdown-menu').selectedItemLabel = defaultGameSelect.shadowRoot.querySelector('core-menu').selectedItem.textContent; - /* Filters */ - document.getElementById('hideVersionNumbers').label = l10n.translate("Hide version numbers").fetch(); - document.getElementById('hideCRCs').label = l10n.translate("Hide CRCs").fetch(); - document.getElementById('hideBashTags').label = l10n.translate("Hide Bash Tags").fetch(); - document.getElementById('hideNotes').label = l10n.translate("Hide notes").fetch(); - document.getElementById('hideDoNotCleanMessages').label = l10n.translate("Hide 'Do not clean' messages").fetch(); - document.getElementById('hideAllPluginMessages').label = l10n.translate("Hide all plugin messages").fetch(); - document.getElementById('hideInactivePlugins').label = l10n.translate("Hide inactive plugins").fetch(); - document.getElementById('hideMessagelessPlugins').label = l10n.translate("Hide messageless plugins").fetch(); - document.getElementById('hiddenPluginsTxt').textContent = l10n.translate("Hidden plugins:").fetch(); - document.getElementById('hiddenMessagesTxt').textContent = l10n.translate("Hidden messages:").fetch(); + document.getElementById('languageSelect').previousElementSibling.textContent = l10n.translate('Language'); + document.getElementById('languageSelect').previousElementSibling.label = l10n.translate('Language changes will be applied after LOOT is restarted.'); - /* Summary */ - document.getElementById('summary').firstElementChild.textContent = l10n.translate("General Information").fetch(); - document.getElementById('masterlistRevision').previousElementSibling.textContent = l10n.translate("Masterlist Revision").fetch(); - document.getElementById('masterlistDate').previousElementSibling.textContent = l10n.translate("Masterlist Date").fetch(); - document.getElementById('totalWarningNo').previousElementSibling.textContent = l10n.translate("Warnings").fetch(); - document.getElementById('totalErrorNo').previousElementSibling.textContent = l10n.translate("Errors").fetch(); - document.getElementById('totalMessageNo').previousElementSibling.textContent = l10n.translate("Total Messages").fetch(); - document.getElementById('activePluginNo').previousElementSibling.textContent = l10n.translate("Active Plugins").fetch(); - document.getElementById('dirtyPluginNo').previousElementSibling.textContent = l10n.translate("Dirty Plugins").fetch(); - document.getElementById('totalPluginNo').previousElementSibling.textContent = l10n.translate("Total Plugins").fetch(); + document.getElementById('enableDebugLogging').previousElementSibling.textContent = l10n.translate('Enable debug logging'); + document.getElementById('enableDebugLogging').parentElement.label = l10n.translate('The output is logged to the LOOTDebugLog.txt file.'); - /* Settings dialog */ - document.getElementById('settingsDialog').heading = l10n.translate("Settings").fetch(); + document.getElementById('updateMasterlist').previousElementSibling.textContent = l10n.translate('Update masterlist before sorting'); - var defaultGameSelect = document.getElementById('defaultGameSelect'); - defaultGameSelect.previousElementSibling.textContent = l10n.translate("Default Game").fetch(); - defaultGameSelect.firstElementChild.textContent = l10n.translate("Autodetect").fetch(); - /* The selected text doesn't update, so force that translation. */ - defaultGameSelect.shadowRoot.querySelector('paper-dropdown-menu').selectedItemLabel = defaultGameSelect.shadowRoot.querySelector('core-menu').selectedItem.textContent; + const gameTable = document.getElementById('gameTable'); + gameTable.querySelector('th:first-child').textContent = l10n.translate('Name'); + gameTable.querySelector('th:nth-child(2)').textContent = l10n.translate('Base Game'); + gameTable.querySelector('th:nth-child(3)').textContent = l10n.translate('LOOT Folder'); + gameTable.querySelector('th:nth-child(4)').textContent = l10n.translate('Master File'); + gameTable.querySelector('th:nth-child(5)').textContent = l10n.translate('Masterlist Repository URL'); + gameTable.querySelector('th:nth-child(6)').textContent = l10n.translate('Masterlist Repository Branch'); + gameTable.querySelector('th:nth-child(7)').textContent = l10n.translate('Install Path'); + gameTable.querySelector('th:nth-child(8)').textContent = l10n.translate('Install Path Registry Key'); - document.getElementById('languageSelect').previousElementSibling.textContent = l10n.translate("Language").fetch(); - document.getElementById('languageSelect').previousElementSibling.label = l10n.translate("Language changes will be applied after LOOT is restarted.").fetch(); + /* As the game table is attached on launch, its "Add New Row" + tooltip doesn't benefit from the template translation above. */ + gameTable.querySelector('tr:last-child core-tooltip').setAttribute('label', l10n.translate('Add New Row')); - document.getElementById('enableDebugLogging').previousElementSibling.textContent = l10n.translate("Enable debug logging").fetch(); - document.getElementById('enableDebugLogging').parentElement.label = l10n.translate("The output is logged to the LOOTDebugLog.txt file.").fetch(); + document.getElementById('settingsDialog').getElementsByClassName('accept')[0].textContent = l10n.translate('Apply'); + document.getElementById('settingsDialog').getElementsByClassName('cancel')[0].textContent = l10n.translate('Cancel'); + } - document.getElementById('updateMasterlist').previousElementSibling.textContent = l10n.translate("Update masterlist before sorting").fetch(); + function translateFirstRunDialog(l10n) { + /* First-run dialog */ + const firstRun = document.getElementById('firstRun'); + firstRun.heading = l10n.translate('First-Time Tips'); - var gameTable = document.getElementById('gameTable'); - gameTable.querySelector('th:first-child').textContent = l10n.translate("Name").fetch(); - gameTable.querySelector('th:nth-child(2)').textContent = l10n.translate("Base Game").fetch(); - gameTable.querySelector('th:nth-child(3)').textContent = l10n.translate("LOOT Folder").fetch(); - gameTable.querySelector('th:nth-child(4)').textContent = l10n.translate("Master File").fetch(); - gameTable.querySelector('th:nth-child(5)').textContent = l10n.translate("Masterlist Repository URL").fetch(); - gameTable.querySelector('th:nth-child(6)').textContent = l10n.translate("Masterlist Repository Branch").fetch(); - gameTable.querySelector('th:nth-child(7)').textContent = l10n.translate("Install Path").fetch(); - gameTable.querySelector('th:nth-child(8)').textContent = l10n.translate("Install Path Registry Key").fetch(); + firstRun.querySelector('li:nth-child(3)').textContent = l10n.translate('CRCs are only displayed after plugins have been loaded, either by conflict filtering, or by sorting.'); + firstRun.querySelector('li:nth-child(4)').textContent = l10n.translate('Double-click a plugin in the sidebar to quickly open its metadata editor. Multiple metadata editors can be opened at once.'); + firstRun.querySelector('li:nth-child(5)').textContent = l10n.translate('Plugins can be drag and dropped from the sidebar into editors\' "load after", "requirements" and "incompatibility" tables.'); + firstRun.querySelector('li:nth-child(6)').textContent = l10n.translate('Some features are disabled while there is an editor open, or while there is a sorted load order that has not been applied or discarded.'); + firstRun.querySelector('li:last-child').textContent = l10n.translate('Many interface elements have tooltips. If you don\'t know what something is, try hovering your mouse over it to find out. Otherwise, LOOT\'s documentation can be accessed through the main menu.'); - /* As the game table is attached on launch, its "Add New Row" - tooltip doesn't benefit from the template translation above. */ - gameTable.querySelector('tr:last-child core-tooltip').setAttribute('label', l10n.translate("Add New Row").fetch()); + firstRun.getElementsByTagName('paper-button')[0].textContent = l10n.translate('OK'); + } - document.getElementById('settingsDialog').getElementsByClassName('accept')[0].textContent = l10n.translate("Apply").fetch(); - document.getElementById('settingsDialog').getElementsByClassName('cancel')[0].textContent = l10n.translate("Cancel").fetch(); + return (l10n) => { + translatePluginCardTemplate(l10n); + translatePluginEditorTemplate(l10n); + translatePluginListItemTemplate(l10n); + translateMessageDialogTemplate(l10n); - /* First-run dialog */ - var firstRun = document.getElementById('firstRun'); - firstRun.heading = l10n.translate("First-Time Tips").fetch(); + translateFileRowTemplate(l10n); + translateMessageRowTemplate(l10n); + translateTagRowTemplate(l10n); + translateDirtyInfoRowTemplate(l10n); + translateLocationRowTemplate(l10n); + translateGameRowTemplate(l10n); + translateNewRowTemplate(l10n); - firstRun.querySelector('li:nth-child(3)').textContent = l10n.translate("CRCs are only displayed after plugins have been loaded, either by conflict filtering, or by sorting.").fetch(); - firstRun.querySelector('li:nth-child(4)').textContent = l10n.translate("Double-click a plugin in the sidebar to quickly open its metadata editor. Multiple metadata editors can be opened at once.").fetch(); - firstRun.querySelector('li:nth-child(5)').textContent = l10n.translate("Plugins can be drag and dropped from the sidebar into editors' \"load after\", \"requirements\" and \"incompatibility\" tables.").fetch(); - firstRun.querySelector('li:nth-child(6)').textContent = l10n.translate("Some features are disabled while there is an editor open, or while there is a sorted load order that has not been applied or discarded.").fetch(); - firstRun.querySelector('li:last-child').textContent = l10n.translate("Many interface elements have tooltips. If you don't know what something is, try hovering your mouse over it to find out. Otherwise, LOOT's documentation can be accessed through the main menu.").fetch(); + translateMainToolbar(l10n); + translateSidebar(l10n); - firstRun.getElementsByTagName('paper-button')[0].textContent = l10n.translate("OK").fetch(); - }, - - getJedInstance: function(locale) { - return this.loadLocaleData(locale).catch(function(error){ - console.log(error); - return defaultData; - }).then(function(result){ - return new jed({ - 'locale_data': result, - 'domain': 'messages' - }); - }); - } - - }; + translateSummaryCard(l10n); + translateSettingsDialog(l10n); + translateFirstRunDialog(l10n); + }; })); diff --git a/src/gui/html/js/loot.js b/src/gui/html/js/loot.js deleted file mode 100644 index 183ce78a..00000000 --- a/src/gui/html/js/loot.js +++ /dev/null @@ -1,309 +0,0 @@ -var loot = { - installedGames: [], - settings: {}, - game: { - folder: '', - globalMessages: [], - masterlist: {}, - plugins: [], - - /* Call whenever game is changed or game menu / game table are rewritten. */ - updateSelectedGame: function() { - document.getElementById('gameMenu').value = this.folder; - - /* Also disable deletion of the game's row in the settings dialog. */ - var table = document.getElementById('gameTable'); - for (var i = 0; i < table.tBodies[0].rows.length; ++i) { - if (table.tBodies[0].rows[i].getElementsByClassName('folder').length > 0) { - if (table.tBodies[0].rows[i].getElementsByClassName('folder')[0].value == this.folder) { - table.setReadOnly(table.tBodies[0].rows[i], ['delete']); - } else { - table.setReadOnly(table.tBodies[0].rows[i], ['delete'], 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.removeAttribute('disabled'); - } else { - redateButton.setAttribute('disabled', true); - } - }, - - 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').children; - for (var i = 0; i < gameMenuItems.length; ++i) { - if (this.installedGames.indexOf(gameMenuItems[i].getAttribute('value')) == -1) { - gameMenuItems[i].setAttribute('disabled', true); - gameMenuItems[i].removeEventListener('click', onChangeGame, false); - } else { - gameMenuItems[i].removeAttribute('disabled'); - gameMenuItems[i].addEventListener('click', onChangeGame, false); - } - } - }, - - /* Call whenever settings are changed. */ - updateSettingsUI: function() { - var gameSelect = document.getElementById('defaultGameSelect'); - var gameMenu = document.getElementById('gameMenu'); - 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', onChangeGame, false); - gameMenu.removeChild(gameMenu.firstElementChild); - } - gameTable.clear(); - - /* Now fill with new values. */ - this.settings.games.forEach(function(game){ - var menuItem = document.createElement('paper-item'); - menuItem.setAttribute('value', game.folder); - menuItem.setAttribute('noink', ''); - menuItem.textContent = game.name; - gameMenu.appendChild(menuItem); - gameSelect.appendChild(menuItem.cloneNode(true)); - - 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; - - 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 = l10n.jed.translate("N/A").fetch(); - } - if (change.object[change.name] && change.object[change.name].date) { - document.getElementById('masterlistDate').textContent = change.object[change.name].date; - } else { - document.getElementById('masterlistDate').textContent = l10n.jed.translate("N/A").fetch(); - } - } 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('summary').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)) - } - }); - }); - } -}; diff --git a/src/gui/html/js/plugin.js b/src/gui/html/js/plugin.js index 64202b6a..6e740999 100644 --- a/src/gui/html/js/plugin.js +++ b/src/gui/html/js/plugin.js @@ -22,193 +22,303 @@ . */ 'use strict'; -/* Plugin object for managing data and UI interaction. */ -function Plugin(obj) { - this.name = obj.name; - this.crc = obj.crc; - this.version = obj.version; - this.isActive = obj.isActive; - this.isEmpty = obj.isEmpty; - this.isMaster = obj.isMaster; - this.loadsArchive = obj.loadsArchive; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.Plugin = factory(); + } +}(this, () => { + /* Messages, tags, CRCs and version strings can all be hidden by filters. + Use getters with no setters for member variables as data should not be + written to objects of this class. */ + class PluginCardContent { + constructor(plugin, filters) { + this._name = plugin.name; + this._isActive = plugin.isActive || false; + this._isEmpty = plugin.isEmpty; + this._isMaster = plugin.isMaster; + this._loadsArchive = plugin.loadsArchive; - this.masterlist = obj.masterlist; - this.userlist = obj.userlist; + if (!filters.hideVersionNumbers) { + this._version = plugin.version; + } else { + this._version = ''; + } - this.modPriority = obj.modPriority; - this.isGlobalPriority = obj.isGlobalPriority; - this.messages = obj.messages; - this.tags = obj.tags; - this.isDirty = obj.isDirty; + if (!filters.hideCRCs) { + this._crc = plugin.crc; + } else { + this._crc = 0; + } - this.id = this.name.replace(/\s+/g, ''); - this.isMenuOpen = false; - this.isEditorOpen = false; - this.isConflictFilterChecked = false; - this.isSearchResult = false; - - /* Converts between the LOOT metadata object for tags, and their - editor row representation. */ - Plugin.prototype.convTagObj = function(tag) { - var newTag = { - condition: tag.condition - }; - if (tag.type) { - /* Input is row data. */ - if (tag.type == 'remove') { - newTag.name = '-' + tag.name; - } else { - newTag.name = tag.name; - } - } else { - /* Input is metadata object. */ - if (tag.name[0] == '-') { - newTag.type = 'remove'; - newTag.name = tag.name.substr(1); - } else { - newTag.type = 'add'; - newTag.name = tag.name; - } - } - return newTag; - } - - Plugin.prototype.getTagStrings = function() { - var tagsAdded = []; - var tagsRemoved = []; - - if (this.tags) { - for (var i = 0; i < this.tags.length; ++i) { - if (this.tags[i].name[0] == '-') { - tagsRemoved.push(this.tags[i].name.substr(1)); - } else { - tagsAdded.push(this.tags[i].name); - } - } - } - /* Now make sure that the same tag doesn't appear in both arrays. - Prefer the removed list. */ - for (var i = 0; i < tagsAdded.length; ++i) { - for (var j = 0; j < tagsRemoved.length; ++j) { - if (tagsRemoved[j].toLowerCase() == tagsAdded[i].toLowerCase()) { - /* Remove tag from the tagsAdded array. */ - tagsAdded.splice(i, 1); - --i; - } - } - } + if (!filters.hideBashTags) { + this._tags = plugin.tags; + } else { + this._tags = []; + } + this._messages = plugin.messages.map((message) => { return { - added: tagsAdded.join(', '), - removed: tagsRemoved.join(', ') + type: message.type, + content: message.content[0].str, }; + }).filter(filters.messageFilter, filters); } - Plugin.prototype.getPriorityString = function() { - if (this.modPriority != 0) { - return this.modPriority.toString(); - } else { - return ''; + get name() { + return this._name; + } + + get isActive() { + return this._isActive; + } + + get isEmpty() { + return this._isEmpty; + } + + get isMaster() { + return this._isMaster; + } + + get loadsArchive() { + return this._loadsArchive; + } + + get version() { + return this._version; + } + + get crc() { + if (this._crc === 0) { + return ''; + } + + /* Pad CRC string to 8 characters. */ + return ('00000000' + this._crc.toString(16).toUpperCase()).slice(-8); + } + + get tags() { + const tagsAdded = []; + const tagsRemoved = []; + + if (this._tags) { + for (let i = 0; i < this._tags.length; ++i) { + if (this._tags[i].name[0] === '-') { + tagsRemoved.push(this._tags[i].name.substr(1)); + } else { + tagsAdded.push(this._tags[i].name); + } } - } - - Plugin.prototype.getCrcString = function() { - if (this.crc == 0) { - return ''; - } else { - /* Pad CRC string to 8 characters. */ - return ('00000000' + this.crc.toString(16).toUpperCase()).slice(-8); + } + /* Now make sure that the same tag doesn't appear in both arrays. + Prefer the removed list. */ + for (let i = 0; i < tagsAdded.length; ++i) { + for (let j = 0; j < tagsRemoved.length; ++j) { + if (tagsRemoved[j].toLowerCase() === tagsAdded[i].toLowerCase()) { + /* Remove tag from the tagsAdded array. */ + tagsAdded.splice(i, 1); + --i; + } } + } + + return { + added: tagsAdded.join(', '), + removed: tagsRemoved.join(', '), + }; } - Plugin.prototype.getUIMessages = function() { - var uiMessages = []; - /* Now add the new messages. */ - if (this.messages && this.messages.length != 0) { - filters.applyMessageFilters(this.messages).forEach(function(message) { - var messageLi = document.createElement('li'); - messageLi.className = message.type; - // Use the Marked library for Markdown formatting support. - messageLi.innerHTML = marked(message.content[0].str); - uiMessages.push(messageLi); + get messages() { + return this._messages; + } - }); + containsText(text) { + if (text === undefined || text.length === 0) { + return true; + } + const needle = text.toLowerCase(); + + if (this.name.toLowerCase().indexOf(needle) !== -1 + || this.crc.toLowerCase().indexOf(needle) !== -1 + || this.version.toLowerCase().indexOf(needle) !== -1) { + return true; + } + + if (this.tags.added.toLowerCase().indexOf(needle) !== -1 + || this.tags.removed.toLowerCase().indexOf(needle) !== -1) { + return true; + } + + for (let i = 0; i < this.messages.length; ++i) { + if (this.messages[i].content.toLowerCase().indexOf(needle) !== -1) { + return true; } + } - return uiMessages; + return false; + } + } + + return class Plugin { + constructor(obj) { + /* Plugin data */ + this.name = obj.name; + this.crc = obj.crc || 0; + this.version = obj.version || ''; + this.isActive = obj.isActive || false; + this.isEmpty = obj.isEmpty || false; + this.isMaster = obj.isMaster || false; + this.loadsArchive = obj.loadsArchive || false; + + this.masterlist = obj.masterlist; + this.userlist = obj.userlist; + + this.priority = obj.priority || 0; + this.isPriorityGlobal = obj.isPriorityGlobal || false; + this._messages = obj.messages || []; + this.tags = obj.tags; + this._isDirty = obj.isDirty || false; + + /* UI state variables */ + this.id = this.name.replace(/\s+/g, ''); + this.isMenuOpen = false; + this.isEditorOpen = false; + this.isConflictFilterChecked = false; + this.isSearchResult = false; } - Plugin.prototype.observer = function(changes) { - changes.forEach(function(change) { - if (change.name == 'tags') { - change.object.computed.tags = change.object.getTagStrings(); - } else if (change.name == 'modPriority') { - change.object.computed.priority = change.object.getPriorityString(); - } else if (change.name == 'crc') { - change.object.computed.crc = change.object.getCrcString(); - } else if (change.name == 'messages') { - /* Update computed list items. */ - change.object.computed.messages = change.object.getUIMessages(); - - /* Update the message counts. */ - var oldTotal = 0; - var newTotal = 0; - var oldWarns = 0; - var newWarns = 0; - var oldErrs = 0; - var newErrs = 0; - - if (change.oldValue) { - oldTotal = change.oldValue.length; - - change.oldValue.forEach(function(message){ - if (message.type == 'warn') { - ++oldWarns; - } else if (message.type == 'error') { - ++oldErrs; - } - }); - } - if (change.object[change.name]) { - newTotal = change.object[change.name].length; - - change.object[change.name].forEach(function(message){ - if (message.type == 'warn') { - ++newWarns; - } else if (message.type == 'error') { - ++newErrs; - } - }); - } - - document.getElementById('filterTotalMessageNo').textContent = parseInt(document.getElementById('filterTotalMessageNo').textContent, 10) + newTotal - oldTotal; - document.getElementById('totalMessageNo').textContent = parseInt(document.getElementById('totalMessageNo').textContent, 10) + newTotal - oldTotal; - document.getElementById('totalWarningNo').textContent = parseInt(document.getElementById('totalWarningNo').textContent, 10) + newWarns - oldWarns; - document.getElementById('totalErrorNo').textContent = parseInt(document.getElementById('totalErrorNo').textContent, 10) + newErrs - oldErrs; - } else if (change.name == 'isDirty') { - /* Update dirty counts. */ - if (change.object[change.name]) { - document.getElementById('dirtyPluginNo').textContent = parseInt(document.getElementById('dirtyPluginNo').textContent, 10) + 1; - } else { - document.getElementById('dirtyPluginNo').textContent = parseInt(document.getElementById('dirtyPluginNo').textContent, 10) - 1; - } - } - }); + static fromJson(key, value) { + if (value !== null && value.__type === 'Plugin') { + return new Plugin(value); + } + return value; } - this.computed = { - tags: this.getTagStrings(), - priority: this.getPriorityString(), - crc: this.getCrcString(), - messages: this.getUIMessages(), - }; - Object.observe(this, this.observer); -} + static tagFromRowData(rowData) { + if (rowData.condition === undefined || rowData.name === undefined || rowData.type === undefined) { + throw new TypeError('Row data members are undefined'); + } + const tag = { + condition: rowData.condition, + name: '', + }; -function jsonToPlugin(key, value) { - if (value !== null && value.__type === 'Plugin') { - var p = new Plugin(value); - return p; + if (rowData.type === 'remove') { + tag.name = '-'; + } + tag.name += rowData.name; + + return tag; } - return value; -} + + static tagToRowData(tag) { + const rowData = { + condition: tag.condition, + }; + + if (tag.name[0] === '-') { + rowData.type = 'remove'; + rowData.name = tag.name.substr(1); + } else { + rowData.type = 'add'; + rowData.name = tag.name; + } + + return rowData; + } + + get priorityString() { + if (this.priority === 0) { + return ''; + } + + return this.priority.toString(); + } + + get messages() { + return this._messages; + } + + set messages(messages) { + /* Update the message counts. */ + let oldTotal = 0; + let newTotal = 0; + let oldWarns = 0; + let newWarns = 0; + let oldErrs = 0; + let newErrs = 0; + + oldTotal = this._messages.length; + + this._messages.forEach((message) => { + if (message.type === 'warn') { + ++oldWarns; + } else if (message.type === 'error') { + ++oldErrs; + } + }); + + newTotal = messages.length; + + messages.forEach((message) => { + if (message.type === 'warn') { + ++newWarns; + } else if (message.type === 'error') { + ++newErrs; + } + }); + + if (newTotal !== oldTotal || newWarns !== oldWarns || newErrs !== oldErrs) { + document.dispatchEvent(new CustomEvent('loot-plugin-message-change', { + detail: { + totalDiff: newTotal - oldTotal, + warningDiff: newWarns - oldWarns, + errorDiff: newErrs - oldErrs, + }, + })); + } + + this._messages = messages; + } + + get isDirty() { + return this._isDirty; + } + + set isDirty(dirty) { + /* Update dirty counts. */ + if (dirty !== this._isDirty) { + document.dispatchEvent(new CustomEvent('loot-plugin-isdirty-change', { + detail: { + isDirty: dirty, + }, + })); + } + + this._isDirty = dirty; + } + + getCardContent(filters) { + return new PluginCardContent(this, filters); + } + + static onMessageChange(evt) { + document.getElementById('filterTotalMessageNo').textContent = parseInt(document.getElementById('filterTotalMessageNo').textContent, 10) + evt.detail.totalDiff; + document.getElementById('totalMessageNo').textContent = parseInt(document.getElementById('totalMessageNo').textContent, 10) + evt.detail.totalDiff; + document.getElementById('totalWarningNo').textContent = parseInt(document.getElementById('totalWarningNo').textContent, 10) + evt.detail.warningDiff; + document.getElementById('totalErrorNo').textContent = parseInt(document.getElementById('totalErrorNo').textContent, 10) + evt.detail.errorDiff; + } + static onIsDirtyChange(evt) { + if (evt.detail.isDirty) { + document.getElementById('dirtyPluginNo').textContent = parseInt(document.getElementById('dirtyPluginNo').textContent, 10) + 1; + } else { + document.getElementById('dirtyPluginNo').textContent = parseInt(document.getElementById('dirtyPluginNo').textContent, 10) - 1; + } + } + }; +})); diff --git a/src/gui/html/js/query.js b/src/gui/html/js/query.js new file mode 100644 index 00000000..8c5c7696 --- /dev/null +++ b/src/gui/html/js/query.js @@ -0,0 +1,36 @@ +'use strict'; +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.query = factory(); + } +}(this, () => { + return (requestName, ...args) => { + if (!requestName) { + throw new Error('No request name passed'); + } + let request; + if (args.length === 0) { + request = requestName; + } else { + request = JSON.stringify({ + name: requestName, + args, + }); + } + return new Promise((resolve, reject) => { + window.cefQuery({ + request, + persistent: false, + onSuccess: resolve, + onFailure: (errorCode, errorMessage) => { + reject(new Error('Error code: ' + errorCode + '; ' + errorMessage)); + }, + }); + }); + }; +})); diff --git a/src/gui/html/js/translator.js b/src/gui/html/js/translator.js new file mode 100644 index 00000000..60720c77 --- /dev/null +++ b/src/gui/html/js/translator.js @@ -0,0 +1,77 @@ +'use strict'; + +(function exportModule(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['bower_components/Jed/jed', 'bower_components/jed-gettext-parser/jedGettextParser'], factory); + } else { + // Browser globals + root.loot = root.loot || {}; + root.loot.Translator = factory(root.Jed, root.jedGettextParser); + } +}(this, (Jed, jedGettextParser) => { + return class Translator { + /* Returns a Promise */ + constructor(locale) { + this.locale = locale || 'en'; + this.jed = undefined; + } + + load() { + const defaultTranslationData = { + 'messages': { + '': { + 'domain': 'messages', + 'lang': 'en', + 'plural_forms': 'nplurals=2; plural=(n != 1);', + }, + }, + }; + + let translationDataPromise; + if (this.locale === 'en') { + /* Just resolve to an empty data set. */ + translationDataPromise = Promise.resolve(defaultTranslationData); + } else { + translationDataPromise = new Promise((resolve, reject) => { + const url = 'loot://l10n/' + this.locale + '/LC_MESSAGES/loot.mo'; + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'arraybuffer'; + xhr.addEventListener('readystatechange', (evt) => { + if (evt.target.readyState === 4) { + /* Status is 0 for local file URL loading. */ + if (evt.target.status >= 200 && evt.target.status < 400) { + resolve(jedGettextParser.mo.parse(evt.target.response)); + } else { + reject(new Error(evt.target.statusText)); + } + } + }, false); + xhr.send(); + }); + } + + return translationDataPromise.catch((error) => { + console.log('Error loading translation data: ' + error.message); // eslint-disable-line no-console + return defaultTranslationData; + }).then((result) => { + this.jed = new Jed({ + 'locale_data': result, + 'domain': 'messages', + }); + }); + } + + translate(text, ...substitutions) { + if (text === undefined) { + return ''; + } + if (this.jed === undefined) { + return text; + } + const func = this.jed.translate(text); + return func.fetch.apply(func, substitutions); + } + }; +})); diff --git a/src/tests/gui/html/js/test.html b/src/tests/gui/html/js/test.html new file mode 100644 index 00000000..44de2886 --- /dev/null +++ b/src/tests/gui/html/js/test.html @@ -0,0 +1,48 @@ + +LOOT UI JS Tests + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/gui/html/js/test_filters.js b/src/tests/gui/html/js/test_filters.js new file mode 100644 index 00000000..01f72557 --- /dev/null +++ b/src/tests/gui/html/js/test_filters.js @@ -0,0 +1,206 @@ +'use strict'; + +describe('Filters', () => { + /* Mock the Translator class. */ + class Translator { + translate(text) { + return text; + } + } + + let l10n; + + beforeEach(() => { + l10n = new Translator(); + }); + + describe('#constructor()', () => { + it('should throw if no parameter is passed', () => { + (() => { new loot.Filters(); }).should.throw(); // eslint-disable-line no-new + }); + + it('should throw if an empty object is passed', () => { + (() => { new loot.Filters({}); }).should.throw(); // eslint-disable-line no-new + }); + + it('should not throw if a valid Translator object is passed', () => { + (() => { new loot.Filters(l10n); }).should.not.throw(); // eslint-disable-line no-new + }); + + it('should initialise filters as not enabled', () => { + const filters = new loot.Filters(l10n); + + filters.hideMessagelessPlugins.should.be.false(); + filters.hideInactivePlugins.should.be.false(); + filters.conflictingPluginNames.should.deepEqual([]); + filters.contentSearchString.should.equal(''); + + filters.hideVersionNumbers.should.be.false(); + filters.hideCRCs.should.be.false(); + filters.hideBashTags.should.be.false(); + filters.hideAllPluginMessages.should.be.false(); + filters.hideNotes.should.be.false(); + filters.hideDoNotCleanMessages.should.be.false(); + }); + + it('should initialise "do not clean" search string', () => { + const filters = new loot.Filters(l10n); + + filters._doNotCleanString.should.equal('do not clean'); + }); + }); + + describe('#pluginFilter()', () => { + let filters; + let plugin; + + /* Mock the PluginCardContent class */ + class PluginCardContent { + constructor(pluginObj, filtersObj) { + this._messages = pluginObj.messages; + this._hideMessages = filtersObj.hideAllPluginMessages; + } + + get messages() { + if (this._hideMessages) { + return []; + } + return this._messages; + } + + containsText(text) { + return text === 'found text'; + } + } + + /* Mock the Plugin class */ + class Plugin { + constructor() { + this.name = 'test'; + this.isActive = false; + this.messages = []; + } + + getCardContent(filtersObj) { + return new PluginCardContent(this, filtersObj); + } + } + + beforeEach(() => { + filters = new loot.Filters(l10n); + plugin = new Plugin(); + }); + + it('should return true if no filters are enabled', () => { + filters.pluginFilter(plugin).should.be.true(); + }); + + it('should return false if inactive plugins filter is enabled', () => { + filters.hideInactivePlugins = true; + filters.pluginFilter(plugin).should.be.false(); + }); + + it('should return true if inactive plugins filter is enabled and plugin is active', () => { + filters.hideInactivePlugins = true; + plugin.isActive = true; + filters.pluginFilter(plugin).should.be.true(); + }); + + it('should return false if messageless plugins filter is enabled', () => { + filters.hideMessagelessPlugins = true; + filters.pluginFilter(plugin).should.be.false(); + }); + + it('should return true if messageless plugins filter is enabled and plugin has a non-zero message array', () => { + filters.hideMessagelessPlugins = true; + plugin.messages = [0]; + filters.pluginFilter(plugin).should.be.true(); + }); + + it('should return false if all plugin messages and messageless plugin filters are enabled and plugin has a non-zero message array', () => { + filters.hideAllPluginMessages = true; + filters.hideMessagelessPlugins = true; + plugin.messages = [0]; + filters.pluginFilter(plugin).should.be.false(); + }); + + it('should return false if conflicting plugins filter is enabled', () => { + filters.conflictingPluginNames = ['conflicting plugin']; + filters.pluginFilter(plugin).should.be.false(); + }); + + it('should return true if conflicting plugins filter is enabled and plugin name is in the conflicting plugins array', () => { + filters.conflictingPluginNames = [ + 'conflicting plugin', + plugin.name, + ]; + filters.pluginFilter(plugin).should.be.true(); + }); + + it('should return false if plugin content filter is enabled', () => { + filters.contentSearchString = 'unfound text'; + filters.pluginFilter(plugin).should.be.false(); + }); + + it('should return true if plugin content filter is enabled and plugin contains the filter text', () => { + filters.contentSearchString = 'found text'; + filters.pluginFilter(plugin).should.be.true(); + }); + }); + + describe('#messageFilter()', () => { + let filters; + let note; + let doNotCleanMessage; + + beforeEach(() => { + filters = new loot.Filters(l10n); + note = { + type: 'say', + content: 'test message', + }; + doNotCleanMessage = { + type: 'warn', + content: 'do not clean', + }; + }); + + it('should return true for a note message when no filters are enabled', () => { + filters.messageFilter(note).should.be.true(); + }); + + it('should return true for a warning "do not clean" message when no filters are enabled', () => { + filters.messageFilter(doNotCleanMessage).should.be.true(); + }); + + it('should return false for a note message when the notes filter is enabled', () => { + filters.hideNotes = true; + filters.messageFilter(note).should.be.false(); + }); + + it('should return true for a warning message when the notes filter is enabled', () => { + filters.hideNotes = true; + filters.messageFilter(doNotCleanMessage).should.be.true(); + }); + + it('should return false for a "do not clean" message when the "do not clean" messages filter is enabled', () => { + filters.hideDoNotCleanMessages = true; + filters.messageFilter(doNotCleanMessage).should.be.false(); + }); + + it('should return true for a message not containing "do not clean" when the "do not clean" messages filter is enabled', () => { + filters.hideDoNotCleanMessages = true; + filters.messageFilter(note).should.be.true(); + }); + + it('should return false for a note message when the all messages filter is enabled', () => { + filters.hideAllPluginMessages = true; + filters.messageFilter(note).should.be.false(); + }); + + it('should return false for a "do not clean" message when the all messages filter is enabled', () => { + filters.hideAllPluginMessages = true; + filters.messageFilter(doNotCleanMessage).should.be.false(); + }); + }); +}); diff --git a/src/tests/gui/html/js/test_game.js b/src/tests/gui/html/js/test_game.js new file mode 100644 index 00000000..18de1394 --- /dev/null +++ b/src/tests/gui/html/js/test_game.js @@ -0,0 +1,361 @@ +'use strict'; + +describe('Game', () => { + /* Mock the Translator class. */ + class Translator { + translate(text) { + return text; + } + } + let l10n; + + beforeEach(() => { + l10n = new Translator(); + }); + + describe('#constructor()', () => { + it('should throw if passed no paramters', () => { + (() => { new loot.Game(); }).should.throw(); // eslint-disable-line no-new + }); + + it('should throw if passed no l10n parameter', () => { + (() => { new loot.Game({}); }).should.throw(); // eslint-disable-line no-new + }); + + it('should not throw if passed an empty object as the first parameter', () => { + (() => { new loot.Game({}, l10n); }).should.not.throw(); // eslint-disable-line no-new + }); + + it('should set folder to a blank string by default', () => { + const game = new loot.Game({}, l10n); + game.folder.should.equal(''); + }); + + it('should set folder to the object\'s value if defined', () => { + const game = new loot.Game({ folder: 'test' }, l10n); + game.folder.should.equal('test'); + }); + + it('should set globalMessages to an empty array by default', () => { + const game = new loot.Game({}, l10n); + game.globalMessages.should.deepEqual([]); + }); + + it('should set globalMessages to the object\'s value if defined', () => { + const game = new loot.Game({ globalMessages: ['test'] }, l10n); + game.globalMessages.should.deepEqual(['test']); + }); + + it('should set masterlist to an empty object by default', () => { + const game = new loot.Game({}, l10n); + game.masterlist.should.deepEqual({}); + }); + + it('should set masterlist to the object\'s value if defined', () => { + const game = new loot.Game({ masterlist: { revision: 0 } }, l10n); + game.masterlist.should.deepEqual({ revision: 0 }); + }); + + it('should set plugins to an empty array by default', () => { + const game = new loot.Game({}, l10n); + game.plugins.should.deepEqual([]); + }); + + it('should set plugins to the object\'s value if defined', () => { + const game = new loot.Game({ plugins: ['test'] }, l10n); + game.plugins.should.deepEqual(['test']); + }); + + it('should set loadOrder to undefined by default', () => { + const game = new loot.Game({}, l10n); + should(game.loadOrder).be.undefined(); + }); + + it('should set loadOrder to undefined even if the object\'s value if defined', () => { + const game = new loot.Game({ loadOrder: ['test'] }, l10n); + should(game.loadOrder).be.undefined(); + }); + + it('should set oldLoadOrder to undefined by default', () => { + const game = new loot.Game({}, l10n); + should(game.oldLoadOrder).be.undefined(); + }); + + it('should set oldLoadOrder to undefined even if the object\'s value if defined', () => { + const game = new loot.Game({ oldLoadOrder: ['test'] }, l10n); + should(game.oldLoadOrder).be.undefined(); + }); + + it('should initialise _notApplicableString', () => { + const game = new loot.Game({ oldLoadOrder: ['test'] }, l10n); + game._notApplicableString.should.equal('N/A'); + }); + }); + + describe('#folder', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + }); + + afterEach(() => { + document.removeEventListener('loot-game-folder-change', handleEvent); + }); + + it('setting value should not dispatch an event if the new value is equal to the old one', (done) => { + handleEvent = () => { + done(new Error('Should not have fired an event')); + }; + document.addEventListener('loot-game-folder-change', handleEvent); + + game.folder = game.folder; + setTimeout(done, 100); + }); + + it('setting value should dispatch an event if the new value differs from the old one', (done) => { + handleEvent = (evt) => { + evt.detail.folder.should.equal('test'); + done(); + }; + document.addEventListener('loot-game-folder-change', handleEvent); + + game.folder = 'test'; + }); + }); + + describe('#globalMessages', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + }); + + afterEach(() => { + document.removeEventListener('loot-game-global-messages-change', handleEvent); + }); + + it('setting value should not dispatch an event if the new value is equal to the old one', (done) => { + handleEvent = () => { + done(new Error('Should not have fired an event')); + }; + document.addEventListener('loot-game-global-messages-change', handleEvent); + + game.globalMessages = game.globalMessages; + setTimeout(done, 100); + }); + + it('setting value should dispatch an event if the new value differs from the old one', (done) => { + const newMessages = [ + { type: 'warn' }, + { type: 'error' }, + ]; + handleEvent = (evt) => { + evt.detail.messages.should.deepEqual(newMessages); + evt.detail.totalDiff.should.equal(2); + evt.detail.errorDiff.should.equal(1); + evt.detail.warningDiff.should.equal(1); + done(); + }; + document.addEventListener('loot-game-global-messages-change', handleEvent); + + game.globalMessages = newMessages; + }); + }); + + describe('#masterlist', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + }); + + afterEach(() => { + document.removeEventListener('loot-game-masterlist-change', handleEvent); + }); + + it('setting value should not dispatch an event if the new value is equal to the old one', (done) => { + handleEvent = () => { + done(new Error('Should not have fired an event')); + }; + document.addEventListener('loot-game-masterlist-change', handleEvent); + + game.masterlist = game.masterlist; + setTimeout(done, 100); + }); + + it('setting value should dispatch an event if the new value differs from the old one', (done) => { + const newMasterlist = { + revision: 'foo', + date: 'bar', + }; + handleEvent = (evt) => { + evt.detail.should.deepEqual(newMasterlist); + done(); + }; + document.addEventListener('loot-game-masterlist-change', handleEvent); + + game.masterlist = newMasterlist; + }); + + it('setting value to undefined should dispatch an event with the details being the not applicable string', (done) => { + handleEvent = (evt) => { + evt.detail.revision.should.equal(game._notApplicableString); + evt.detail.date.should.equal(game._notApplicableString); + done(); + }; + document.addEventListener('loot-game-masterlist-change', handleEvent); + + game.masterlist = undefined; + }); + }); + + describe('#plugins', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + }); + + afterEach(() => { + document.removeEventListener('loot-game-plugins-change', handleEvent); + }); + + it('setting value should dispatch an event if the new value is equal to the old one', (done) => { + handleEvent = () => { + done(); + }; + document.addEventListener('loot-game-plugins-change', handleEvent); + + game.plugins = game.plugins; + }); + + it('setting value should dispatch an event if the new value differs from the old one', (done) => { + const newPlugins = [ + { + isActive: true, + messages: [{ type: 'warn' }], + }, + { + isDirty: true, + messages: [{ type: 'say' }], + }, + ]; + handleEvent = (evt) => { + evt.detail.valuesAreTotals.should.be.true(); + evt.detail.totalMessageNo.should.equal(4); + evt.detail.warnMessageNo.should.equal(2); + evt.detail.errorMessageNo.should.equal(1); + evt.detail.totalPluginNo.should.equal(2); + evt.detail.activePluginNo.should.equal(1); + evt.detail.dirtyPluginNo.should.equal(1); + done(); + }; + document.addEventListener('loot-game-plugins-change', handleEvent); + + /* Set global messages to check they are also counted in the totals. */ + game.globalMessages = [ + { type: 'warn' }, + { type: 'error' }, + ]; + + game.plugins = newPlugins; + }); + }); + + describe('#appendPlugin()', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + }); + + afterEach(() => { + document.removeEventListener('loot-game-plugins-change', handleEvent); + }); + + it('should append the passed plugin to the plugins array', () => { + const newPlugin = { + isActive: true, + messages: [{ type: 'warn' }], + }; + game.appendPlugin(newPlugin); + + game.plugins[0].should.deepEqual(newPlugin); + }); + + it('should dispatch an event with the correct counter differences', (done) => { + handleEvent = (evt) => { + evt.detail.valuesAreTotals.should.be.false(); + evt.detail.totalMessageNo.should.equal(1); + evt.detail.warnMessageNo.should.equal(1); + evt.detail.errorMessageNo.should.equal(0); + evt.detail.totalPluginNo.should.equal(1); + evt.detail.activePluginNo.should.equal(1); + evt.detail.dirtyPluginNo.should.equal(0); + done(); + }; + document.addEventListener('loot-game-plugins-change', handleEvent); + + /* Set global messages to check they are also counted in the totals. */ + game.globalMessages = [ + { type: 'warn' }, + { type: 'error' }, + ]; + + game.appendPlugin({ + isActive: true, + messages: [{ type: 'warn' }], + }); + }); + }); + + describe('#removePluginAtIndex()', () => { + let game; + let handleEvent; + + beforeEach(() => { + game = new loot.Game({}, l10n); + game._plugins = [{ + isActive: true, + messages: [{ type: 'warn' }], + }]; + }); + + afterEach(() => { + document.removeEventListener('loot-game-plugins-change', handleEvent); + }); + + it('should remove the passed plugin to the plugins array', () => { + game.removePluginAtIndex(0); + game.plugins.length.should.equal(0); + }); + + it('should dispatch an event with the correct counter differences', (done) => { + handleEvent = (evt) => { + evt.detail.valuesAreTotals.should.be.false(); + evt.detail.totalMessageNo.should.equal(-1); + evt.detail.warnMessageNo.should.equal(-1); + evt.detail.errorMessageNo.should.equal(0); + evt.detail.totalPluginNo.should.equal(-1); + evt.detail.activePluginNo.should.equal(-1); + evt.detail.dirtyPluginNo.should.equal(0); + done(); + }; + document.addEventListener('loot-game-plugins-change', handleEvent); + + /* Set global messages to check they are also counted in the totals. */ + game.globalMessages = [ + { type: 'warn' }, + { type: 'error' }, + ]; + + game.removePluginAtIndex(0); + }); + }); +}); diff --git a/src/tests/gui/html/js/test_plugin.js b/src/tests/gui/html/js/test_plugin.js new file mode 100644 index 00000000..928d391e --- /dev/null +++ b/src/tests/gui/html/js/test_plugin.js @@ -0,0 +1,789 @@ +'use strict'; + +/* Mock the filters class. */ +class Filters { + constructor() { + this.hideVersionNumbers = false; + this.hideCRCs = false; + this.hideBashTags = false; + this.hideAllPluginMessages = false; + this.hideNotes = false; + this.hideDoNotCleanMessages = false; + } + + messageFilter(message, index) { + if (this.hideAllPluginMessages) { + return false; + } + + if (this.hideNotes && index === 0) { + return false; + } + + if (this.hideDoNotCleanMessages && index === 1) { + return false; + } + + return true; + } +} + +describe('Plugin', () => { + describe('#Plugin()', () => { + it('should throw if nothing is passed', () => { + (() => { new loot.Plugin(); }).should.throw(); // eslint-disable-line no-new + }); + + it('should throw if an object with no name key is passed', () => { + (() => { new loot.Plugin({}); }).should.throw(); // eslint-disable-line no-new + }); + + it('should not throw if some members are undefined', () => { + (() => { new loot.Plugin({ name: 'test' }); }).should.not.throw(); // eslint-disable-line no-new + }); + + it('should set name to passed key\'s value', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.name.should.equal('test'); + }); + + it('should set crc to zero if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.crc.should.equal(0); + }); + + it('should set crc to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + crc: 0xDEADBEEF, + }); + + plugin.crc.should.equal(0xDEADBEEF); + }); + + it('should set version to an empty string if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.version.should.equal(''); + }); + + it('should set version to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + version: 'foo', + }); + + plugin.version.should.equal('foo'); + }); + + it('should set isActive value to false if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isActive.should.be.false(); + }); + + it('should set isActive to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + isActive: true, + }); + + plugin.isActive.should.be.true(); + }); + + it('should set isEmpty value to false if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isEmpty.should.be.false(); + }); + + it('should set isEmpty to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + isEmpty: true, + }); + + plugin.isEmpty.should.be.true(); + }); + + it('should set isMaster value to false if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isMaster.should.be.false(); + }); + + it('should set isMaster to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + isMaster: true, + }); + + plugin.isMaster.should.be.true(); + }); + + it('should set loadsArchive value to false if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.loadsArchive.should.be.false(); + }); + + it('should set loadsArchive to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + loadsArchive: true, + }); + + plugin.loadsArchive.should.be.true(); + }); + + it('should set masterlist value to undefined if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + should(plugin.masterlist).be.undefined(); + }); + + it('should set masterlist to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + masterlist: {}, + }); + + plugin.masterlist.should.be.deepEqual({}); + }); + + it('should set userlist value to undefined if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + should(plugin.userlist).be.undefined(); + }); + + it('should set userlist to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + userlist: {}, + }); + + plugin.userlist.should.be.deepEqual({}); + }); + + it('should set isPriorityGlobal value to false if no key was passed', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isPriorityGlobal.should.be.false(); + }); + + it('should set isPriorityGlobal to passed key\'s value', () => { + const plugin = new loot.Plugin({ + name: 'test', + isEmpty: true, + }); + + plugin.isEmpty.should.be.true(); + }); + + it('should set id to the plugins name without spaces', () => { + const plugin = new loot.Plugin({ name: 'test plugin name' }); + + plugin.id.should.equal('testpluginname'); + }); + + it('should set isMenuOpen to false', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isMenuOpen.should.be.false(); + }); + + it('should set isEditorOpen to false', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isEditorOpen.should.be.false(); + }); + + it('should set isConflictFilterChecked to false', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isConflictFilterChecked.should.be.false(); + }); + + it('should set isSearchResult to false', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isSearchResult.should.be.false(); + }); + }); + + describe('#fromJson()', () => { + it('should return the value object if the JSON is not of the Plugin type', () => { + const testInputObj = { + name: 'test', + crc: 0xDEADBEEF, + }; + const testInputJson = JSON.stringify(testInputObj); + + JSON.parse(testInputJson, loot.Plugin.fromJson).should.deepEqual(testInputObj); + }); + + it('should return a Plugin object if the JSON is of the Plugin type', () => { + const testInputObj = { + name: 'test', + crc: 0xDEADBEEF, + __type: 'Plugin', + }; + const testInputJson = JSON.stringify(testInputObj); + + JSON.parse(testInputJson, loot.Plugin.fromJson).should.be.instanceof(loot.Plugin); + }); + }); + + describe('#tagFromRowData()', () => { + it('should throw if passed nothing', () => { + (() => { loot.Plugin.tagFromRowData(); }).should.throw(); + }); + + it('should return an empty object if passed nothing', () => { + (() => { loot.Plugin.tagFromRowData({}); }).should.throw(); + }); + + it('should return a raw metadata object if passed a row data object that removes a tag', () => { + loot.Plugin.tagFromRowData({ + condition: 'foo', + type: 'remove', + name: 'bar', + }).should.deepEqual({ + condition: 'foo', + name: '-bar', + }); + }); + + it('should return a raw metadata object if passed a row data object that adds a tag', () => { + loot.Plugin.tagFromRowData({ + condition: 'foo', + type: 'add', + name: 'bar', + }).should.deepEqual({ + condition: 'foo', + name: 'bar', + }); + }); + }); + + describe('#tagToRowData()', () => { + it('should throw if passed nothing', () => { + (() => { loot.Plugin.tagToRowData(); }).should.throw(); + }); + + it('should return an empty object if passed nothing', () => { + (() => { loot.Plugin.tagToRowData({}); }).should.throw(); + }); + + it('should return a row data object if passed a raw metadata object that removes a tag', () => { + loot.Plugin.tagToRowData({ + condition: 'foo', + name: '-bar', + }).should.deepEqual({ + condition: 'foo', + type: 'remove', + name: 'bar', + }); + }); + + it('should return a row data object if passed a raw metadata object that adds a tag', () => { + loot.Plugin.tagToRowData({ + condition: 'foo', + name: 'bar', + }).should.deepEqual({ + condition: 'foo', + type: 'add', + name: 'bar', + }); + }); + }); + + describe('#priorityString', () => { + it('should return an empty string if priority is undefined', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.priorityString.should.equal(''); + }); + + it('should return an empty string if priority is zero', () => { + const plugin = new loot.Plugin({ + name: 'test', + priority: 0, + }); + + plugin.priorityString.should.equal(''); + }); + + it('should return priority value as string if non zero', () => { + const plugin = new loot.Plugin({ + name: 'test', + priority: -50, + }); + + plugin.priorityString.should.equal('-50'); + }); + }); + + describe('#messages', () => { + let handleEvent; + + afterEach(() => { + document.removeEventListener('loot-plugin-message-change', handleEvent); + }); + + it('getting messages if they are undefined should return undefined', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.should.not.have.ownProperty('messages'); + }); + + it('getting messages if the array is empty should return an empty array', () => { + const plugin = new loot.Plugin({ + name: 'test', + messages: [], + }); + + plugin.messages.should.be.Array(); + plugin.messages.should.be.empty(); + }); + + it('getting messages should return any that are set', () => { + const messages = [{ + type: 'say', + content: 'test message', + }]; + const plugin = new loot.Plugin({ + name: 'test', + messages: messages, + }); + + plugin.messages.should.be.deepEqual(messages); + }); + + it('setting messages should store any set', () => { + const plugin = new loot.Plugin({ + name: 'test', + messages: [], + }); + const messages = [{ + type: 'say', + content: 'test message', + }]; + + plugin.messages = messages; + + plugin.messages.should.be.deepEqual(messages); + }); + + it('setting messages should not fire an event if no message counts were changed', (done) => { + const plugin = new loot.Plugin({ + name: 'test', + messages: [{ + type: 'say', + content: 'test message', + }], + }); + const messages = [{ + type: 'say', + content: 'another test message', + }]; + + handleEvent = () => { + done(new Error('Should not have fired an event')); + }; + + document.addEventListener('loot-plugin-message-change', handleEvent); + + plugin.messages = messages; + + setTimeout(done, 100); + }); + + it('setting messages should fire an event if message counts were changed', (done) => { + const plugin = new loot.Plugin({ + name: 'test', + messages: [], + }); + const messages = [{ + type: 'error', + content: 'test message', + }]; + + handleEvent = (evt) => { + evt.detail.totalDiff.should.equal(1); + evt.detail.warningDiff.should.equal(0); + evt.detail.errorDiff.should.equal(1); + done(); + }; + + document.addEventListener('loot-plugin-message-change', handleEvent); + + plugin.messages = messages; + }); + }); + + describe('#isDirty', () => { + let handleEvent; + + afterEach(() => { + document.removeEventListener('loot-plugin-isdirty-change', handleEvent); + }); + + it('getting value should return false if isDirty has not been set in the constructor', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isDirty.should.be.false(); + }); + + it('getting value should return true if isDirty is set to true in the constructor', () => { + const plugin = new loot.Plugin({ + name: 'test', + isDirty: true, + }); + + plugin.isDirty.should.be.true(); + }); + + it('setting value should store set value', () => { + const plugin = new loot.Plugin({ name: 'test' }); + + plugin.isDirty = true; + + plugin.isDirty.should.be.true(); + }); + + it('setting value to the current value should not fire an event', (done) => { + const plugin = new loot.Plugin({ name: 'test' }); + + handleEvent = () => { + done(new Error('Should not have fired an event')); + }; + + document.addEventListener('loot-plugin-isdirty-change', handleEvent); + + plugin.isDirty = plugin.isDirty; + + setTimeout(done, 100); + }); + + it('setting value not equal to the current value should fire an event', (done) => { + const plugin = new loot.Plugin({ name: 'test' }); + + handleEvent = (evt) => { + evt.detail.isDirty.should.be.true(); + done(); + }; + + document.addEventListener('loot-plugin-isdirty-change', handleEvent); + + plugin.isDirty = !plugin.isDirty; + }); + }); + + describe('#getCardContent()', () => { + let plugin; + beforeEach(() => { + plugin = new loot.Plugin({ name: 'test' }); + }); + + it('should throw if no argument was passed', () => { + (() => { plugin.getCardContent(); }).should.throw(); + }); + + it('should throw with an empty object', () => { + plugin.messages = [{ + type: 'say', + content: 'test message', + }]; + + (() => { plugin.getCardContent({}); }).should.throw(); + }); + + it('should succeed if passed a filters object', () => { + (() => { plugin.getCardContent(new Filters()); }).should.not.throw(); + }); + }); +}); + +describe('PluginCardContent', () => { + let plugin; + let filters; + + beforeEach(() => { + plugin = new loot.Plugin({ + name: 'test', + version: 'foo', + messages: [ + { + type: 'say', + content: [{ + str: 'test message', + lang: 'en', + }], + }, + { + type: 'warn', + content: [{ + str: 'do not clean', + lang: 'en', + }], + }, + ], + }); + + filters = new Filters(); + }); + + describe('#name', () => { + it('getting value should return plugin\'s value', () => { + plugin.getCardContent(filters).name.should.equal(plugin.name); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).name = ''; }).should.throw(); + }); + }); + + describe('#isActive', () => { + it('getting value should return plugin\'s value', () => { + plugin.getCardContent(filters).isActive.should.equal(plugin.isActive); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).isActive = true; }).should.throw(); + }); + }); + + describe('#isEmpty', () => { + it('getting value should return plugin\'s value', () => { + plugin.getCardContent(filters).isEmpty.should.equal(plugin.isEmpty); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).isEmpty = true; }).should.throw(); + }); + }); + + describe('#isMaster', () => { + it('getting value should return plugin\'s value', () => { + plugin.getCardContent(filters).isMaster.should.equal(plugin.isMaster); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).isMaster = true; }).should.throw(); + }); + }); + + describe('#loadsArchive', () => { + it('getting value should return plugin\'s value', () => { + plugin.getCardContent(filters).loadsArchive.should.equal(plugin.loadsArchive); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).loadsArchive = true; }).should.throw(); + }); + }); + + describe('#version', () => { + it('getting value should return plugin\'s value if the version filter is not enabled', () => { + plugin.getCardContent(filters).version.should.equal(plugin.version); + }); + + it('getting value should return empty string if the version filter is enabled', () => { + filters.hideVersionNumbers = true; + plugin.getCardContent(filters).version.should.equal(''); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).version = ''; }).should.throw(); + }); + }); + + describe('#tags', () => { + it('should return an object containing empty strings if no tags are set', () => { + plugin.getCardContent(filters).tags.should.deepEqual({ + added: '', + removed: '', + }); + }); + + it('should return an object containing strings of comma-separated tag names if tags are set', () => { + plugin.tags = [ + { name: 'Relev' }, + { name: 'Delev' }, + { name: 'Names' }, + { name: '-C.Climate' }, + { name: '-Actor.ABCS' }, + ]; + + plugin.getCardContent(filters).tags.should.deepEqual({ + added: 'Relev, Delev, Names', + removed: 'C.Climate, Actor.ABCS', + }); + }); + + it('should return an object containing empty strings if tags are set and the tags filter is enabled', () => { + plugin.tags = [ + { name: 'Relev' }, + { name: 'Delev' }, + { name: 'Names' }, + { name: '-C.Climate' }, + { name: '-Actor.ABCS' }, + ]; + filters.hideBashTags = true; + + plugin.getCardContent(filters).tags.should.deepEqual({ + added: '', + removed: '', + }); + }); + + it('should output a tag in the removed string if it appears as both added and removed', () => { + plugin.tags = [ + { name: 'Relev' }, + { name: '-Relev' }, + ]; + + plugin.getCardContent(filters).tags.should.deepEqual({ + added: '', + removed: 'Relev', + }); + }); + + it('setting value should throw', () => { + (() => { + plugin.getCardContent(filters).tags = { + added: 'Relev', + removed: 'Delev', + }; + }).should.throw(); + }); + }); + + describe('#crc', () => { + it('should return an empty string if crc is undefined', () => { + plugin.getCardContent(filters).crc.should.equal(''); + }); + + it('should return an empty string if crc is zero', () => { + plugin.crc = 0; + + plugin.getCardContent(filters).crc.should.equal(''); + }); + + it('should return crc value as string if non zero', () => { + plugin.crc = 0xDEADBEEF; + + plugin.getCardContent(filters).crc.should.equal('DEADBEEF'); + }); + + it('should return an empty string if crc is non-zero and the CRC filter is enabled', () => { + plugin.crc = 0xDEADBEEF; + filters.hideCRCs = true; + + plugin.getCardContent(filters).crc.should.equal(''); + }); + + it('should pad crc value to eight digits', () => { + plugin.crc = 0xBEEF; + + plugin.getCardContent(filters).crc.should.equal('0000BEEF'); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).crc = 0xBECADECA; }).should.throw(); + }); + }); + + describe('#messages', () => { + it('should return message objects mapped from the plugin\'s message objects', () => { + plugin.getCardContent(filters).messages.should.deepEqual([ + { + type: plugin.messages[0].type, + content: plugin.messages[0].content[0].str, + }, + { + type: plugin.messages[1].type, + content: plugin.messages[1].content[0].str, + }, + ]); + }); + + it('should return an array missing the note message when the notes filter is enabled', () => { + filters.hideNotes = true; + plugin.getCardContent(filters).messages.should.deepEqual([{ + type: plugin.messages[1].type, + content: plugin.messages[1].content[0].str, + }]); + }); + + it('should return an array missing the "do not clean" message when the "do not clean" messages filter is enabled', () => { + filters.hideDoNotCleanMessages = true; + plugin.getCardContent(filters).messages.should.deepEqual([{ + type: plugin.messages[0].type, + content: plugin.messages[0].content[0].str, + }]); + }); + + it('should return an empty array when the all messages filter is enabled', () => { + filters.hideAllPluginMessages = true; + plugin.getCardContent(filters).messages.should.deepEqual([]); + }); + + it('setting value should throw', () => { + (() => { plugin.getCardContent(filters).messages = []; }).should.throw(); + }); + }); + + describe('#containsText()', () => { + it('should return true if argument is undefined', () => { + plugin.getCardContent(filters).containsText().should.be.true(); + }); + + it('should return true if argument is an empty string', () => { + plugin.getCardContent(filters).containsText('').should.be.true(); + }); + + it('should search name case-insensitively', () => { + plugin.getCardContent(filters).containsText('Tes').should.be.true(); + }); + + it('should search CRC case-insensitively', () => { + plugin.crc = 0xDEADBEEF; + plugin.getCardContent(filters).containsText('dead').should.be.true(); + }); + + it('should search added tags case-insensitively', () => { + plugin.tags = [ + { name: 'Relev' }, + { name: 'Delev' }, + { name: 'Names' }, + { name: '-C.Climate' }, + { name: '-Actor.ABCS' }, + ]; + plugin.getCardContent(filters).containsText('climate').should.be.true(); + }); + + it('should search removed tags case-insensitively', () => { + plugin.tags = [ + { name: 'Relev' }, + { name: 'Delev' }, + { name: 'Names' }, + { name: '-C.Climate' }, + { name: '-Actor.ABCS' }, + ]; + plugin.getCardContent(filters).containsText('.abc').should.be.true(); + }); + + it('should search message content case-insensitively', () => { + plugin.getCardContent(filters).containsText('Clean').should.be.true(); + }); + + it('should not find text that is not present', () => { + plugin.getCardContent(filters).containsText('say').should.be.false(); + }); + }); +}); diff --git a/src/tests/gui/html/js/test_query.js b/src/tests/gui/html/js/test_query.js new file mode 100644 index 00000000..7c4d0193 --- /dev/null +++ b/src/tests/gui/html/js/test_query.js @@ -0,0 +1,39 @@ +'use strict'; + +/* Mock the window.cefQuery method */ +window.cefQuery = (obj) => { + if (obj.request === 'fail') { + obj.onFailure(-1, obj.request); + } else { + obj.onSuccess(obj.request); + } +}; + +describe('query()', () => { + it('should throw if no arguments are passed', () => { + (() => { loot.query(); }).should.throw(); + }); + + it('should return a promise', () => { + loot.query('test').should.be.a.Promise(); // eslint-disable-line new-cap + }); + + it('should succeed if a request name is passed', () => { + return loot.query('test').should.be.fulfilledWith('test'); + }); + + it('should succeed if a request name and arguments are passed', () => { + return loot.query('test', 1, false, ['a']).should.be.fulfilledWith(JSON.stringify({ + name: 'test', + args: [ + 1, + false, + ['a'], + ], + })); + }); + + it('should fail with an Error object when an error occurs', () => { + return loot.query('fail').should.be.rejectedWith(Error, { message: 'Error code: -1; fail' }); + }); +}); diff --git a/src/tests/gui/html/js/test_translator.js b/src/tests/gui/html/js/test_translator.js new file mode 100644 index 00000000..461b8694 --- /dev/null +++ b/src/tests/gui/html/js/test_translator.js @@ -0,0 +1,96 @@ +'use strict'; + +describe('Translator', () => { + describe('#Tranlsator()', () => { + it('should set locale to "en" if no locale is given', () => { + const l10n = new loot.Translator(); + l10n.locale.should.equal('en'); + }); + + it('should set the locale to the given value', () => { + const l10n = new loot.Translator('de'); + l10n.locale.should.equal('de'); + }); + }); + + describe('#load()', () => { + it('should return a Promise', () => { + const l10n = new loot.Translator(); + + l10n.load().should.be.a.Promise(); // eslint-disable-line new-cap + }); + + it('should be fulfilled for a locale of "en"', () => { + const l10n = new loot.Translator('en'); + + return l10n.load().should.be.fulfilled(); + }); + + /* Cannot test rejection or other locales as the URL uses is invalid in the + browser, causing a CORS error that cannot be handled in JavaScript. */ + }); + + describe('#translate()', () => { + let l10n; + + beforeEach(() => { + l10n = new loot.Translator(); + }); + + it('should return original string if the translator has not been loaded', () => { + l10n.translate('foo').should.equal('foo'); + }); + + it('should return an empty string if nothing is passed', () => { + return l10n.load().then(() => { + l10n.translate().should.equal(''); + }); + }); + + it('should return the input string if the current locale is "en"', () => { + return l10n.load().then(() => { + l10n.translate('foo').should.equal('foo'); + }); + }); + + it('should return the translated string if locale data has been loaded', () => { + /* Since loading data doesn't work in the browser, hack it by setting some + data manually. */ + l10n.jed = new window.Jed({ + 'locale_data': { + 'messages': { + '': { + 'domain': 'messages', + 'lang': 'en', + 'plural_forms': 'nplurals=2; plural=(n != 1);', + }, + 'foo': ['bar'], + }, + }, + 'domain': 'messages', + }); + + l10n.translate('foo').should.equal('bar'); + }); + + it('should subsitute additional arguments into string', () => { + /* Since loading data doesn't work in the browser, hack it by setting some + data manually. */ + l10n.jed = new window.Jed({ + 'locale_data': { + 'messages': { + '': { + 'domain': 'messages', + 'lang': 'en', + 'plural_forms': 'nplurals=2; plural=(n != 1);', + }, + 'foo %1$s %2$s': ['%2$s is bar'], + }, + }, + 'domain': 'messages', + }); + + l10n.translate('foo %1$s %2$s', 'is not', 'bar').should.equal('bar is bar'); + }); + }); +});