// ==UserScript==
// @name PlatesMania Notifications Enhancer
// @namespace pm-like-avatars
// @version 1.5
// @description Avatars + relative timestamps on your own /userXXXX page. PM panel: swap flag → photo, add flag emoji, user avatar, relative time, proper hover previews, and show latest comments in comment notifications. Likes list: show plate PNG; tooltip preview gets car logo+model (or text) under the image.
// @match https://*.platesmania.com/user*
// @match http://*.platesmania.com/user*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// ---------- Only run on your own profile ----------
function getOwnUserPath() {
const loginBarUser = document.querySelector('.loginbar.pull-right > li > a[href^="/user"]');
if (loginBarUser) return new URL(loginBarUser.getAttribute("href"), location.origin).pathname;
const langLink = document.querySelector('.languages a[href*="/user"]');
if (langLink) return new URL(langLink.getAttribute("href")).pathname;
return null;
}
const ownPath = getOwnUserPath();
const herePath = location.pathname.replace(/\/+$/, "");
if (!ownPath || herePath !== ownPath.replace(/\/+$/, "")) return;
// ---------- Config ----------
const CACHE_KEY = "pm_profile_pic_cache_v1";
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const NOMER_CACHE_KEY = "pm_nomer_photo_cache_v1";
const NOMER_MAX_AGE_MS = 60 * 24 * 60 * 60 * 1000;
const PLATE_CACHE_KEY = "pm_plate_png_cache_v1";
const PLATE_MAX_AGE_MS = 180 * 24 * 60 * 60 * 1000;
// store plate meta (png + make + model + logoUrl if exists)
const PLATE_META_CACHE_KEY = "pm_plate_meta_cache_v1";
const LIKE_ITEM_SELECTOR = ".col-xs-12.margin-bottom-5.bg-info";
const CONTAINER_SELECTOR = "#mCSB_2_container, #content";
const PROCESSED_FLAG = "pmAvatarAdded";
GM_addStyle(`
.pm-like-avatar{width:20px!important;height:20px!important;border-radius:4px!important;object-fit:cover!important;vertical-align:text-bottom!important;margin-right:6px!important;overflow:hidden!important;display:inline-block!important}
.pm-avatar-preview{position:fixed!important;width:200px!important;height:200px!important;max-width:200px!important;max-height:200px!important;border-radius:10px!important;box-shadow:0 8px 28px rgba(0,0,0,0.28)!important;background:#fff!important;z-index:2147483647!important;pointer-events:none!important;display:none;object-fit:cover}
.pm-nomer-thumb{width:40px!important;height:40px!important;object-fit:cover!important;border-radius:6px!important}
.pm-plate{height:18px!important;width:auto!important;vertical-align:text-bottom!important;display:inline-block!important}
/* keep the old size for in-row logos if used elsewhere */
.pm-car-logo{height:18px!important;width:auto!important;vertical-align:text-bottom!important;margin-left:6px!important}
.pm-car-model{margin-left:6px!important;vertical-align:text-bottom!important;display:inline-block!important}
/* NEW: tooltip meta row under preview image */
.pm-tt-meta{margin-top:6px!important;display:flex!important;align-items:center!important;gap:8px!important;justify-content:center!important}
.pm-tt-logo{height:25px!important;width:auto!important;vertical-align:middle!important}
.pm-tt-text{font-weight:600!important;white-space:nowrap!important}
.pm-comment-preview{color:#333!important;display:flex;align-items:center;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3;gap:8px}
.pm-comment-preview b{font-weight:700}
.pm-comment-preview img{height:20px!important;width:auto!important;vertical-align:middle;display:inline-block!important;float:none!important;margin-left:4px;margin-right:4px}
.pm-comment-preview br{display:none}
.pm-vote{display:inline-flex;align-items:center;gap:4px;margin-left:auto;flex:0 0 auto}
.pm-vote .pm-vote-btn{cursor:pointer;color:#9ACD32;line-height:1}
.pm-vote .pm-vote-value{padding:0 3px;color:#CCC;font:bold 10pt Arial;line-height:1}
.pm-vote .pm-vote-value.pm-positive{color:rgb(51,153,0)!important}
.pm-vote .pm-vote-value.pm-negative{color:#CC0000!important}
.pm-vote .pm-vote-btn.pm-busy{opacity:.6;pointer-events:none}
`);
// ---------- Cache helpers ----------
function readCache(key) {
try {
const raw = typeof GM_getValue === "function" ? GM_getValue(key, "{}") : localStorage.getItem(key) || "{}";
return JSON.parse(raw);
} catch { return {}; }
}
function writeCache(obj, key) {
const raw = JSON.stringify(obj);
if (typeof GM_setValue === "function") GM_setValue(key, raw);
else localStorage.setItem(key, raw);
}
function getFromCache(id, key = CACHE_KEY, maxAge = MAX_AGE_MS) {
const c = readCache(key);
const e = c[id];
if (!e) return null;
if (!e.ts || (Date.now() - e.ts > maxAge)) { delete c[id]; writeCache(c, key); return null; }
return e.url;
}
function putInCache(id, url, key = CACHE_KEY) {
const c = readCache(key);
c[id] = { url, ts: Date.now() };
writeCache(c, key);
}
// meta cache (object payload)
function getMetaFromCache(id, key = PLATE_META_CACHE_KEY, maxAge = PLATE_MAX_AGE_MS) {
const c = readCache(key), e = c[id];
if (!e) return null;
if (!e.ts || (Date.now() - e.ts > maxAge)) { delete c[id]; writeCache(c, key); return null; }
return e.data;
}
function putMetaInCache(id, data, key = PLATE_META_CACHE_KEY) {
const c = readCache(key);
c[id] = { data, ts: Date.now() };
writeCache(c, key);
}
(function prune() {
const c1 = readCache(CACHE_KEY), c2 = readCache(NOMER_CACHE_KEY), c3 = readCache(PLATE_CACHE_KEY), c4 = readCache(PLATE_META_CACHE_KEY);
let ch1=false,ch2=false,ch3=false,ch4=false, now=Date.now();
for (const [k,v] of Object.entries(c1)) if (!v || !v.ts || (now - v.ts > MAX_AGE_MS)) { delete c1[k]; ch1=true; }
for (const [k,v] of Object.entries(c2)) if (!v || !v.ts || (now - v.ts > NOMER_MAX_AGE_MS)) { delete c2[k]; ch2=true; }
for (const [k,v] of Object.entries(c3)) if (!v || !v.ts || (now - v.ts > PLATE_MAX_AGE_MS)) { delete c3[k]; ch3=true; }
for (const [k,v] of Object.entries(c4)) if (!v || !v.ts || (now - v.ts > PLATE_MAX_AGE_MS)) { delete c4[k]; ch4=true; }
if (ch1) writeCache(c1, CACHE_KEY);
if (ch2) writeCache(c2, NOMER_CACHE_KEY);
if (ch3) writeCache(c3, PLATE_CACHE_KEY);
if (ch4) writeCache(c4, PLATE_META_CACHE_KEY);
})();
// ---------- Shared hover preview for avatars only ----------
const PM_PREVIEW_SIZE = 200, PM_PREVIEW_PAD = 12;
const pmPreviewEl = document.createElement("img");
pmPreviewEl.className = "pm-avatar-preview";
document.addEventListener("DOMContentLoaded", () => { if (!pmPreviewEl.isConnected) document.body.appendChild(pmPreviewEl); });
if (!pmPreviewEl.isConnected) document.body.appendChild(pmPreviewEl);
function pmMovePreview(e){const vw=window.innerWidth;let x=e.clientX-PM_PREVIEW_SIZE/2;let y=e.clientY-PM_PREVIEW_SIZE-PM_PREVIEW_PAD;if(x<4)x=4;if(x+PM_PREVIEW_SIZE>vw-4)x=vw-PM_PREVIEW_SIZE-4;if(y<4)y=e.clientY+PM_PREVIEW_PAD;pmPreviewEl.style.left=`${x}px`;pmPreviewEl.style.top=`${y}px`;}
function pmShowPreview(src,e,mode="cover"){pmPreviewEl.style.objectFit=(mode==="contain")?"contain":"cover";pmPreviewEl.src=src||"";pmPreviewEl.style.display="block";pmMovePreview(e);}
function pmHidePreview(){pmPreviewEl.style.display="none";pmPreviewEl.removeAttribute("src");}
function attachPreview(el,src,mode="cover"){el.addEventListener("mouseenter",(e)=>pmShowPreview(src,e,mode));el.addEventListener("mousemove",pmMovePreview);el.addEventListener("mouseleave",pmHidePreview);}
function attachSelfPreview(el,mode="cover"){el.addEventListener("mouseenter",(e)=>pmShowPreview(el.src,e,mode));el.addEventListener("mousemove",pmMovePreview);el.addEventListener("mouseleave",pmHidePreview);}
function attachPreviewLazy(el,getSrc,mode="cover"){el.addEventListener("mouseenter",(e)=>{const src=getSrc();if(src) pmShowPreview(src,e,mode);});el.addEventListener("mousemove",pmMovePreview);el.addEventListener("mouseleave",pmHidePreview);}
// ---------- Utils ----------
function absUrl(href){try{return new URL(href,location.origin).toString();}catch{return href;}}
function escapeHtml(s=""){return s.replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));}
const slug = s => s.normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
// ---------- Avatars ----------
async function fetchProfileAvatar(userPath){
const res=await fetch(absUrl(userPath),{credentials:"same-origin"}); if(!res.ok) throw new Error(`HTTP ${res.status}`);
const html=await res.text(); const doc=new DOMParser().parseFromString(html,"text/html");
const img=doc.querySelector(".profile-img[src]"); return img?absUrl(img.getAttribute("src")):null;
}
function addAvatarToRow(rowEl, avatarUrl){
if(!rowEl || rowEl.dataset[PROCESSED_FLAG]==="1") return;
const strong=rowEl.querySelector("strong"); const userLink=strong&&strong.querySelector('a[href^="/user"]'); if(!userLink) return;
if (strong.querySelector("img.pm-like-avatar")) { rowEl.dataset[PROCESSED_FLAG]="1"; return; }
const img=document.createElement("img"); img.className="pm-like-avatar"; img.width=20; img.height=20; img.alt=""; img.src=avatarUrl;
attachPreview(img, avatarUrl, "cover"); strong.insertBefore(img, userLink); rowEl.dataset[PROCESSED_FLAG]="1";
}
// ---------- Small task queue ----------
const queue=[]; let active=0, CONCURRENCY=3;
function enqueue(task){queue.push(task); runQueue();}
function runQueue(){while(active<CONCURRENCY && queue.length){active++; const fn=queue.shift(); Promise.resolve().then(fn).finally(()=>{active--; runQueue();});}}
function processRow(rowEl){
if(!rowEl || rowEl.dataset[PROCESSED_FLAG]==="1" || rowEl.dataset.pmAvatarPending==="1") return;
const userLink=rowEl.querySelector('strong > a[href^="/user"]'); if(!userLink) return;
const userId=(userLink.getAttribute("href").match(/\/user(\d+)\b/)||[])[1]; if(!userId) return;
const cached=getFromCache(userId); if(cached){ addAvatarToRow(rowEl,cached); return; }
rowEl.dataset.pmAvatarPending="1";
enqueue(async()=>{try{const url=await fetchProfileAvatar(userLink.getAttribute("href")); if(url){putInCache(userId,url); addAvatarToRow(rowEl,url);} else {rowEl.dataset[PROCESSED_FLAG]="1";}}catch{}finally{delete rowEl.dataset.pmAvatarPending;}});
}
function processAllAvatars(root=document){ root.querySelectorAll(LIKE_ITEM_SELECTOR).forEach(processRow); }
// ---------- Relative "x ago" ----------
function parseMoscowTime(str){const m=str.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/); if(!m) return null; const [,Y,Mo,D,H,Mi,S]=m.map(Number); const ms=Date.UTC(Y,Mo-1,D,H-3,Mi,S); return new Date(ms);}
function rel(date){ if(!date) return ""; const s=Math.max(0,(Date.now()-date.getTime())/1000); if(s<60) return `${Math.floor(s)}s ago`; const m=s/60; if(m<60) return `${Math.floor(m)}m ago`; const h=m/60; if(h<24) return `${Math.floor(h)}h ago`; const d=h/24; if(d<30) return `${Math.floor(d)}d ago`; return date.toLocaleDateString(); }
function processAllTimes(root=document){
root.querySelectorAll(`${LIKE_ITEM_SELECTOR} small`).forEach(el=>{
if(el.dataset.pmTimeDone==="1") return;
const t=el.textContent.trim(); const dt=parseMoscowTime(t);
if(dt){ el.textContent=rel(dt); el.dataset.pmTimeDone="1"; }
});
}
// ---------- PM panel helpers ----------
function ccToFlag(cc){ if(!cc||cc.length!==2) return ""; const A=0x1F1E6,a=cc.toUpperCase(); return String.fromCodePoint(A+(a.charCodeAt(0)-65), A+(a.charCodeAt(1)-65)); }
async function fetchNomerSmallPhoto(nomerHref){
const res=await fetch(absUrl(nomerHref),{credentials:"same-origin"}); if(!res.ok) throw new Error(`HTTP ${res.status}`);
const html=await res.text(); const m=html.match(/https?:\/\/img\d+\.platesmania\.com\/[^"'<>]+\/m\/\d+\.jpg/); if(!m) return null;
return m[0].replace(/\/m\//,"/s/");
}
// NEW: fetch plate PNG + first make/model + try logo existence
async function fetchNomerMeta(nomerHref){
const res = await fetch(absUrl(nomerHref), { credentials: "same-origin" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, "text/html");
// plate png (regex fastest)
const mPng = html.match(/https?:\/\/img\d+\.platesmania\.com\/\d{6}\/inf\/\d+[a-z0-9]*\.png/i);
const pngUrl = mPng ? mPng[0] : null;
// make/model from the h3 that contains /catalog links
let make = null, model = null, logoUrl = null;
const col = doc.querySelector('.col-md-6.col-sm-7');
const h3Candidates = col ? Array.from(col.querySelectorAll('h3')) : [];
const mmH3 =
h3Candidates.find(h => h.matches('.text-center.margin-bottom-10')) ||
h3Candidates.find(h => h.querySelector('a[href*="/catalog"]')) ||
null;
if (mmH3) {
const links = mmH3.querySelectorAll('a[href*="/catalog"]');
if (links[0]) make = (links[0].textContent || "").trim();
if (links[1]) model = (links[1].textContent || "").trim();
if (make) {
const s = slug(make).toLowerCase();
const testUrl = `https://raw.githubusercontent.com/filippofilip95/car-logos-dataset/refs/heads/master/logos/thumb/${s}.png`;
try {
const r = await fetch(testUrl, { method: "GET", cache: "force-cache" });
if (r.ok) logoUrl = testUrl;
} catch {}
}
}
return { pngUrl, make, model, logoUrl };
}
// ---------- Helper: inject logo/model into tooltip preview ----------
function ensureTooltipMeta(linkEl, meta){
if (!linkEl || !meta) return;
// Build HTML fragment for meta row
const safeMake = (meta.make || "").trim();
const safeModel = (meta.model || "").trim();
const textFallback = [safeMake, safeModel].filter(Boolean).join(" ");
const metaFragment = meta.logoUrl
? `<div class="pm-tt-meta"><img class="pm-tt-logo" alt="" src="${meta.logoUrl}"><span class="pm-tt-text">${escapeHtml(safeModel || "")}</span></div>`
: (textFallback ? `<div class="pm-tt-meta"><span class="pm-tt-text">${escapeHtml(textFallback)}</span></div>` : "");
if (!metaFragment) return;
// 1) Update the attribute used by PM's tooltip (keeps it consistent for future hovers)
const currentTitle = linkEl.getAttribute("data-original-title") || linkEl.getAttribute("title") || "";
if (currentTitle && !/pm-tt-meta/.test(currentTitle)) {
// Keep the existing <img ...> first, then our meta below
// If the title already contains an <img>, we just append our block.
const newTitle = `${currentTitle}${metaFragment}`;
linkEl.setAttribute("data-original-title", newTitle);
// some builds also mirror in title=""
if (linkEl.hasAttribute("title")) linkEl.setAttribute("title", newTitle);
}
// 2) If a live tooltip is open, patch its DOM immediately
const tipId = linkEl.getAttribute("aria-describedby");
if (tipId) {
const tip = document.getElementById(tipId);
const inner = tip && tip.querySelector(".tooltip-inner");
if (inner && !inner.querySelector(".pm-tt-meta")) {
// append a DOM node rather than nuking the existing image
const wrapper = document.createElement("div");
wrapper.className = "pm-tt-meta";
if (meta.logoUrl) {
const img = document.createElement("img");
img.className = "pm-tt-logo";
img.alt = "";
img.src = meta.logoUrl;
const span = document.createElement("span");
span.className = "pm-tt-text";
span.textContent = safeModel || "";
wrapper.appendChild(img);
wrapper.appendChild(span);
} else {
const span = document.createElement("span");
span.className = "pm-tt-text";
span.textContent = textFallback;
wrapper.appendChild(span);
}
inner.appendChild(wrapper);
}
}
}
// ---------- Likes list: plate → image; meta goes to tooltip (NOT next to link) ----------
function swapLinkTextWithPlateAndMeta(linkEl, meta){
if(!linkEl || !meta || !meta.pngUrl) return;
if (linkEl.querySelector("img.pm-plate")) return;
// keep native PM hover: DO NOT add our preview handlers here
linkEl.textContent = "";
const plateImg=document.createElement("img");
plateImg.className="pm-plate"; plateImg.alt=""; plateImg.src=meta.pngUrl;
linkEl.appendChild(plateImg);
// NEW: inject logo+model (or text fallback) into tooltip preview
ensureTooltipMeta(linkEl, meta);
linkEl.dataset.pmPlateDone="1";
}
function processPlateInRow(rowEl){
const link=rowEl.querySelector('a[href*="/nomer"]');
if(!link || link.dataset.pmPlateDone==="1" || rowEl.dataset.pmPlatePending==="1") return;
const href=link.getAttribute("href")||""; const id=(href.match(/nomer(\d+)/)||[])[1]; if(!id) return;
const cachedMeta = getMetaFromCache(id);
if (cachedMeta){ swapLinkTextWithPlateAndMeta(link, cachedMeta); return; }
// Fallback to old png-only cache (for users with existing cache)
const cachedPng = getFromCache(id, PLATE_CACHE_KEY, PLATE_MAX_AGE_MS);
if (cachedPng){
// still try to use any meta stored (if exists), else just insert plate
swapLinkTextWithPlateAndMeta(link, { pngUrl: cachedPng, make:null, model:null, logoUrl:null });
return;
}
rowEl.dataset.pmPlatePending="1";
enqueue(async()=>{
try{
const meta=await fetchNomerMeta(href);
if (meta.pngUrl){ putInCache(id, meta.pngUrl, PLATE_CACHE_KEY); }
putMetaInCache(id, meta);
swapLinkTextWithPlateAndMeta(link, meta);
}catch{}finally{ delete rowEl.dataset.pmPlatePending; }
});
}
function processAllPlates(root=document){ root.querySelectorAll(LIKE_ITEM_SELECTOR).forEach(processPlateInRow); }
// ---------- Comments preview + votes (PM notifications) ----------
async function fetchNomerComments(nomerHref, count=1){
const res=await fetch(absUrl(nomerHref),{credentials:"same-origin"}); if(!res.ok) throw new Error(`HTTP ${res.status}`);
const html=await res.text(); const doc=new DOMParser().parseFromString(html,"text/html");
const bodies=Array.from(doc.querySelectorAll('#ok .media.media-v2 .media-body'));
const items=bodies.map(b=>{
const userEl=b.querySelector('.media-heading strong a, .media-heading strong a span'); let user=""; if(userEl) user=(userEl.textContent||"").trim();
const contentEl=b.querySelector('div[id^="z"]'); const html=contentEl?contentEl.innerHTML.trim():"";
let cid=null; if(contentEl && contentEl.id && /^z\d+$/.test(contentEl.id)) cid=contentEl.id.slice(1);
else{ const smallA=b.closest('.media')?.querySelector('h4 small a[href^="#"]'); if(smallA) cid=smallA.getAttribute('href').replace('#',''); }
let votesText="0"; if(cid){ const valEl=b.querySelector(`#commentit-itogo-${cid}`); if(valEl) votesText=(valEl.textContent||"0").trim(); }
return { user, html, cid, votesText };
}).filter(x=>x.user && x.html && x.cid);
return items.slice(-count);
}
function insertAvatarBy(img,iEl,userLink){
const nodes=Array.from(iEl.childNodes); const byNode=nodes.find(n=>n.nodeType===3 && /\bby\s*$/i.test(n.textContent));
if(byNode && byNode.nextSibling===userLink) iEl.insertBefore(img,userLink);
else if(byNode){ if(byNode.nextSibling) iEl.insertBefore(img,byNode.nextSibling); else iEl.appendChild(img); }
else iEl.insertBefore(img,userLink);
}
function styleVoteValue(el,text){ el.classList.remove('pm-positive','pm-negative'); const t=(text||"").trim(); if(/^\+/.test(t)) el.classList.add('pm-positive'); else if(/^-/.test(t)) el.classList.add('pm-negative'); }
async function sendPreviewVote(commentId,valueEl,btnEl){
if(!commentId) return; btnEl.classList.add('pm-busy');
try{
const url=absUrl(`/newcom_1_baseu/func.php?g=1&n=${encodeURIComponent(commentId)}`);
const res=await fetch(url,{method:'GET',credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}});
const text=(await res.text()).trim(); valueEl.textContent=text || valueEl.textContent || "0"; styleVoteValue(valueEl,valueEl.textContent);
}catch{}finally{ setTimeout(()=>btnEl.classList.remove('pm-busy'),600); }
}
function wireUpPreviewVotes(root=document){
root.querySelectorAll('.pm-vote .pm-vote-btn[data-comment-id]:not([data-pm-wired])').forEach(btn=>{
btn.dataset.pmWired="1";
btn.addEventListener('click',(e)=>{ e.preventDefault(); const cid=btn.getAttribute('data-comment-id'); const valueEl=btn.parentElement?.querySelector('.pm-vote-value'); if(!cid || !valueEl) return; sendPreviewVote(cid,valueEl,btn); });
});
}
function processPmAlert(alertEl){
if(!alertEl || alertEl.dataset.pmPanelDone==="1" || alertEl.dataset.pmPanelPending==="1") return;
const strong=alertEl.querySelector(".overflow-h > strong"); const titleLink=strong && strong.querySelector('a[href*="/nomer"]');
if(!strong || !titleLink){ alertEl.dataset.pmPanelDone="1"; return; }
const path=new URL(titleLink.getAttribute("href"),location.origin).pathname;
const pathParts=path.split("/").filter(Boolean); const cc=pathParts[0]||""; const nomerId=(path.match(/nomer(\d+)/)||[])[1];
const flag=ccToFlag(cc);
if(flag && titleLink && !titleLink.dataset.pmFlagged){ titleLink.textContent=`${flag} ${titleLink.textContent.trim()}`; titleLink.dataset.pmFlagged="1"; }
const flagImg=alertEl.querySelector("img.rounded-x, .alert img");
if(flagImg && nomerId){
const cached=getFromCache(nomerId,NOMER_CACHE_KEY,NOMER_MAX_AGE_MS);
if(cached){ flagImg.src=cached; flagImg.classList.add("pm-nomer-thumb"); attachPreviewLazy(flagImg,()=> (flagImg.src?flagImg.src.replace(/\/s\//,"/m/"):""),"contain"); }
else{
alertEl.dataset.pmPanelPending="1";
enqueue(async()=>{try{const photoUrl=await fetchNomerSmallPhoto(titleLink.getAttribute("href")); if(photoUrl){putInCache(nomerId,photoUrl,NOMER_CACHE_KEY); flagImg.src=photoUrl; flagImg.classList.add("pm-nomer-thumb"); attachPreviewLazy(flagImg,()=>photoUrl.replace(/\/s\//,"/m/"),"contain"); }}catch{}finally{ delete alertEl.dataset.pmPanelPending; }});
}
}
const iEl=alertEl.querySelector("i");
if(iEl){
const userLink=iEl.querySelector('a[href^="/user"]');
if(userLink && !iEl.querySelector("img.pm-like-avatar")){
const userHref=userLink.getAttribute("href"); const userId=(userHref.match(/\/user(\d+)\b/)||[])[1];
const addAvatar=(url)=>{ const img=document.createElement("img"); img.className="pm-like-avatar"; img.width=20; img.height=20; img.alt=""; img.src=url; attachPreview(img,url,"cover"); insertAvatarBy(img,iEl,userLink); };
const cached=userId && getFromCache(userId); if(cached) addAvatar(cached);
else if(userId){ enqueue(async()=>{try{const url=await fetchProfileAvatar(userHref); if(url){putInCache(userId,url); addAvatar(url);}}catch{} }); }
}
if(!iEl.dataset.pmRelTimeDone){
const time=iEl.getAttribute("data-original-title")||""; const mDate=iEl.textContent && iEl.textContent.match(/\((\d{4}-\d{2}-\d{2})\)/);
const dateStr=mDate?mDate[1]:""; const dt=(time && dateStr)?parseMoscowTime(`${dateStr} ${time}`):null;
if(dt){ iEl.innerHTML=iEl.innerHTML.replace(/\(\d{4}-\d{2}-\d{2}\)/,(`(${rel(dt)})`)); iEl.dataset.pmRelTimeDone="1"; }
}
}
// Replace "New comments..." with previews (+ upvote)
const pDesc=alertEl.querySelector(".overflow-h > p");
const plusEm=strong && strong.querySelector("small.pull-right em");
let count=1; if(plusEm){ const m=plusEm.textContent && plusEm.textContent.match(/\+(\d+)/); if(m) count=Math.max(1,parseInt(m[1],10)); }
if(pDesc && titleLink && !pDesc.dataset.pmCommentsLoaded && !pDesc.dataset.pmCommentsPending){
pDesc.dataset.pmCommentsPending="1";
enqueue(async()=>{try{
const comments=await fetchNomerComments(titleLink.getAttribute("href"),count);
if(comments && comments.length){
const html=comments.map(c=>{
const safeUser=escapeHtml(c.user); const votes=(c.votesText||"0").trim();
const positiveClass=/^\+/.test(votes)?" pm-positive":(/^-/.test(votes)?" pm-negative":"");
return `<div class="pm-comment-preview"><span class="pm-comment-text"><b>${safeUser}:</b> ${c.html}</span><span class="pm-vote" data-pm-cid="${c.cid}"><i class="fa fa-plus-circle pm-vote-btn" title="Upvote" data-comment-id="${c.cid}"></i><span class="pm-vote-value${positiveClass}" id="pm-commentit-itogo-${c.cid}">${escapeHtml(votes)}</span></span></div>`;
}).join("");
pDesc.innerHTML=html; wireUpPreviewVotes(pDesc);
}
}catch{}finally{ delete pDesc.dataset.pmCommentsPending; pDesc.dataset.pmCommentsLoaded="1"; }});
}
alertEl.dataset.pmPanelDone="1";
}
function processPmPanel(root=document){ const alerts=root.querySelectorAll('.panel .alert.alert-blocks, #scrollbar3 .alert.alert-blocks'); alerts.forEach(processPmAlert); }
// ---------- Observe ----------
function observe(){
const container=document.querySelector(CONTAINER_SELECTOR)||document.body;
const mo=new MutationObserver(muts=>{muts.forEach(m=>m.addedNodes.forEach(n=>{ if(!(n instanceof HTMLElement)) return;
processAllAvatars(n); processAllTimes(n); processAllPlates(n); processPmPanel(n); wireUpPreviewVotes(n);
}));});
mo.observe(container,{childList:true,subtree:true});
const pmPanel=document.querySelector('#scrollbar3')||document.querySelector('.panel .panel-title i.fa-send')?.closest('.panel');
if(pmPanel){
const mo2=new MutationObserver(muts=>{muts.forEach(m=>m.addedNodes.forEach(n=>{ if(n instanceof HTMLElement){ processPmPanel(n); wireUpPreviewVotes(n); }}));});
mo2.observe(pmPanel,{childList:true,subtree:true});
}
}
// ---------- Kickoff ----------
processAllAvatars(document);
processAllTimes(document);
processAllPlates(document);
processPmPanel(document);
wireUpPreviewVotes(document);
observe();
setInterval(()=>{ processAllAvatars(document); processAllTimes(document); processAllPlates(document); processPmPanel(document); wireUpPreviewVotes(document); },1500);
})();