// ==UserScript==
// @name JvAlt
// @namespace http://tampermonkey.net/
// @version 1.1.1
// @description Script facilitant la gestion des double-comptes JVC.
// @author PneuTueur
// @match *://*.jeuxvideo.com/forums*
// @icon https://jvflux.fr/images/3/3b/icon2.png
// @license MIT
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
const UPDATE_DELAY = 50;
const CHECKS_PER_DAY = 10;
const SCRIPT_CHECKS_PER_DAY = 2;
const MAX_ACCOUNTS = 256;
const STATUS_TO_INFO = {"available": "est disponible", "not-found": "n'existe pas", "banned": "est banni", "checking": "est en cours de traitement"};
const BEFORE_TRANSITION = 400;
const SCRIPT_URL = "https://greasyfork.org/fr/scripts/495483-jvalt";
const PROXY_URL = "https://corsproxy.io/?";
async function checkScriptUpdate() {
const ms = Date.now();
const toCheck = (ms - GM_getValue('lastScriptCheck', 0)) / (1000 * 60 * 60 * 24) >= 1/SCRIPT_CHECKS_PER_DAY;
if (!toCheck) return false;
const res = await fetch(PROXY_URL + encodeURIComponent(SCRIPT_URL) + "?dummy=" + ms);
const pageContent = await res.text();
const parser = new DOMParser();
const htmlDocument = parser.parseFromString(pageContent, "text/html");
const latestVersion = htmlDocument.querySelector('dd.script-show-version span').textContent;
const currentVersion = GM_info.script.version;
if (latestVersion != currentVersion && GM_getValue('lastIgnoredJvAltVersion', null) != latestVersion) {
if (confirm("Nouvelle version de JV Alt disponible. Souhaitez-vous l'installer ?")) {
window.open(SCRIPT_URL, '_blank');
} else {
GM_setValue('lastIgnoredJvAltVersion', latestVersion);
}
}
GM_setValue('lastScriptCheck', ms);
}
function toCheck(account) {
if (account.lastCheck === 0 || account.status === 'failed' || account.status === 'checking') return true;
if (account.lastCheck) {
const today = new Date();
const lastCheckDate = new Date(account.lastCheck);
const diff = today.getTime() - lastCheckDate.getTime();
const days = diff / (1000 * 60 * 60 * 24);
return days >= 1/CHECKS_PER_DAY
}
return true;
};
async function checkAccount(account) {
let getAccountPage = await fetch(`https://www.jeuxvideo.com/profil/${account.name.toLowerCase()}?mode=infos`);
let i = 0;
while (getAccountPage.status === 503) {
await new Promise(resolve => setTimeout(resolve, UPDATE_DELAY*(i+1)));
getAccountPage = await fetch(getAccountPage);
i++;
if (i >= 2) {
account.status = "failed";
account.level = 0;
return account;
}
}
account.lastCheck = Date.now();
if (getAccountPage.status===404) {
account.status = "not-found";
account.level = -1;
return account;
}
const pageContent = await getAccountPage.text();
const parser = new DOMParser();
const htmlDocument = parser.parseFromString(pageContent, "text/html");
const bannedBanner = htmlDocument.querySelector(".alert.alert-danger");
const errorImage = htmlDocument.querySelector("img.img-erreur");
if (bannedBanner) {
account.status = "banned";
account.level = 0;
account.name = htmlDocument.querySelector('title').textContent.trim().replace('Informations personnelles sur le profil ', '').replace(' - jeuxvideo.com', '');
} else {
account.status = "available";
account.level = parseInt(htmlDocument.querySelector('span.JvCare.ladder-link').textContent.trim().replace('Niveau ', ''));
account.name = htmlDocument.querySelector('title').textContent.trim().replace('Informations personnelles sur le profil ', '').replace(' - jeuxvideo.com', '');
}
return account;
};
(function() {
'use strict';
const container = document.querySelector("#forum-right-col");
if (!container) return;
var totalAccounts;
const savedAccounts = GM_getValue("totalAccounts", localStorage.getItem("totalAccounts"));
if (savedAccounts && savedAccounts !== '' && savedAccounts.length !== 0) {
totalAccounts = JSON.parse(savedAccounts);
} else {
totalAccounts = [];
}
const buildContainer = () => {
const templateCSS = `
<style>
#jvalt-lists-container {
padding-left: 10px;
position: relative;
max-height: 20rem;
overflow: auto;
}
.jvalt-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.jvalt-accounts-list li {
width: 105%;
align-items: center;
display: flex;
}
ul.jvalt-accounts-list {
list-style-type: none;
vertical-align: top;
display: inline-block;
margin-bottom: 0;
}
ul.jvalt-accounts-list#list1 {
padding-left: .2rem;
}
ul.jvalt-accounts-list#list2 {
position: absolute;
right: 0;
margin-right: 2.5em;
padding-left: 0;
}
li.jvalt-account-item a {
font-weight: bold;
}
li.jvalt-account-item.checking a {
opacity: 0.3;
}
li.jvalt-account-item.banned a, span.banned {
color: red!important;
}
li.jvalt-account-item.available a, span.available {
color: lawngreen!important;
}
li.jvalt-account-item.not-found a, span.not-found {
color: yellow!important;
}
li.jvalt-account-item.not-found, p.jvalt-form-info {
opacity: 1;
transition: opacity .5s ease-out;
}
li.jvalt-account-item.to-remove, p.jvalt-form-info.to-remove {
opacity: 0 !important;
}
li.jvalt-account-item.failed a {
color: orange!important;
}
li.jvalt-account-item .jvalt-list-refresh {
margin-right: 6px;
margin-left: auto;
}
li.jvalt-account-item .jvalt-list-clickable-span {
font-weight: bold;
}
li.jvalt-account-item .jvalt-list-clickable-span:hover {
cursor: pointer;
color: var(--jv-text-hover-secondary);
}
li.jvalt-account-item sup.jvalt-account-level {
margin-right: 7px;
}
.bloc-forums-preferes .jvalt-add-account {
padding: 7px 12px 7px 12px;
border-color: var(--jv-input-border-color);
background-color: var(--jv-input-bg-color);
border-radius: 0.25rem;
}
.form-group {
position: relative;
}
.form-group-span {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
}
.jvalt-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: .0625rem solid var(--jv-border-color);
margin-bottom: .5rem;
}
.jvalt-header-options {
padding-bottom: .4rem;
display: flex;
}
.jvalt-header-file {
display: flex;
}
.jvalt-header-clickable {
color: var(--jv-blue-gray-color);
text-transform: none;
font-size: .8125rem;
margin-left: 5px;
cursor: pointer;
}
.jvalt-import-input {
display: none;
}
.jvalt-header-remove {
margin-left: .9rem;
margin-top: .1rem;
opacity: .7;
cursor: pointer;
}
.jvalt-header-remove:hover {
opacity: 1;
}
.jvalt-form {
margin-top: 9px;
}
.jvalt-form-info {
margin-bottom: .2rem;
margin-top: .2rem;
text-align: center;
}
.jvalt-iterate-form-group {
margin-top: 10px;
}
.jvalt-iterate-inputs-group {
display: flex;
gap: 10px;
}
.jvalt-radical-input {
width: 350%;
}
.jvalt-submit-iterate {
margin-top: 10px;
}
</style>
`
const templateHTML = `
<div class="card card-jv-forum card-forum-margin jvalt-root">
<div class="card-header">JV Alt</div>
<div class="card-body">
<div class="bloc-forums-preferes">
<div class="jvalt-header">
<h4 style="border-bottom: none; margin-bottom: 0; padding-bottom: .4rem;" class="jvalt-account-subtitle titre-info-fofo">Comptes</h4>
<div class="jvalt-header-options">
<div class="jvalt-header-file">
<a class="jvalt-header-clickable jvalt-header-import">Importer<input class="jvalt-import-input" type="file" accept=".json"></input></a>
<a href="#" class="jvalt-header-clickable jvalt-header-export">Exporter</a>
</div>
<span class="picto-msg-croix jvalt-header-remove" title="Supprimer toute la liste"><span>Supprimer</span></span>
</div>
</div>
<div id="jvalt-lists-container">
<ul class="jvalt-accounts-list" id="list1"></ul>
<ul class="jvalt-accounts-list" id="list2"></ul>
</div>
<div class="jvalt-form">
<h4 class="titre-info-fofo">Ajouter un compte</h4>
<input maxlength="15" class="txt-search form-control jvalt-add-account" type="text" placeholder="Ajouter un compte (Entrée pour valider)" autocomplete="off" value="">
</div>
<div class="jvalt-form">
<h4 class="titre-info-fofo">Ajouter une série</h4>
<div class="jvalt-iterate-form-group">
<form class="jvalt-iterate-form" onsubmit="return false;">
<div class="jvalt-iterate-inputs-group">
<input maxlength="13" required class="txt-search form-control jvalt-add-account jvalt-radical-input" placeholder="Entrez un radical" type="text"</input><input required min="1" class="txt-search form-control jvalt-iterate-input" id="jvalt-iterate1" type="number" placeholder="01"></input><input required class="txt-search form-control jvalt-iterate-input" id="jvalt-iterate2" type="number" placeholder="10"></input>
</div>
<button type="submit" class="jvalt-submit-iterate btn btn-actu-new-list-forum">Valider</button>
</form>
</div>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', templateCSS);
container.insertAdjacentHTML('beforeend', templateHTML);
};
buildContainer();
const bannedAccountContainer = document.querySelector("div.jvalt-root");
const listsContainer = document.querySelector('#jvalt-lists-container');
const htmlList1 = bannedAccountContainer.querySelector("ul.jvalt-accounts-list#list1");
const htmlList2 = bannedAccountContainer.querySelector("ul.jvalt-accounts-list#list2");
const exportButton = document.querySelector('a.jvalt-header-export');
const importInput = document.querySelector('input.jvalt-import-input');
const addAccountInput = document.querySelector("input.jvalt-add-account");
const headerRemove = document.querySelector('span.jvalt-header-remove');
const importLink = document.querySelector('a.jvalt-header-import');
const exportLink = document.querySelector('a.jvalt-header-export');
const radicalInput = document.querySelector('input.jvalt-radical-input');
const iterate1 = document.querySelector('input#jvalt-iterate1');
const iterate2 = document.querySelector('input#jvalt-iterate2');
const submitIterateBtn = document.querySelector('button.jvalt-submit-iterate');
const updateAccount = (account) => {
checkAccount(account).then(async (accountRes) => {
const accountIndex = totalAccounts.findIndex(a => a.name === account.name);
totalAccounts[accountIndex] = accountRes;
updateWholeContainer(totalAccounts);
if (accountRes.status === 'not-found') {
setTimeout(() => removeAccount(accountRes), BEFORE_TRANSITION);
}
});
}
const updateAccounts = async (accounts, check = true) => {
const accountsToUpdate = check ? accounts.filter(a => toCheck(a)) : accounts;
for (const a of accountsToUpdate) {
await updateAccount(a);
await new Promise(resolve => setTimeout(resolve, UPDATE_DELAY));
}
}
const importFile = () => {
if (importInput.value=='') { return -1; }
var file = importInput.files[0];
var invalidFormatString = 'ERREUR : Format invalide (voir exemple de format valide dans un fichier exporté).';
var currentAccounts = totalAccounts.map(a => a.name.toLowerCase());
var reader = new FileReader();
reader.onload = (event) => {
var jsonString = event.target.result;
try {
var accountsList = JSON.parse(jsonString);
var addableAccounts = [];
for (let account of accountsList) {
if (!("name" in account) || !("lastCheck" in account) || !("status" in account) || !("level" in account)) {
alert(invalidFormatString);
return -2;
}
if (!currentAccounts.includes(account.name.toLowerCase())) {
addableAccounts.push(account);
}
}
if (addableAccounts.length===0) return -2;
addableAccounts = addableAccounts.slice(0, MAX_ACCOUNTS - totalAccounts.length);
totalAccounts = totalAccounts.concat(addableAccounts);
updateWholeContainer(totalAccounts);
updateAccounts(addableAccounts, true);
} catch(SyntaxError) {
alert(invalidFormatString);
return -2;
}
};
reader.readAsText(file);
importInput.value = '';
}
const createExport = (data, filename = 'jvalt_list') => {
const blob = new Blob([JSON.stringify(data)], { type: "application/json" });
exportButton.href = URL.createObjectURL(blob);
exportButton.download = filename + ".json";
}
const removeAccount = async (account, tempAccount = false) => {
if (tempAccount) {
totalAccounts = totalAccounts.filter(a => a.name !== account.name);
updateWholeContainer(totalAccounts, true, false);
const accountEl = document.querySelector(`li.jvalt-account-item[name="${account.name}"`);
accountEl.addEventListener('transitionend', () => {
accountEl.remove();
});
accountEl.classList.add('to-remove');
} else {
totalAccounts = totalAccounts.filter(a => a.name !== account.name);
updateWholeContainer(totalAccounts);
}
}
const removeAll = () => {
htmlList1.innerHTML = '';
htmlList2.innerHTML = '';
totalAccounts = [];
GM_setValue('totalAccounts', []);
}
const addTempAccounts = (accounts, updateList = true) => {
if (updateList) {
totalAccounts = totalAccounts.concat(accounts);
}
accounts.forEach(a => {
const list = (htmlList1.querySelectorAll('li').length === htmlList2.querySelectorAll('li').length) ? htmlList1 : htmlList2;
const htmlStr = `<li name="${a.name}" class="jvalt-account-item ${a.status}"><a style="pointer-events: none;">${a.name}</a>`;
list.insertAdjacentHTML('beforeend', htmlStr);
});
}
const addFormInfo = (account) => {
const previousForm = document.querySelector('.jvalt-form-info');
if (previousForm) {
previousForm.remove();
}
const formInfo = document.createElement('p');
formInfo.setAttribute('class','jvalt-form-info');
formInfo.innerHTML = `${account.name} ${account.status!=='not-found' ? `<span class="${account.status}">${STATUS_TO_INFO[account.status]}</span>` : `<span class="${account.status}">${STATUS_TO_INFO[account.status]}</span>`}`;
listsContainer.appendChild(formInfo);
return formInfo
}
const removeFormInfo = (formInfo) => {
formInfo.classList.add('to-remove');
formInfo.addEventListener('transitionend', (event) => {
formInfo.remove();
});
}
const updateWholeContainer = (accounts, saveUpdate = true, rewriteHTML = true) => {
if (rewriteHTML) {
accounts.sort((a, b) => b.level - a.level);
htmlList1.innerHTML = '';
htmlList2.innerHTML = '';
accounts.forEach((a, index) => {
const list = (index%2==0) ? htmlList1 : htmlList2;
const htmlStr = `<li name="${a.name}" class="jvalt-account-item ${a.status}"><a title="Ce pseudo ${STATUS_TO_INFO[a.status]}"${a.status==='not-found' ? 'style="pointer-events: none;"' : ""} target="_blank" href="https://www.jeuxvideo.com/profil/${a.name.toLowerCase()}?mode=infos">${a.name}</a><span><sup class="jvalt-account-level">${a.level > 0 ? a.level : ""}</sup></span><span name="${a.name}" class="jvalt-list-clickable-span jvalt-list-refresh" title="Rafraîchir">🗘</span><span name="${a.name}" class="jvalt-list-clickable-span jvalt-list-remove" title="Supprimer">×</span></li>`;
list.insertAdjacentHTML('beforeend', htmlStr);
});
}
if (saveUpdate) {
GM_setValue('totalAccounts', JSON.stringify(accounts));
createExport(totalAccounts);
}
};
const submitIterate = (event) => {
if (radicalInput.checkValidity() && iterate1.checkValidity() && iterate2.checkValidity()) {
event.preventDefault();
const accountsToAdd = [];
for (let i = parseInt(iterate1.value); i <= parseInt(iterate2.value); i++) {
let nb = i < 10 ? '0' + i : i;
let pseudo = radicalInput.value + nb;
if (totalAccounts.findIndex(a => a.name.toLowerCase() === pseudo.toLowerCase()) < 0) {
accountsToAdd.push({ name: pseudo, lastCheck: 0, status: "checking", level: 0 });
}
}
radicalInput.value = '';
iterate1.value = '';
iterate2.value = '';
if (totalAccounts.length + accountsToAdd.length >= MAX_ACCOUNTS) {
alert('[ERREUR] Nombre maximal de comptes atteint.');
return -1;
}
if (accountsToAdd.length === 0) return -1;
addTempAccounts(accountsToAdd);
updateAccounts(accountsToAdd, false);
}
}
addAccountInput.addEventListener("keypress", (event) => {
if (event.keyCode === 13) {
const value = event.target.value;
const canAdd = value !== "" && totalAccounts.findIndex(a => a.name.toLowerCase() === value.toLowerCase()) < 0;
if (totalAccounts.length >= MAX_ACCOUNTS) {
alert('[ERREUR] Nombre maximal de comptes atteint.');
return -1;
}
if (canAdd) {
const account = { name: value, lastCheck: 0, status: "checking", level: 0 };
addTempAccounts([account], false);
checkAccount(account).then(async accountRes => {
const formInfo = await addFormInfo(accountRes);
setTimeout(() => removeFormInfo(formInfo), BEFORE_TRANSITION);
if (accountRes.status === 'not-found') {
updateWholeContainer(totalAccounts.concat([accountRes]), false);
setTimeout(() => removeAccount(accountRes, true), BEFORE_TRANSITION);
} else {
totalAccounts.push(accountRes);
updateWholeContainer(totalAccounts);
}
});
}
event.target.value = '';
}
});
headerRemove.onclick = removeAll;
importInput.onchange = importFile;
bannedAccountContainer.addEventListener("click", (event) => {
if (event.target.classList.contains("jvalt-list-remove")) {
const account = totalAccounts.find(a => a.name === event.target.parentNode.getAttribute("name"));
removeAccount(account);
} else if (event.target.classList.contains("jvalt-list-refresh")) {
let accountIndex = totalAccounts.findIndex(a => a.name === event.target.parentNode.getAttribute("name"));
let account = totalAccounts[accountIndex];
let accountLink = event.target.parentNode.querySelector('a');
event.target.parentNode.className = 'jvalt-account-item';
event.target.parentNode.classList.add('checking');
checkAccount(account).then(accountRes => {
totalAccounts[accountIndex] = accountRes;
updateWholeContainer(totalAccounts);
if (accountRes.status === 'not-found') {
setTimeout(() => removeAccount(accountRes, true), 500);
}
});
}
});
exportLink.addEventListener('click', (event) => {
if (!totalAccounts || totalAccounts.length===0) {
event.preventDefault();
alert("Il n'y a rien à exporter.");
return;
}
});
importLink.addEventListener('click', (event) => {
importInput.click();
});
radicalInput.addEventListener('change', (event) => {
const exponent = 15 - radicalInput.value.length;
const maxValue = 10**exponent - 1;
iterate2.setAttribute('max', maxValue);
});
iterate1.addEventListener('change', (event) => {
const minInput = iterate1.getAttribute('min');
const minPossible = minInput ? parseInt(minInput) : parseInt(iterate1.value);
const minVal = Math.max(parseInt(iterate1.value), minPossible);
var toDisplay = (0 <= minVal && minVal < 10) ? '0' + minVal : minVal;
iterate1.value = toDisplay;
iterate2.setAttribute('max', minVal + MAX_ACCOUNTS - 1);
});
iterate2.addEventListener('change', (event) => {
const maxInput = iterate2.getAttribute('max');
const maxPossible = maxInput ? parseInt(maxInput) : parseInt(iterate2.value);
const maxVal = Math.min(parseInt(iterate2.value), maxPossible);
var toDisplay = (0 <= maxVal && maxVal < 10) ? '0' + maxVal : maxVal;
iterate2.value = toDisplay;
iterate1.setAttribute('min', Math.max(1, maxVal - MAX_ACCOUNTS));
});
submitIterateBtn.onclick = submitIterate;
if (totalAccounts.length > 0) {
updateWholeContainer(totalAccounts);
updateAccounts(totalAccounts, true);
}
checkScriptUpdate();
console.log('JvAlt is ready');
})();