JVCHAT
Version de Shiho corrigée (manque plus que la gestion du scroll auto...)unknown
javascript
3 days ago
70 kB
95
No Index
// ==UserScript==
// @name JVChat DEBUG
// @description JVChat avec debug intégré
// @author m7r-227 - captain_cid31
// @namespace JVChat
// @license MIT
// @version 0.2.4-debug
// @match https://*.jeuxvideo.com/forums/42-*
// @match https://*.jeuxvideo.com/forums/1-*
// @grant none
// ==/UserScript==
// downloadURL https://update.greasyfork.org/scripts/574211/JVChat%20DEBUG.user.js
// updateURL https://update.greasyfork.org/scripts/574211/JVChat%20DEBUG.meta.js
const _DBG = false ;
const _P = '%c[JVChat]';
const _S = 'color:#00e5ff;font-weight:bold';
const _W = 'color:#ffab00;font-weight:bold';
const _E = 'color:#ff1744;font-weight:bold';
function dbg(...a) { if (_DBG) console.log(_P, _S, ...a); }
function wrn(...a) { if (_DBG) console.warn(_P, _W, ...a); }
function err(...a) { if (_DBG) console.error(_P, _E, ...a); }
function addStyle() {
const style = document.createElement('style');
style.textContent = `
@font-face {
font-family: "jvchat-icons";
src: url(data:font/woff;base64,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);
}
.refresh-loader {
--size: 30px;
--thickness: 3px;
--value: 0;
width: var(--size);
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--jv-text-secondary) calc(var(--value) * 1%), var(--jv-block-bg-color) 0);
position: relative;
display: flex;
align-items: center;
justify-content: center;
font: 600 calc(var(--size) * 0.25)/1.1 system-ui;
color: var(--jv-text-secondary);
transition: background .4s ease;
align-self: end;
}
.refresh-loader::before {
content: '';
position: absolute;
inset: calc(var(--thickness));
border-radius: 50%;
background: var(--jv-block-bg-color);
}
.refresh-loader__text {
position: relative;
pointer-events: none;
}
.btn-open-jvchat {
margin-right: 0.3125rem;
}
#jvchat-user-notif.has-notif::after,
#jvchat-user-mp.has-notif::after {
z-index: 2;
content: " " attr(data-val) "";
color: #fff;
line-height: 1.25rem;
font-size: 0.9rem;
padding: 0 .25rem;
position: absolute;
top: .6875rem;
right: -.6875rem;
background: #ff3c00;
width: 1.1rem;
height: 1.1rem;
border-radius: 1rem;
}
#jvchat-mp-and-notif .nav-link,
.nav-link-search,
.account-pseudo {
color: white !important;
}
.jvchat-container {
display: flex;
height: 100vh;
background-color: var(--jv-bg-color);
font-family: system-ui, sans-serif;
}
.jvchat-sidebar {
flex: 0 0 15%;
max-width: 254px;
background-color: var(--jv-block-bg-color);
border-right: 1px solid #2b2e30;
padding: 1rem;
box-sizing: border-box;
overflow-y: auto;
display: flex;
flex-direction: column;
position: relative;
}
.jvchat-profile {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.jvchat-username {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 700;
text-align: center;
color: #fff;
}
.jvchat-profile-avatar {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
}
.jvchat-profile-actions {
display: flex;
gap: 0.75rem;
}
.jvchat-topic-heading {
font-size: 0.875rem;
line-height: 1.42857;
margin: 0;
align-self: flex-start;
}
.jvchat-chat {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.jvchat-messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.jvchat-message {
display: flex;
gap: 0.75rem;
background: var(--jv-block-bg-color);
padding: 0.75rem;
border-radius: 0.5rem;
border: 0.0625rem solid var(--jv-border-color);
}
.jvchat-message:nth-of-type(2n) {
background: var(--jv-block-even-bg-color);
}
.jvchat-content p:last-of-type {
margin-bottom: 0px;
}
.jvchat-message-controls {
display: flex;
align-items: center;
gap: 0.75rem;
visibility: hidden;
}
.jvchat-message-controls span {
cursor: pointer;
}
.jvchat-message:hover .jvchat-message-controls {
visibility: visible;
}
.jvchat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
flex-shrink: 0;
}
.jvchat-message-body {
flex: 1;
color: var(--jv-text-color)
}
.jvchat-message-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.jvchat-user {
font-weight: 600;
}
.jvchat-date {
font-size: 0.75rem;
opacity: 0.7;
}
.jvchat-form {
padding: 0 1rem;
/*padding: 1rem;*/
padding-top: 0;
}
.messageEditor__edit {
height: 7rem ! important;
line-height: normal;
}
.jvchat-badge {
position: absolute;
top: 8px;
right: -10px;
min-width: 16px;
padding: 1px 4px;
font: 16px/16px Arial, sans-serif;
color: #fff;
background: #ff3c00;
border-radius: 9999px;
text-align: center;
z-index: 10;
pointer-events: none;
}
.jvchat-settings-panel {
position: absolute;
inset: 0;
background-color: var(--jv-block-bg-color);
transform: translateX(-100%);
transition: transform 0.35s cubic-bezier(.4,0,.2,1);
z-index: 2000;
display: flex;
flex-direction: column;
gap: .75rem;
padding: 1rem 1.25rem;
box-shadow: 2px 0 12px rgba(0,0,0,.15);
overflow-y: auto;
}
.jvchat-settings-panel.open {
transform: translateX(0);
}
#jvchat-settings-list {
display: flex;
flex-direction: column;
gap: .75rem;
}
.jvchat-message-deleted > div {
opacity: 0.2;
filter: grayscale(100%);
}
.jvchat-message-deleted {
position: relative;
}
.jvchat-message-deleted:after {
content: "Message supprimé";
position: absolute;
top: 40%;
left: 50%;
color: gray;
font-weight: bold;
opacity: 0.7;
/* MODIF PERSO : CONSERVER LES BOUTONS CITER ET MODIFIER POUR LES MESSAGES SUPPRIMÉS */
pointer-events: none !important;
}
.messageEditor__containerPreview,
.buttonsEditor__groupPreview {
display:none;
}
/* MODIF PERSO : CONSERVER LES BOUTONS CITER ET MODIFIER POUR LES MESSAGES SUPPRIMÉS */
.jvchat-message-deleted .jvchat-message-controls span:not(.jvchat-quote-btn):not(.jvchat-edit-btn) {
display: none !important;
}
.jvchat-message-deleted .jvchat-quote-btn,
.jvchat-message-deleted .jvchat-edit-btn {
display: inline-block !important;
opacity: 1 !important;
pointer-events: auto !important;
position: relative;
z-index: 100; /* Pour passer au dessus de l'overlay "supprimé" */
}
.jvchat-message-deleted .jvchat-message-controls {
visibility: visible !important;
opacity: 1 !important;
}
`;
document.head.appendChild(style);
}
(function createJVChatButton() {
const html = '<button class="buttonsNavbar__button btn-open-jvchat" type="button"><i class="buttonsNavbar__icon icon-topics-list"></i><div class="buttonsNavbar__label">JVChat</div></button>';
const refreshBtn = document.querySelector('.buttonsNavbar .buttonsNavbar__icon.icon-refresh').parentNode;
refreshBtn.insertAdjacentHTML('beforebegin', html);
const button = document.querySelector('.btn-open-jvchat');
button.addEventListener('click', () => {
const jvchat = new JVChat();
jvchat.start();
});
})();
class JVChat {
constructor() {
this.jvcApi = null;
this.jvcClient = null;
this.lastPage = 1;
this.messages = [];
this.refreshRate = 6000;
this.isTabActive = true;
this.unreadMessagesCount = 0;
this.autoScrollingEnabled = true;
}
async start() {
addStyle();
this.jvcClient = new JVCClient();
const info = this.jvcClient.parseURL(location.href);
this.topicId = info.topicId;
this.forumId = info.forumId;
this.viewId = info.viewId;
this.topicTitle = info.title;
this.jvcApi = new JVCAPI(this.viewId, this.forumId, this.topicId, this.topicTitle);
this.jvchatSettings = new JVChatSettings();
this.createJVChatInterface();
const page = parsePage(document);
this.lastPage = page.lastPage;
dbg(`start() | lastPage initial = ${this.lastPage}`);
// Charger les 2 dernières pages au démarrage (max 40 msgs)
await this.loadInitialMessages();
const loader = document.querySelector('.refresh-loader');
let lastRefresh = performance.now();
setInterval(() => {
const now = performance.now();
const elapsed = now - lastRefresh;
const percent = (elapsed / this.refreshRate) * 100;
loader.style.setProperty('--value', percent);
if (elapsed >= this.refreshRate) {
lastRefresh = now;
this.fetchNewMessages();
}
}, 50);
document.addEventListener('visibilitychange', () => {
this.isTabActive = document.visibilityState === 'visible';
if (this.isTabActive) {
this.unreadMessagesCount = 0;
this.jvcClient.updateFaviconWithCount(0);
this.scrollToBottom();
this.autoScrollingEnabled = true;
}
});
}
async loadInitialMessages() {
dbg(`📦 Chargement initial — lastPage=${this.lastPage}`);
const t0 = performance.now();
try {
// Si le topic a au moins 2 pages, charger l'avant-dernière d'abord
if (this.lastPage >= 2) {
const prevPageNum = this.lastPage - 1;
dbg(`📦 Chargement page ${prevPageNum} (avant-dernière)`);
const prevDoc = await this.jvcApi.getPageDocument(prevPageNum);
const prevPage = parsePage(prevDoc);
for (const message of prevPage.messages) {
this.addMessage(message);
}
dbg(`📦 Page ${prevPageNum}: ${prevPage.messages.length} messages chargés`);
}
// Charger la dernière page
dbg(`📦 Chargement page ${this.lastPage} (dernière)`);
const lastDoc = await this.jvcApi.getPageDocument(this.lastPage);
const lastPage = parsePage(lastDoc);
this.payload = getPayload(lastDoc);
this.jvcApi.payload = this.payload;
this.lastPage = lastPage.lastPage;
this.connectedUser = this.getConnectedUser(lastDoc);
document.querySelector('#jvchat-topic-title').textContent = lastPage.title;
document.querySelector('#jvchat-connected-count').textContent = `${this.payload.forumInfo.header.btnVal} connecté${this.payload.forumInfo.header.btnVal === 1 ? '' : 's'}`;
//document.querySelector('#jvchat-connected-count').textContent = lastPage.connectedCount + (lastPage.connectedCount === 1 ? ' connecté' : ' connectés');
document.querySelector('#jvchat-messages-count').textContent = lastPage.messagesCount + (lastPage.messagesCount === 1 ? ' message' : ' messages');
const profileContainer = document.querySelector('.jvchat-profile');
if (this.connectedUser) {
profileContainer.style.display = '';
document.querySelector('#jvchat-username').textContent = this.connectedUser.username;
document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl;
const messageBadge = document.querySelector('#jvchat-user-mp-badge');
if (this.connectedUser.messageCount > 0) {
messageBadge.textContent = this.connectedUser.messageCount;
messageBadge.style.display = '';
} else {
messageBadge.style.display = 'none';
}
const notificationBadge = document.querySelector('#jvchat-user-notif-badge');
if (this.connectedUser.notificationCount > 0) {
notificationBadge.textContent = this.connectedUser.notificationCount;
notificationBadge.style.display = '';
} else {
notificationBadge.style.display = 'none';
}
const profileAnchors = document.querySelectorAll('.jvchat-profile-url');
for (const anchor of profileAnchors) {
anchor.href = this.connectedUser.profileUrl;
}
document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl;
} else {
profileContainer.style.display = 'none';
}
for (const message of lastPage.messages) {
this.addMessage(message);
}
this.scrollToBottom();
const dt = (performance.now() - t0).toFixed(0);
dbg(`📦 Chargement initial OK (${dt}ms) | ${this.messages.length} messages au total | lastPage=${this.lastPage}`);
} catch (error) {
err(`📦 Chargement initial ❌:`, error.message);
// Fallback: lancer le polling normal qui chargera au moins la dernière page
}
}
async fetchNewMessages() {
const cycle = (this._cycle = (this._cycle || 0) + 1);
const prevLastPage = this.lastPage;
const t0 = performance.now();
dbg(`━━━ Cycle #${cycle} ━━━ polling page ${this.lastPage}`);
try {
const doc = await this.jvcApi.getPageDocument(this.lastPage);
const page = parsePage(doc);
this.payload = getPayload(doc);
this.jvcApi.payload = this.payload;
// ═══ Mise à jour de lastPage (provisoire, avant le check page pleine) ═══
this.lastPage = page.lastPage;
// ═══ DEBUG: payload check ═══
if (!this.payload) {
err(`Cycle #${cycle} payload est NULL !`);
} else if (!this.payload.ajaxToken) {
wrn(`Cycle #${cycle} payload sans ajaxToken`);
}
document.querySelector('#jvchat-topic-title').textContent = page.title;
document.querySelector('#jvchat-connected-count').textContent = `${this.payload.forumInfo.header.btnVal} connecté${this.payload.forumInfo.header.btnVal === 1 ? '' : 's'}`;
//document.querySelector('#jvchat-connected-count').textContent = page.connectedCount + (page.connectedCount === 1 ? ' connecté' : ' connectés');
document.querySelector('#jvchat-messages-count').textContent = page.messagesCount + (page.messagesCount === 1 ? ' message' : ' messages');
this.connectedUser = this.getConnectedUser(doc);
const profileContainer = document.querySelector('.jvchat-profile');
if (this.connectedUser) {
profileContainer.style.display = '';
document.querySelector('#jvchat-username').textContent = this.connectedUser.username;
document.querySelector('#jvchat-user-avatar').src = this.connectedUser.avatarUrl;
const messageBadge = document.querySelector('#jvchat-user-mp-badge');
if (this.connectedUser.messageCount > 0) {
messageBadge.textContent = this.connectedUser.messageCount;
messageBadge.style.display = '';
} else {
messageBadge.style.display = 'none';
}
const notificationBadge = document.querySelector('#jvchat-user-notif-badge');
if (this.connectedUser.notificationCount > 0) {
notificationBadge.textContent = this.connectedUser.notificationCount;
notificationBadge.style.display = '';
} else {
notificationBadge.style.display = 'none';
}
const profileAnchors = document.querySelectorAll('.jvchat-profile-url');
for (const anchor of profileAnchors) {
anchor.href = this.connectedUser.profileUrl;
}
document.querySelector('.jvchat-subscriptions-url').href = this.connectedUser.subscriptionsUrl;
} else {
profileContainer.style.display = 'none';
}
let newMsgCount = 0;
for (const message of page.messages) {
const existed = this.messages.some(m => m.id === message.id);
this.addMessage(message);
if (!existed) newMsgCount++;
}
if (newMsgCount > 0) {
dbg(`➕ ${newMsgCount} nouveaux messages ajoutés`);
}
// ═══ FIX PRINCIPAL v3 ═══
// La pagination JVC est injectée côté client (JS), absente du HTML brut.
// Quand la page courante est pleine (20 msgs), on probe la page suivante.
// Si JVC retourne la page N+1 → elle existe, on avance.
// Si JVC redirige vers la page N → elle n'existe pas encore, on reste.
if (page.messages.length >= 20) {
try {
const nextPageNum = this.lastPage + 1;
const nextDoc = await this.jvcApi.getPageDocument(nextPageNum);
const nextPageData = parsePage(nextDoc);
// Si le numéro de page retourné est supérieur, la page existe
if (nextPageData.lastPage > this.lastPage) {
this.lastPage = nextPageData.lastPage;
dbg(`📄 Page suivante ${this.lastPage} existe → avance`);
// Traiter aussi les messages de la nouvelle page
for (const msg of nextPageData.messages) {
this.addMessage(msg);
}
}
} catch (e) {
// Fetch de la page suivante échoué, on reste sur la page courante
dbg(`📄 Probe page suivante échoué: ${e.message}`);
}
}
if (this.lastPage !== prevLastPage) {
dbg(`📄 lastPage: ${prevLastPage} → ${this.lastPage}`);
}
const lastMessages = this.messages.slice(this.messages.length - 20, this.messages.length);
for (const message of lastMessages) {
if (message.page < this.lastPage) {
continue;
}
const existing = page.messages.find((m) => m.id === message.id);
if (!existing && !message.isDeleted) {
const msg = await this.jvcApi.getMessage(message.id);
if (!msg) {
message.element.classList.add('jvchat-message-deleted');
message.isDeleted = true;
dbg(`🗑️ Message #${message.id} marqué supprimé`);
}
}
}
if (!this.isTabActive) {
this.jvcClient.updateFaviconWithCount(this.unreadMessagesCount);
}
if (this.autoScrollingEnabled) {
this.scrollToBottom();
}
const dt = (performance.now() - t0).toFixed(0);
dbg(`Cycle #${cycle} OK (${dt}ms) | lastPage=${this.lastPage} | total msgs=${this.messages.length} | autoScroll=${this.autoScrollingEnabled}`);
} catch (error) {
err(`Cycle #${cycle} ❌ CRASH:`, error.message);
err(`Stack:`, error.stack);
}
}
scrollToBottom() {
requestAnimationFrame(() => {
this.messagesContainer.scrollTo({
top: this.messagesContainer.scrollHeight,
behavior: 'smooth'
});
});
}
addMessage(message) {
const existing = this.messages.find((m) => m.id === message.id);
if (existing) {
return;
}
if (!this.isTabActive) {
this.unreadMessagesCount++;
}
const html = `
<article class="jvchat-message mt-2" data-id="${message.id}">
<div>
<a href="${message.profileUrl}" target="_blank">
<img src="${message.avatarUrl}" alt="Alice avatar" class="jvchat-avatar mb-2">
</a>
</div>
<div class="jvchat-message-body">
<div class="jvchat-message-header">
<a href="/forums/message/${message.id}" target="_blank"> <!-- ======== MODIF PERSO N°2 : href="${message.profileUrl}" / href="/forums/message/${message.id}" : cliquer sur pseudo = redirige vers page du message ======== -->
<span class="jvchat-user">${message.username}</span>
</a>
<div class="d-flex">
<div class="jvchat-message-controls me-3">
<span class="picto-msg-quote jvchat-quote-btn" title="Citer"><span>Citer</span></span>
<span class="picto-msg-crayon jvchat-edit-btn" title="Editer" style="display: none;"><span>Editer</span></span>
<span class="picto-msg-croix jvchat-delete-btn" title="Supprimer" style="display: none;"><span>Supprimer</span></span>
</div>
<span class="jvchat-date">${message.creationDate.slice(message.creationDate.length - 8, message.creationDate.length)}</span>
</div>
</div>
<div class="jvchat-content text-enrichi-forum txt-msg">
${message.content}
</div>
</div>
</article>
`;
this.messagesContainer.insertAdjacentHTML('beforeend', html);
const messageElement = document.querySelector(`.jvchat-message[data-id="${message.id}"]`);
message.element = messageElement;
if (this.jvchatSettings.getSettingValue('display_page_separator')) {
let pageSeparator = document.querySelector(`.jvchat-page-separator-${message.page}`);
if (!pageSeparator) {
const template = document.createElement('template');
template.innerHTML = `
<div class="jvchat-page-separator-${message.page} text-center">
<small class="text-muted">
Page ${message.page}
</small>
</div>
`.trim();
pageSeparator = template.content.firstElementChild;
}
messageElement.insertAdjacentElement('afterend', pageSeparator);
}
//Fix JVCare
const jvcares = Array.from(messageElement.getElementsByClassName("JvCare"));
for (const jvcare of jvcares) {
const a = document.createElement("a");
a.setAttribute("target", "_blank");
a.setAttribute("href", this.jvcClient.jvCake(jvcare.getAttribute("class")));
a.innerHTML = jvcare.innerHTML;
jvcare.outerHTML = a.outerHTML;
}
//Fix Citation ouvrable
const togglableQuotes = Array.from(messageElement.querySelectorAll('.text-enrichi-forum > blockquote > blockquote'));
for (const togglableQuote of togglableQuotes) {
const toggleButton = document.createElement('button');
toggleButton.classList.add('message__collapsedQuote');
togglableQuote.insertBefore(toggleButton, togglableQuote.firstChild);
toggleButton.addEventListener('click', () => {
const blockQuote = toggleButton.closest('.message__blockquote');
blockQuote.classList.toggle('message__blockquote--visible');
});
}
if (this.connectedUser && this.connectedUser.username === message.username) {
const deleteBtn = messageElement.querySelector('.jvchat-delete-btn');
deleteBtn.style.display = 'block';
deleteBtn.addEventListener('click', () => {
this.deleteMessage(message);
});
const editBtn = messageElement.querySelector('.jvchat-edit-btn');
editBtn.style.display = 'block';
editBtn.addEventListener('click', () => {
this.editMessage(message, messageElement);
});
}
const quoteBtn = messageElement.querySelector('.jvchat-quote-btn');
quoteBtn.addEventListener('click', async () => {
dbg(`💬 Citation demandée pour #${message.id}`);
try {
// --- ÉTAPE DE FIX POUR LES IMAGES SUPPRIMÉES ---
// On crée un clone pour ne pas modifier l'affichage réel
const tempDiv = messageElement.querySelector('.jvchat-content').cloneNode(true) ;
// On transforme les JvCare en vrais liens (<a>) dans ce clone
const jvcares = Array.from(tempDiv.getElementsByClassName("JvCare")) ;
for (const jvcare of jvcares) {
const a = document.createElement("a") ;
const href = this.jvcClient.jvCake(jvcare.getAttribute("class")) ;
a.setAttribute("href", href) ;
// On s'assure que le contenu (l'image img-url) est préservé
a.innerHTML = jvcare.innerHTML ;
jvcare.replaceWith(a) ;
}
// ----------------------------------------------
// Maintenant on utilise tempDiv au lieu du sélecteur direct
const txt = reverseMessage(tempDiv, true) ;
let content = `> Le ${message.creationDate} ${message.username} a écrit :\n${txt}` ;
content += '\n\n' ;
if (this.jvcClient.createMessageTextarea.value.trim() !== '') {
content = '\n\n' + content ;
}
this.jvcClient.insertAtCursor(content) ;
this.jvcClient.createMessageTextarea.focus() ;
dbg(`💬 Citation insérée OK`) ;
}
catch (e) {
err(`💬 Citation ❌:`, e.message, e.stack);
this.jvcClient.alert('error', `Erreur citation : ${e.message}`);
}
});
const images = messageElement.querySelectorAll('.img-shack');
for (const image of images) {
const parent = image.parentElement;
const link = this.jvcClient.jvCake(parent.getAttribute('class'));
const anchor = document.createElement('a');
anchor.href = link;
anchor.target = '_blank';
anchor.appendChild(image);
parent.insertAdjacentElement('afterend', anchor);
parent.remove();
}
const mosaics = this.findMosaics(messageElement);
for (const mosaic of mosaics) {
const container = document.createElement('div');
container.style.lineHeight = 0;
mosaic[0].parentElement.insertAdjacentElement('beforebegin', container);
for (const image of mosaic) {
let insertLineBreak = false;
if (image.parentElement.nextElementSibling && image.parentElement.nextElementSibling.tagName === 'BR') {
insertLineBreak = true;
}
container.appendChild(image.parentElement);
if (insertLineBreak) {
container.appendChild(document.createElement('br'));
}
}
const btn = document.createElement('button');
btn.classList.add('btn', 'btn-annuler-modif-msg', 'mt-2');
if (this.jvchatSettings.getSettingValue('hide_mosaics')) {
btn.textContent = 'Cacher';
container.classList.add('d-none');
} else {
btn.textContent = 'Afficher';
}
btn.addEventListener('click', () => {
container.classList.toggle('d-none');
if (container.classList.contains('d-none')) {
btn.textContent = 'Afficher';
} else {
btn.textContent = 'Cacher';
}
});
container.insertAdjacentElement('afterend', btn);
}
this.messages.push(message);
}
findMosaics(messageEl) {
const tol = 6;
const nodes = [...messageEl.querySelectorAll('img.img-shack')].map(img => {
const { x, y, width: w, height: h } = img.getBoundingClientRect();
return { img, x, y, w, h };
});
if (nodes.length < 4) return [];
const snap = (value, buckets) => {
const bucket = buckets.find(v => Math.abs(v - value) < tol);
if (bucket !== undefined) return bucket;
buckets.push(value);
return value;
};
const rowVals = [];
const colVals = [];
nodes.forEach(n => {
n.row = snap(n.y, rowVals);
n.col = snap(n.x, colVals);
});
const adj = new Map(nodes.map(n => [n, new Set]));
const byRow = new Map();
const byCol = new Map();
nodes.forEach(n => {
(byRow.get(n.row) || byRow.set(n.row, []).get(n.row)).push(n);
(byCol.get(n.col) || byCol.set(n.col, []).get(n.col)).push(n);
});
byRow.forEach(list => {
list.sort((a, b) => a.x - b.x);
for (let i = 0; i < list.length - 1; i++) {
if (Math.abs(list[i + 1].x - list[i].x - list[i].w) < tol) {
adj.get(list[i]).add(list[i + 1]);
adj.get(list[i + 1]).add(list[i]);
}
}
});
byCol.forEach(list => {
list.sort((a, b) => a.y - b.y);
for (let i = 0; i < list.length - 1; i++) {
if (Math.abs(list[i + 1].y - list[i].y - list[i].h) < tol) {
adj.get(list[i]).add(list[i + 1]);
adj.get(list[i + 1]).add(list[i]);
}
}
});
const mosaics = [];
const seen = new Set();
nodes.forEach(start => {
if (seen.has(start)) return;
const stack = [start];
const component = [];
while (stack.length) {
const n = stack.pop();
if (seen.has(n)) continue;
seen.add(n);
component.push(n);
adj.get(n).forEach(nb => stack.push(nb));
}
const row = new Set(component.map(n => n.row));
const col = new Set(component.map(n => n.col));
if (row.size < 2 || col.size < 2) {
return;
}
component.sort((a, b) => (a.row === b.row ? a.col - b.col : a.row - b.row));
mosaics.push(component.map(n => n.img));
});
return mosaics;
}
getPageUrl(page) {
return `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`;
}
createJVChatInterface() {
for (const child of document.body.children) {
child.style.display = 'none';
}
const html = `
<div class="jvchat-container">
<aside class="jvchat-sidebar">
<span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span>
<div class="jvchat-profile" style="display: none;">
<a class="jvchat-profile-url" target="_blank">
<h2 id="jvchat-username" class="jvchat-username"></h2>
</a>
<a class="jvchat-profile-url" target="_blank">
<img id="jvchat-user-avatar" src="https://image.jeuxvideo.com/avatar/default.jpg" alt="Profile avatar" class="jvchat-profile-avatar">
</a>
<div class="jvchat-profile-actions">
<a href="https://www.jeuxvideo.com/messages-prives/boite-reception.php" target="_blank">
<span class="headerAccount__pm" data-val="0">
<i class="icon-pm"></i>
<span id="jvchat-user-mp-badge" class="jvchat-badge" style="display: none;">0</span>
</span>
</a>
<a class="jvchat-subscriptions-url" target="_blank">
<span class="headerAccount__notif" data-val="0">
<i class="icon-bell-off"></i>
<span id="jvchat-user-notif-badge" class="jvchat-badge" style="display: none;">0</span>
</span>
</a>
<button type="button" class="toggleTheme" onclick="window.jvc.toggleTheme();"></button>
</div>
</div>
<a href="${this.getPageUrl(1)}" class="mt-3">
<h3 id="jvchat-topic-title" class="jvchat-topic-heading"></h3>
</a>
<span id="jvchat-connected-count"s> </span>
<span id="jvchat-messages-count"s> </span>
<div style="flex-grow: 1;"></div>
<div class="refresh-loader" style="display: none;--value:0;"> <!-- ======== MODIF PERSO N°1, ENLEVER LE CERCLE DE CHARGEMENT EN BAS À GAUCHE : "display: none;" ======== -->
<span class="refresh-loader__text" style="display: none;">0%</span>
</div>
<div class="jvchat-settings-panel" id="jvchat-settings-panel">
<span class="breadcrumb-icon icon-config align-self-end jvchat-toggle-settings" title="Paramètres"><span>Réglages</span></span>
<div class="jvchat-settings-title">Settings</div>
<div id="jvchat-settings-list">
</div>
<button class="jvchat-toggle-settings btn btn-annuler-modif-msg mt-2"> Fermer </button>
</div>
</aside>
<section class="jvchat-chat">
<div class="jvchat-messages">
</div>
<div class="jvchat-form">
</div>
</section>
</div>
`;
document.body.insertAdjacentHTML('beforeend', html);
this.messagesContainer = document.querySelector('.jvchat-messages');
const JVCForm = document.querySelector('#forums-post-message-editor');
const formContainer = document.querySelector('.jvchat-form');
formContainer.appendChild(JVCForm);
const observer = new MutationObserver(() => {
const captcha = JVCForm.querySelector('.js-captcha-logo');
if (captcha) {
captcha.parentElement.parentElement.style.display = 'none';
observer.disconnect();
}
});
observer.observe(JVCForm, { attributes: true, childList: true, subtree: true });
const postButton = JVCForm.querySelector('.postMessage');
const editorButtons = JVCForm.querySelector('.buttonsEditor');
editorButtons.appendChild(postButton);
postButton.classList.add('float-end');
const previewToggleButton = document.querySelector('.buttonsEditor__groupPreview .buttonSwitch');
if (previewToggleButton.classList.contains('buttonSwitch--isActive') && !this.jvchatSettings.getSettingValue('display_preview_by_default')) {
previewToggleButton.click();
}
if (!previewToggleButton.classList.contains('buttonSwitch--isActive') && this.jvchatSettings.getSettingValue('display_preview_by_default')) {
previewToggleButton.click();
}
previewToggleButton.addEventListener('click', () => {
setTimeout(() => {
if (previewToggleButton.classList.contains('buttonSwitch--isActive')) {
JVCForm.querySelector('.messageEditor__containerPreview').classList.add('mt-3');
}
}, 0);
});
const textarea = JVCForm.querySelector('textarea#message_reponse');
textarea.setAttribute('placeholder', 'Hop hop hop, le message ne va pas s\'écrire tout seul !');
textarea.style.height = '';
textarea.addEventListener('blur', () => {
textarea.style.height = '';
});
const textareaObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'style') {
if (textarea.style.height === '160px') {
textarea.style.height = '';
}
}
}
});
textareaObserver.observe(textarea, { attributes: true, attributeFilter: ['style'] });
postButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.createMessage(textarea.value);
});
JVCForm.classList.remove('mb-3');
JVCForm.querySelector('.messageEditor__containerEdit').style.marginBottom = '0px';
JVCForm.querySelector('.messageEditor__containerPreview').style.marginBottom = '0px';
const settingPanel = document.querySelector('#jvchat-settings-panel');
const settingsToggleBtns = document.querySelectorAll('.jvchat-toggle-settings');
for (const btn of settingsToggleBtns) {
btn.addEventListener('click', () => {
settingPanel.classList.toggle('open');
});
}
this.messagesContainer.addEventListener('scroll', () => {
const isAtTheBottom = this.messagesContainer.scrollHeight - this.messagesContainer.scrollTop <= this.messagesContainer.clientHeight + 1;
this.autoScrollingEnabled = isAtTheBottom;
});
this.setupSettings();
}
getConnectedUser(doc) {
if (doc.querySelector('.headerAccount__pm')) {
const messageCount = parseInt(doc.querySelector('.headerAccount__pm').getAttribute('data-val'));
const notificationCount = parseInt(doc.querySelector('.headerAccount__notif').getAttribute('data-val'));
const avatarUrl = doc.querySelector('.headerAccount__avatar').style.backgroundImage.slice(5, -2).replace('/avatar-md/', '/avatar/');
const username = doc.querySelector('.headerAccount__pseudo').textContent.trim();
const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`;
const subscriptionsUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=abonnements`;
return { username, avatarUrl, profileUrl, subscriptionsUrl, messageCount, notificationCount };
}
return null;
}
setupSettings() {
const settingsList = document.querySelector('#jvchat-settings-list');
for (const setting of this.jvchatSettings.settings) {
const html = `
<div class="jvchat-setting">
<label for="${setting.key}"> ${setting.name} </label>
<input type="checkbox" id="${setting.key}" ${setting.value ? 'checked' : ''} />
</div>
`;
settingsList.insertAdjacentHTML('beforeend', html);
const input = settingsList.querySelector(`#${setting.key}`);
input.addEventListener('change', () => {
this.jvchatSettings.setSetting(setting.key, input.checked);
});
}
}
async createMessage(message) {
try {
const formData = new FormData();
formData.set('text', message);
formData.set('topicId', this.topicId);
formData.set('forumId', this.forumId);
formData.set('group', 1);
formData.set('messageId', 'undefined');
formData.set('ajax_hash', this.payload.ajaxToken);
for (const key in this.payload.formSession) {
formData.append(key, this.payload.formSession[key]);
}
const response = await fetch('https://www.jeuxvideo.com/forums/message/add', {
credentials: 'include',
method: 'POST',
mode: 'cors',
body: formData
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
if (result.errors) {
const messages = [];
for (const key of Object.keys(result.errors)) {
messages.push(result.errors[key]);
}
throw new Error(messages.join(' | '));
}
this.jvcClient.setTextAreaValue('');
this.fetchNewMessages();
} catch (err) {
this.jvcClient.alert('error', err.message);
console.error(err);
}
}
async deleteMessage(message) {
const deleteUrl = this.payload.topicActions.deleteMessageUrl;
const url = `https://www.jeuxvideo.com${deleteUrl}&ids=${message.id}`;
//const url = `https://www.jeuxvideo.com/forums/message/delete?type=delete&ajax_hash=${deleteHash}&ids=${message.id}`;
const response = await fetch(url, {
credentials: 'include',
method: 'POST',
mode: 'cors'
});
const result = await response.json();
if (result.errors.length > 0) {
throw new Error(result.errors.join(' | '));
}
this.fetchNewMessages();
}
/* GESTIONNAIRE DE LA MODIFICATION DE MESSAGE */
async editMessage(message, messageElement) {
try {
const messageContent = messageElement.querySelector('.jvchat-content') ; // Au lieu d'appeler l'API qui plante, on prend le texte déjà affiché
// --- FIX JVCARE POUR L'EDITION ---
const tempDiv = messageContent.cloneNode(true);
const jvcares = Array.from(tempDiv.getElementsByClassName("JvCare"));
for (const jvcare of jvcares) {
const a = document.createElement("a");
const href = this.jvcClient.jvCake(jvcare.getAttribute("class"));
a.setAttribute("href", href);
a.innerHTML = jvcare.innerHTML;
jvcare.replaceWith(a);
}
// ---------------------------------
let currentText = message.content_raw || reverseMessage(tempDiv, true) ; // On utilise la fonction reverseMessage déjà présente dans le script pour transformer le HTML enrichi en texte brut
// Si currentText vient de reverseMessage et contient les "> " de citation : nettoyage
if (!message.content_raw) {
currentText = currentText
.split('\n') // On coupe par ligne
.map(line => line.startsWith('> ') ? line.substring(2) : (line.startsWith('>') ? line.substring(1) : line)) // On vire le "> " ou ">"
.join('\n') ; // On recolle tout
}
const oldMessageContent = messageContent.innerHTML ;
messageContent.innerHTML = `
<div class="messageEditor__containerEdit">
<textarea class="messageEditor__edit mb-2" style="width:100%; min-height:100px; background:#222; color:#fff; border:1px solid #444;"></textarea>
</div>
<div class="d-flex justify-content-start gap-2">
<button class="simpleButton cancelMessage">Annuler</button>
<button class="simpleButton postMessage">Modifier</button>
</div>
` ;
const textarea = messageContent.querySelector('.messageEditor__edit') ;
textarea.value = currentText ;
textarea.focus() ;
// BOUTON ANNULER
messageContent.querySelector('.cancelMessage').onclick = () => {
messageContent.innerHTML = oldMessageContent ;
} ;
// BOUTON VALIDER
messageContent.querySelector('.postMessage').onclick = async () => {
try {
const newHtml = await this.jvcApi.updateMessage(message.id, textarea.value) ;
messageContent.innerHTML = newHtml ;
// On met à jour le content_raw pour la prochaine édition
message.content_raw = textarea.value ;
} catch (e) {
console.error("Erreur lors de la modif : " + e.message) ;
messageContent.innerHTML = oldMessageContent ;
}
} ;
} catch (error) {
console.error("Erreur édit : ", error) ;
}
}
/* FIN ASYNC EDITMESSAGE - GESTIONNAIRE DE LA MODIFICATION DE MESSAGE */
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function parsePage(doc) {
const title = doc.querySelector('#title-display-container').textContent.trim();
const connectedContainer = doc.querySelector('#forums-info-app .sideCardForum__headerExtra .sideCardForum__Link');
let connectedCount = 0;
if (connectedContainer) {
connectedCount = parseInt(connectedContainer.textContent.trim());
}
let lastPage = 1;
// ═══ BUG FIX: utiliser doc au lieu de document ═══
const pageAnchors = doc.querySelectorAll('.pagination .pagination__navigation a');
// ═══ DEBUG ═══
const liveAnchors = document.querySelectorAll('.pagination .pagination__navigation a');
dbg(`parsePage | doc anchors: ${pageAnchors.length} | document (live/caché) anchors: ${liveAnchors.length}`);
if (liveAnchors.length === 0 && doc !== document) {
dbg(`✅ Confirmation: le DOM live est caché (0 anchors). On utilise bien doc.`);
}
for (const anchor of pageAnchors) {
const page = parseInt(anchor.textContent.trim());
if (!isNaN(page) && page > lastPage) {
lastPage = page;
}
}
const currentPage = doc.querySelector('.pagination__item--current');
if (currentPage) {
const page = parseInt(currentPage.textContent);
if (!isNaN(page) && page > lastPage) {
lastPage = page;
}
}
const items = doc.querySelectorAll('.bloc-liste-num-page span a');
for (const item of items) {
const page = parseInt(item.textContent.trim());
if (!isNaN(page) && page > lastPage) {
lastPage = page;
}
}
const messageList = doc.querySelectorAll('#listMessages .messageUser');
const messages = [];
for (const message of messageList) {
let avatarUrl = 'https://image.jeuxvideo.com/avatar/default.jpg';
const avatar = message.querySelector('img.avatar__image');
if (avatar) {
avatarUrl = avatar.src;
}
const username = message.querySelector('.messageUser__label').textContent.trim();
const profileUrl = `https://www.jeuxvideo.com/profil/${username.toLowerCase()}?mode=infos`;
const id = message.id.split('-').pop();
messages.push({
id,
page: lastPage,
username,
avatarUrl,
profileUrl,
content: message.querySelector('.messageUser__msg').innerHTML,
creationDate: message.querySelector('.messageUser__date').textContent.trim()
});
}
const messagesCount = (lastPage * 20) - 20 + messages.length;
dbg(`parsePage résultat | lastPage=${lastPage} | msgs sur page=${messages.length} | total estimé=${messagesCount}`);
return { title, connectedCount, messagesCount, lastPage, messages };
}
function getPayload(doc) {
const scripts = doc.getElementsByTagName('script');
let rawPayloadString = null;
for (let i = 0; i < scripts.length; i++) {
const scriptContent = scripts[i].textContent || scripts[i].innerText;
if (scriptContent) {
const match = scriptContent.match(/window\.jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/);
if (match && match[1]) {
rawPayloadString = match[1];
break;
}
const jvcVarMatch = scriptContent.match(/jvc\.forumsAppPayload\s*=\s*['"]([^'"]+)['"]/);
if (!rawPayloadString && jvcVarMatch && jvcVarMatch[1]) {
rawPayloadString = jvcVarMatch[1];
break;
}
}
}
if (rawPayloadString) {
try {
const decodedPayload = JSON.parse(atob(rawPayloadString));
return decodedPayload;
} catch (e) {
err('getPayload décodage ❌:', e);
return null;
}
} else {
wrn('getPayload: forumsAppPayload introuvable dans le doc');
return null;
}
}
function reverseMessage(node, isInit, isUl) {
let quote = "";
let prevIsP = false;
let startsWithSpoil = false;
for (let child of node.childNodes) {
let name = child.nodeName;
switch (name) {
case "P": {
quote += reverseMessage(child) + "\n\n";
break;
}
case "STRONG": {
quote += "'''" + reverseMessage(child) + "'''";
break;
}
case "U": {
quote += "<u>" + reverseMessage(child) + "</u>";
break;
}
case "S": {
quote += "<s>" + reverseMessage(child) + "</s>";
break;
}
case "EM": {
quote += "''" + reverseMessage(child) + "''";
break;
}
case "BR": {
quote += "\n";
break;
}
case "UL": {
quote += reverseMessage(child, false, true) + "\n\n";
break;
}
case "OL": {
quote += reverseMessage(child, false, false) + "\n\n";
break;
}
case "LI": {
if (isUl === true) {
quote += "* " + reverseMessage(child) + "\n";
} else {
quote += "# " + reverseMessage(child) + "\n";
}
break;
}
case "DIV": {
let classList = child.classList;
if (classList.contains("message__spoil")) {
if (quote === "") {
startsWithSpoil = true;
}
quote += "<spoil>" + reverseMessage(child) + "</spoil>\n\n"
} else if (classList.contains("message__spoilContent")) {
quote += reverseMessage(child);
}
break;
}
case "SPAN": {
let classList = child.classList;
if (classList.contains("message__spoil")) {
quote += "<spoil>" + reverseMessage(child) + "</spoil>";
} else if (classList.contains("message__spoilContent")) {
quote += reverseMessage(child);
}
break;
}
case "LABEL": {
break;
}
case "INPUT": {
break;
}
case "IMG": {
quote += child.alt;
break;
}
case "A": {
if (child.href) {
quote += child.href;
} else {
quote += reverseMessage(child);
}
break;
}
case "PRE": {
quote += reverseMessage(child) + "\n\n";
break;
}
case "CODE": {
quote += "<code>" + child.textContent + "</code>";
break;
}
case "BLOCKQUOTE": {
if (prevIsP) {
quote = quote.trimEnd() + "\n" + reverseMessage(child).replace(/^/gm, '> ') + "\n\n";
} else {
quote += reverseMessage(child).replace(/^/gm, '> ') + "\n\n";
}
break;
}
case "#text": {
// The "isInit" check is to prevent the empty text surroudning message
// However, it may happen that the root node contains valid text child, so it need to be added somehow
// For some reason, an "new line" may be missing in this case, so just add it
if (!isInit || child.textContent.trim() !== "") {
quote += child.textContent;
if (isInit && !quote.endsWith("\n")) {
quote += "\n";
}
}
break;
}
default: {
break;
}
}
if (name == "P") {
prevIsP = true;
} else {
prevIsP = false;
}
}
quote = quote.replace(/(\n){3,}/g, '\n\n');
if (startsWithSpoil && isInit) {
quote = "\n" + quote.trimEnd();
} else {
quote = quote.trim();
}
if (isInit) {
quote = quote.replace(/^/gm, '> ');
}
return quote;
}
class JVCAPI {
constructor(viewId, forumId, topicId, topicTitle) {
this.viewId = viewId;
this.forumId = forumId;
this.topicId = topicId;
this.topicTitle = topicTitle;
this.payload = null;
}
async getMessageContentToUpdate(messageId) {
try {
const url = `https://www.jeuxvideo.com/forums/ajax_edit_message.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}&action=get`;
const response = await fetch(url, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Echec de la requête pour récupérer le message à editer');
}
const data = await response.json();
if (data.errors.length === 0) {
return data.jvcode;
} else {
throw new Error(data.errors[0]);
}
} catch (error) {
throw new Error(error);
}
}
async updateMessage(messageId, content) {
const formData = new FormData();
formData.set('text', content);
formData.set('topicId', this.topicId);
formData.set('forumId', this.forumId);
formData.set('group', 1);
formData.set('messageId', messageId);
formData.set('ajax_hash', this.payload.ajaxToken);
for (const key in this.payload.formSession) {
formData.append(key, this.payload.formSession[key]);
}
try {
const response = await fetch('https://www.jeuxvideo.com/forums/message/edit', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
if (!data.errors) {
return data.html;
} else {
throw new Error(data.errors[0]);
}
} catch (error) {
throw new Error(error);
}
}
async getMessage(messageId) {
try {
const url = `https://www.jeuxvideo.com/forums/message/${messageId}`;
const response = await fetch(url);
if (!response.ok) {
return null;
}
return await response.text();
} catch (err) {
return null;
}
}
async getPageDocument(page) {
try {
const url = `https://www.jeuxvideo.com/forums/${this.viewId}-${this.forumId}-${this.topicId}-${page}-0-1-0-${this.topicTitle}.htm`;
dbg(`⬇ Fetch page ${page}: ${url}`);
const t0 = performance.now();
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
dbg(`⬆ Page ${page} OK (${(performance.now() - t0).toFixed(0)}ms)`);
return doc;
} catch (error) {
err(`⬆ Fetch page ${page} ❌:`, error.message);
throw new Error(error);
}
}
async getMessageQuote(messageId) {
try {
const url = `https://www.jeuxvideo.com/forums/ajax_citation.php?id_message=${messageId}&ajax_hash=${this.payload.ajaxToken}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
const result = await response.json();
return result.txt;
} catch (error) {
throw new Error(error);
}
}
}
class JVCClient {
constructor() {
const link = document.querySelector('link[rel*=\'icon\']');
this.faviconUrl = link ? link.href : '/favicon.ico';
this.createMessageTextarea = document.querySelector('textarea#message_reponse');
}
parseURL(url) {
const matches = url.match(/^https:\/\/www\.jeuxvideo\.com\/(?:recherche\/)?forums\/(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(.*?)\.htm/);
if (matches === null) {
throw new Error('Url mismatch');
}
const viewId = parseInt(matches[1]);
const forumId = parseInt(matches[2]);
const topicId = parseInt(matches[3]);
const topicPage = parseInt(matches[4]);
const forumOffset = parseInt(matches[6]);
const title = matches[8];
return { viewId, forumId, topicId, topicPage, forumOffset, title };
}
updateFaviconWithCount(count) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
const canvas = document.createElement('canvas');
const size = 16;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, size, size);
if (count > 0) {
ctx.fillStyle = 'DodgerBlue';
ctx.fillRect(0, 0, ctx.measureText(count).width + 3, 11);
ctx.fillStyle = 'white';
ctx.font = 'bold 10px Verdana';
ctx.textBaseline = 'bottom';
ctx.fillText(count, 1, 11);
}
const newFavicon = canvas.toDataURL('image/png');
let link = document.querySelector('link[rel*=\'icon\']');
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = newFavicon;
};
img.src = this.faviconUrl;
}
alert(type, message) {
let color = 'text-white';
let bgColor = 'bg-danger';
switch (type) {
case 'error':
bgColor = 'bg-danger';
break;
case 'warning':
color = 'text-black';
bgColor = 'bg-warning';
break;
case 'success':
bgColor = 'bg-success';
break;
}
const id = `jvchat-alert-${Date.now()}`;
const html = `
<div id="${id}" class="position-fixed top-0 w-100 pe-none" style="z-index: 2147483647;">
<div class="toast-container w-100 d-flex align-items-center flex-column p-3 pe-auto">
<div class="toast align-items-center ${color} ${bgColor} border-0 fade show">
<div class="d-flex">
<div class="toast-body">
<div>${message}</div>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', html.trim());
const alert = document.querySelector(`#${id}`);
setTimeout(() => {
alert.remove();
}, 5000);
}
jvCake(str) {
const base16 = '0A12B34C56D78E9F';
const s = str.split(' ')[1];
let lien = '';
for (let i = 0; i < s.length; i += 2) {
lien += String.fromCharCode(base16.indexOf(s.charAt(i)) * 16 + base16.indexOf(s.charAt(i + 1)));
}
return lien;
}
setTextAreaValue(value) {
const prototype = Object.getPrototypeOf(this.createMessageTextarea);
const nativeSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
nativeSetter.call(this.createMessageTextarea, value);
this.createMessageTextarea.dispatchEvent(new Event('input', { bubbles: true }));
}
insertAtCursor(textToInsert) {
const value = this.createMessageTextarea.value;
const start = this.createMessageTextarea.selectionStart;
const end = this.createMessageTextarea.selectionEnd;
const newValue = value.slice(0, start) + textToInsert + value.slice(end);
this.setTextAreaValue(newValue);
this.createMessageTextarea.selectionStart = this.createMessageTextarea.selectionEnd = start + textToInsert.length;
}
}
class JVChatSettings {
constructor() {
this.settings = [
{
key: 'hide_mosaics',
name: 'Masquer les mosaïques',
description: 'Cache automatiquement les mosaïques d\'images NoelShack pour réduire le flooding.',
value: true
},
{
key: 'display_page_separator',
name: 'Afficher le numéro de page courante',
description: '',
value: true
},
{
key: 'display_preview_by_default',
name: 'Afficher la prévisualisation par défaut',
description: '',
value: false
}
];
this.loadSettings();
}
getSetting(key) {
return this.settings.find((setting) => setting.key === key);
}
getSettingValue(key) {
return this.getSetting(key).value;
}
setSetting(key, value) {
const setting = this.getSetting(key);
setting.value = value;
this.saveSettings();
}
saveSettings() {
for (const setting of this.settings) {
localStorage.setItem(setting.key, setting.value);
}
}
loadSettings() {
for (const setting of this.settings) {
const item = localStorage.getItem(setting.key);
if (item) {
setting.value = JSON.parse(item);
}
}
}
}Editor is loading...
Leave a Comment