// ==UserScript== // @name Blackline+ // @version 4.0.0 // @icon https://rugplay.com/favicon.ico // @description Blackline+ — Advanced Rugplay intelligence suite. Live analytics, rugpull AI scoring, bot detection, portfolio tracking, wallet analysis, and more. // @author Blackline+ // @match https://rugplay.com/* // @match https://xprismplay.dpdns.org/* // @grant GM_info // @grant GM_addStyle // @grant GM_setClipboard // @connect rugplay.com // @connect self // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ───────────────────────────────────────────────────────────────────────── // BLACKLINE+ v4.0.0 // Ground-up redesign. Clean architecture, intentional UI. // ───────────────────────────────────────────────────────────────────────── const BL_VERSION = '4.0.0'; // ─── Storage ───────────────────────────────────────────────────────────── const K = { history: 'bl4_history', settings: 'bl4_settings', tags: 'bl4_tags', watchlist: 'bl4_watchlist', notes: 'bl4_notes', hidden: 'bl4_hidden', reports: 'bl4_reports', votes: 'bl4_votes', session: 'bl4_session', // persisted session state (open panel, active tab) }; const MAX_HISTORY = 5000; const store = { get(k, fb = null) { try { const r = localStorage.getItem(k); return r !== null ? JSON.parse(r) : fb; } catch { return fb; } }, set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch { return false; } }, del(k) { try { localStorage.removeItem(k); } catch {} }, }; // ─── Session State Persistence ──────────────────────────────────────────── // Survives page refresh. Keeps track of which panel was open, feed state, etc. const session = { _s: null, _defaults: { feedOpen: false, centerTab: 'overview', searchOpen: false }, all() { if (!this._s) this._s = { ...this._defaults, ...store.get(K.session, {}) }; return this._s; }, get(k) { return this.all()[k]; }, set(k, v) { this.all()[k] = v; store.set(K.session, this._s); }, }; // ─── Configuration ──────────────────────────────────────────────────────── const DEFAULTS = { liveToasts: true, toastBuys: true, toastSells: true, rugAlerts: true, rugThreshold: 70, watchlistAlerts: true, volumeSpikeAlerts: true, volumeSpikeMultiple: 3, toastDuration: 4500, notifBadges: true, clickableRows: true, highlightWatched: true, showMiniChart: true, autoRefreshCoin: true, darkEnhance: true, compactMode: false, showPresetBtns: true, showQuickSell: true, showBotBadges: true, buyPresets: [5, 10, 50, 100], defaultSlippage: 1, adBlocker: true, livePortfolioRefresh: true, appearOffline: false, stickyPortfolio: false, showRugpullerBadge: true, hkCenter: '`', hkFeed: 'f', hkSearch: 's', }; const cfg = { _c: null, all() { if (!this._c) this._c = { ...DEFAULTS, ...store.get(K.settings, {}) }; return this._c; }, get(k) { return this.all()[k]; }, set(k, v) { this.all()[k] = v; store.set(K.settings, this._c); }, }; // ─── Trade History ──────────────────────────────────────────────────────── const tradeDB = { _c: null, all() { if (!this._c) this._c = store.get(K.history, []); return this._c; }, flush() { this._c = this._c.slice(-MAX_HISTORY); store.set(K.history, this._c); }, add(t) { if (!t?.timestamp) return false; const id = `${t.userId}_${t.timestamp}_${t.coinSymbol}_${t.type}`; if (this.all().some(x => x.id === id)) return false; this._c.push({ id, userId: String(t.userId || ''), username: t.username || '', timestamp: Number(t.timestamp), coinSymbol: (t.coinSymbol || '').toUpperCase(), type: (t.type || '').toUpperCase(), price: parseFloat(t.price) || 0, quantity: parseFloat(t.amount) || 0, totalValue: parseFloat(t.totalValue) || 0, }); this.flush(); return true; }, forUser(u) { return this.all().filter(t => t.username?.toLowerCase() === u.toLowerCase()).sort((a,b) => b.timestamp - a.timestamp); }, forCoin(sym) { return this.all().filter(t => t.coinSymbol === sym.toUpperCase()).sort((a,b) => b.timestamp - a.timestamp); }, stats() { const a = this.all(); const buys = a.filter(t => t.type === 'BUY'); const sells = a.filter(t => t.type === 'SELL'); return { total: a.length, buys: buys.length, sells: sells.length, volume: a.reduce((s, t) => s + t.totalValue, 0), coins: new Set(a.map(t => t.coinSymbol)).size, users: new Set(a.map(t => t.username).filter(Boolean)).size, }; }, coinLeaderboard(n = 20) { const m = {}; this.all().forEach(t => { if (!m[t.coinSymbol]) m[t.coinSymbol] = { trades: 0, volume: 0, buys: 0, sells: 0 }; m[t.coinSymbol].trades++; m[t.coinSymbol].volume += t.totalValue; if (t.type === 'BUY') m[t.coinSymbol].buys++; else m[t.coinSymbol].sells++; }); return Object.entries(m).sort((a,b) => b[1].volume - a[1].volume).slice(0,n) .map(([sym, d]) => ({ sym, ...d })); }, userLeaderboard(n = 20) { const m = {}; this.all().forEach(t => { if (!t.username) return; if (!m[t.username]) m[t.username] = { trades: 0, volume: 0 }; m[t.username].trades++; m[t.username].volume += t.totalValue; }); return Object.entries(m).sort((a,b) => b[1].trades - a[1].trades).slice(0,n) .map(([u, d]) => ({ username: u, ...d })); }, recentVolume(sym, windowMs = 60000) { const cutoff = Date.now() - windowMs; return this.forCoin(sym).filter(t => t.timestamp > cutoff).reduce((s,t) => s+t.totalValue, 0); }, clear() { this._c = []; store.del(K.history); }, }; // ─── Tag DB ─────────────────────────────────────────────────────────────── const TAG_PRESETS = { DEV: { emoji:'💻', label:'Dev', bg:'#312e81', text:'#a5b4fc' }, VIP: { emoji:'👑', label:'VIP', bg:'#78350f', text:'#fcd34d' }, FRIEND: { emoji:'🤝', label:'Friend', bg:'#14532d', text:'#86efac' }, RUGPULLER: { emoji:'🚩', label:'Rugpuller', bg:'#7f1d1d', text:'#fca5a5' }, WHALE: { emoji:'🐋', label:'Whale', bg:'#164e63', text:'#67e8f9' }, SCAMMER: { emoji:'⛔', label:'Scammer', bg:'#450a0a', text:'#f87171' }, TRUSTED: { emoji:'✅', label:'Trusted', bg:'#052e16', text:'#4ade80' }, BOT: { emoji:'🤖', label:'Bot', bg:'#1e1b4b', text:'#a5b4fc' }, DEGEN: { emoji:'🎰', label:'Degen', bg:'#4a1942', text:'#e879f9' }, INSIDER: { emoji:'🔮', label:'Insider', bg:'#1e3a5f', text:'#7dd3fc' }, }; const tagDB = { all() { return store.get(K.tags, {}); }, get(u) { const d = this.all(); return d[u?.toLowerCase()] || null; }, set(u, tag, orig) { const d = this.all(); d[u.toLowerCase()] = { tag, orig: orig || u }; store.set(K.tags, d); }, remove(u) { const d = this.all(); delete d[u.toLowerCase()]; store.set(K.tags, d); }, count() { return Object.keys(this.all()).length; }, }; // ─── Watchlist / Notes / Hidden ─────────────────────────────────────────── const wlDB = { all() { return store.get(K.watchlist, []); }, has(s) { return this.all().includes(s?.toUpperCase()); }, add(s) { const l = this.all(); const u = s.toUpperCase(); if (!l.includes(u)) { l.push(u); store.set(K.watchlist, l); } }, remove(s){ const l = this.all().filter(x => x !== s?.toUpperCase()); store.set(K.watchlist, l); }, }; const notesDB = { all() { return store.get(K.notes, {}); }, get(s) { return this.all()[s?.toUpperCase()] || ''; }, set(s, txt) { const d = this.all(); d[s.toUpperCase()] = txt; store.set(K.notes, d); }, remove(s) { const d = this.all(); delete d[s.toUpperCase()]; store.set(K.notes, d); }, }; const hiddenDB = { all() { return store.get(K.hidden, []); }, has(s) { return this.all().includes(s?.toUpperCase()); }, add(s) { const l = this.all(); const u = s.toUpperCase(); if (!l.includes(u)) { l.push(u); store.set(K.hidden, l); } }, remove(s){ const l = this.all().filter(x => x !== s?.toUpperCase()); store.set(K.hidden, l); }, }; // ─── Community Reports ──────────────────────────────────────────────────── const communityReports = { all() { return store.get(K.reports, []); }, add(r) { const all = this.all(); const id = `${Date.now()}_${Math.random().toString(36).slice(2,7)}`; all.unshift({ id, username: r.username, coinSymbol: (r.coinSymbol||'').toUpperCase(), description: r.description, timestamp: Date.now(), likes: 0, dislikes: 0 }); store.set(K.reports, all.slice(0, 200)); return id; }, vote(id, type) { const votes = store.get(K.votes, {}); if (votes[id]) return false; const all = this.all(); const rep = all.find(r => r.id === id); if (!rep) return false; if (type === 'like') rep.likes++; else rep.dislikes++; votes[id] = type; store.set(K.reports, all); store.set(K.votes, votes); return true; }, getVote(id) { return store.get(K.votes, {})[id] || null; }, }; // ─── Portfolio Estimator ────────────────────────────────────────────────── const portfolio = { compute(username) { const trades = tradeDB.forUser(username); const m = {}; trades.forEach(t => { if (!m[t.coinSymbol]) m[t.coinSymbol] = { sym: t.coinSymbol, qty: 0, spent: 0, earned: 0, buys: 0, sells: 0 }; if (t.type === 'BUY') { m[t.coinSymbol].qty += t.quantity; m[t.coinSymbol].spent += t.totalValue; m[t.coinSymbol].buys++; } if (t.type === 'SELL') { m[t.coinSymbol].qty -= t.quantity; m[t.coinSymbol].earned += t.totalValue; m[t.coinSymbol].sells++; } }); return Object.values(m).map(h => { const pnl = h.earned - h.spent; const avgBuy = h.buys > 0 ? h.spent / h.buys : 0; return { ...h, qty: Math.max(0, h.qty), pnl, estValue: Math.max(0, h.qty) * avgBuy }; }).sort((a, b) => b.estValue - a.estValue); }, }; // ─── Bot Detector ───────────────────────────────────────────────────────── const botDetector = { analyze(username) { const trades = tradeDB.forUser(username); if (trades.length < 4) return null; let score = 0; // Interval regularity const intervals = []; for (let i=1;i 2) { const avg = intervals.reduce((a,b)=>a+b,0)/intervals.length; const dev = Math.sqrt(intervals.reduce((s,v)=>s+(v-avg)**2,0)/intervals.length); if (avg > 0 && dev/avg < 0.15) score += 35; else if (avg > 0 && dev/avg < 0.30) score += 18; } // Directional uniformity const buys = trades.filter(t=>t.type==='BUY').length, sells = trades.length - buys; const ratio = Math.max(buys, sells) / trades.length; if (ratio > 0.95) score += 25; else if (ratio > 0.85) score += 14; // Uniform sizes const vals = trades.map(t=>t.totalValue).filter(v=>v>0); if (vals.length > 2) { const avg = vals.reduce((a,b)=>a+b,0)/vals.length; const cv = Math.sqrt(vals.reduce((s,v)=>s+(v-avg)**2,0)/vals.length) / avg; if (cv < 0.05) score += 25; else if (cv < 0.15) score += 12; } // Multi-coin speed const recent = trades.filter(t => Date.now()-t.timestamp < 300000); const coins = new Set(recent.map(t=>t.coinSymbol)); if (coins.size >= 5 && recent.length >= 8) score += 15; if (score < 30) return null; if (score >= 70) return { score, label: 'High-Confidence Bot', color: '#f87171', tier: 'high' }; if (score >= 45) return { score, label: 'Likely Bot', color: '#fbbf24', tier: 'mid' }; return { score, label: 'Bot Activity', color: '#94a3b8', tier: 'low' }; }, autoTag(username) { if (!username || tagDB.get(username)) return; const r = this.analyze(username); if (r && r.tier === 'high') tagDB.set(username, 'BOT', username); }, }; // ─── Rug Scorer ─────────────────────────────────────────────────────────── const rugScorer = { score(sym) { const trades = tradeDB.forCoin(sym); if (trades.length < 3) return null; let score = 0; const buys = trades.filter(t=>t.type==='BUY'), sells = trades.filter(t=>t.type==='SELL'); const sellRatio = sells.length / trades.length; if (sellRatio > 0.80) score += 30; else if (sellRatio > 0.65) score += 18; else if (sellRatio > 0.50) score += 8; const sellVol = sells.reduce((s,t)=>s+t.totalValue,0); const buyVol = buys.reduce((s,t)=>s+t.totalValue,0); if (sellVol > 0 && sellVol/(sellVol+buyVol) > 0.75) score += 25; else if (sellVol/(sellVol+buyVol+.01) > 0.60) score += 12; const recent10 = trades.slice(0, Math.min(10, trades.length)); const recentSR = recent10.filter(t=>t.type==='SELL').length / recent10.length; if (recentSR > 0.80) score += 15; const whaleDumps = sells.filter(t=>t.totalValue > 500).length; if (whaleDumps > 2) score += 15; else if (whaleDumps > 0) score += 6; const recentTs = sells.slice(0,5).map(t=>t.timestamp); if (recentTs.length >= 3) { const span = Math.max(...recentTs) - Math.min(...recentTs); if (span < 30000) score += 15; } return Math.min(100, score); }, label(score) { if (score >= 75) return { text: 'Critical Risk', color: '#f87171', bg: 'rgba(248,113,113,.12)' }; if (score >= 50) return { text: 'High Risk', color: '#fb923c', bg: 'rgba(251,146,60,.12)' }; if (score >= 30) return { text: 'Medium Risk', color: '#fbbf24', bg: 'rgba(251,191,36,.12)' }; return { text: 'Low Risk', color: '#4ade80', bg: 'rgba(74,222,128,.12)' }; }, sparkline(sym, w=260, h=40) { const trades = tradeDB.forCoin(sym).slice().reverse().slice(-40); if (trades.length < 3) return ''; const vals = trades.map(t => t.totalValue); const min = Math.min(...vals), max = Math.max(...vals); const range = max - min || 1; const pts = vals.map((v,i) => { const x = (i/(vals.length-1))*w; const y = h - ((v-min)/range)*(h-4) - 2; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); return ``; }, }; // ─── Volume Spike Tracker ───────────────────────────────────────────────── const spikeTracker = { check(sym) { const recent = tradeDB.recentVolume(sym, 60000); const baseline= tradeDB.recentVolume(sym, 600000) / 10; if (baseline < 1) return null; const mult = recent / baseline; return mult >= cfg.get('volumeSpikeMultiple') ? mult.toFixed(1) : null; }, }; // ─── Utility ────────────────────────────────────────────────────────────── const COLORS = ['#818cf8','#34d399','#fb923c','#f472b6','#facc15','#60a5fa','#a78bfa','#4ade80']; const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel)); const sleep = ms => new Promise(r => setTimeout(r, ms)); const U = { esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }, fmtUSD(n) { const v = parseFloat(n) || 0; if (v >= 1e9) return `$${(v/1e9).toFixed(2)}B`; if (v >= 1e6) return `$${(v/1e6).toFixed(2)}M`; if (v >= 1e3) return `$${(v/1e3).toFixed(1)}K`; return `$${v.toFixed(2)}`; }, fmtNum(n) { const v = parseFloat(n) || 0; if (v >= 1e6) return `${(v/1e6).toFixed(2)}M`; if (v >= 1e3) return `${(v/1e3).toFixed(1)}K`; return v.toFixed(v < 1 ? 4 : 2); }, fmtDate(ts) { if (!ts) return '—'; return new Date(Number(ts)).toLocaleString('en-US', { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit', hour12:true }); }, timeAgo(ts) { const s = Math.floor((Date.now() - Number(ts)) / 1000); if (s < 5) return 'just now'; if (s < 60) return `${s}s ago`; const m = Math.floor(s/60); if (m < 60) return `${m}m ago`; const h = Math.floor(m/60); if (h < 24) return `${h}h ago`; return `${Math.floor(h/24)}d ago`; }, isCoin() { return location.pathname.startsWith('/coin/'); }, isUser() { return location.pathname.startsWith('/user/'); }, getCoin() { const m = location.pathname.match(/\/coin\/([^/?]+)/); return m ? m[1].toUpperCase() : null; }, getPageUser(){ const m = document.querySelector('meta[property="og:title"]')?.content?.match(/\(@([^)]+)\)/); return m?.[1] || null; }, getLoggedInUser() { return new Promise(res => { const check = () => { const el = document.querySelector('#bits-c1 .truncate.text-xs') || document.querySelector('[data-slot="sidebar-footer"] span.truncate.text-xs'); if (el?.textContent?.trim()) return res(el.textContent.replace('@','').trim()); setTimeout(check, 120); }; check(); }); }, copy(text) { try { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); return true; } } catch {} try { navigator.clipboard?.writeText(text); return true; } catch {} const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); return true; }, copyToast(text, msg = 'Copied!') { this.copy(text); toaster.show({ type:'copy', title: msg }); }, debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }, paginate(container, { current, total }, onClick) { container.innerHTML = ''; if (!total || total <= 1) return; const mk = (label, page, disabled, active) => { const b = document.createElement('button'); b.className = `bl-pgbtn${active?' bl-pga':disabled?' bl-pgd':''}`; b.textContent = label; if (!disabled && !active) b.addEventListener('click', () => onClick(page)); container.appendChild(b); }; mk('‹', current-1, current === 1, false); const pages = new Set([1, total, current, current-1, current+1].filter(p => p >= 1 && p <= total)); let prev = 0; [...pages].sort((a,b)=>a-b).forEach(p => { if (p > prev + 1) { const e = document.createElement('span'); e.className='bl-pgdots'; e.textContent='…'; container.appendChild(e); } mk(p, p, false, p === current); prev = p; }); mk('›', current+1, current >= total, false); }, }; // ─── Design System / CSS ────────────────────────────────────────────────── GM_addStyle(` /* ── Token system ── */ :root { --bl-bg0: #06060a; --bl-bg1: #0c0c14; --bl-bg2: #111118; --bl-bg3: #18181f; --bl-border: rgba(255,255,255,.07); --bl-border-hi: rgba(255,255,255,.13); --bl-accent: #6366f1; --bl-accent-dim: rgba(99,102,241,.18); --bl-accent-border: rgba(99,102,241,.35); --bl-green: #22c55e; --bl-red: #ef4444; --bl-yellow: #f59e0b; --bl-text0: #f1f5f9; --bl-text1: #94a3b8; --bl-text2: #475569; --bl-mono: ui-monospace, SFMono-Regular, 'Cascadia Code', monospace; --bl-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif; --bl-r-sm: 6px; --bl-r-md: 10px; --bl-r-lg: 14px; --bl-r-xl: 18px; --bl-shadow: 0 4px 24px rgba(0,0,0,.45); --bl-shadow-lg: 0 16px 64px rgba(0,0,0,.7); --bl-z-feed: 9000; --bl-z-search: 9100; --bl-z-modal: 9200; --bl-z-toast: 9300; --bl-z-status: 8900; } /* ── Toast container ── */ #bl-toasts { position: fixed; bottom: 24px; right: 24px; z-index: var(--bl-z-toast); display: flex; flex-direction: column; gap: 8px; pointer-events: none; width: 320px; } .bl-toast { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; background: var(--bl-bg2); border: 1px solid var(--bl-border-hi); border-radius: var(--bl-r-lg); box-shadow: var(--bl-shadow); pointer-events: all; font-family: var(--bl-sans); transform: translateX(calc(100% + 28px)); opacity: 0; transition: transform .22s cubic-bezier(.22,1,.36,1), opacity .22s ease; position: relative; overflow: hidden; } .bl-toast::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--bl-accent); border-radius: 2px 0 0 2px; } .bl-toast.bl-t-buy::before { background: var(--bl-green); } .bl-toast.bl-t-sell::before { background: var(--bl-red); } .bl-toast.bl-t-warn::before { background: var(--bl-yellow); } .bl-toast.bl-t-alert::before { background: var(--bl-accent); } .bl-toast.in { transform: translateX(0); opacity: 1; } .bl-toast.out { transform: translateX(calc(100% + 28px)); opacity: 0; } .bl-t-icon { font-size: 15px; flex-shrink: 0; margin-top: 1px; } .bl-t-content { flex: 1; min-width: 0; } .bl-t-title { font-size: 13px; font-weight: 600; color: var(--bl-text0); line-height: 1.3; } .bl-t-body { font-size: 12px; color: var(--bl-text1); margin-top: 2px; } .bl-t-close { background: none; border: none; color: var(--bl-text2); cursor: pointer; font-size: 14px; line-height: 1; padding: 0; flex-shrink: 0; transition: color .12s; } .bl-t-close:hover { color: var(--bl-text0); } /* ── Modal system ── */ .bl-overlay { position: fixed; inset: 0; z-index: var(--bl-z-modal); background: rgba(0,0,0,.72); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; padding: 20px; opacity: 0; transition: opacity .18s ease; } .bl-overlay.in { opacity: 1; } .bl-overlay.out { opacity: 0; } .bl-modal { background: var(--bl-bg1); border: 1px solid var(--bl-border-hi); border-radius: var(--bl-r-xl); width: 100%; max-height: 88vh; display: flex; flex-direction: column; box-shadow: var(--bl-shadow-lg); transform: translateY(12px) scale(.98); transition: transform .22s cubic-bezier(.22,1,.36,1); } .bl-overlay.in .bl-modal { transform: translateY(0) scale(1); } .bl-m-head { display: flex; align-items: center; padding: 18px 20px 16px; border-bottom: 1px solid var(--bl-border); flex-shrink: 0; gap: 12px; } .bl-m-title { font-size: 15px; font-weight: 700; color: var(--bl-text0); flex: 1; font-family: var(--bl-sans); } .bl-m-close { background: none; border: none; color: var(--bl-text2); cursor: pointer; width: 28px; height: 28px; border-radius: var(--bl-r-sm); display: flex; align-items: center; justify-content: center; font-size: 16px; transition: background .12s, color .12s; flex-shrink: 0; } .bl-m-close:hover { background: rgba(255,255,255,.06); color: var(--bl-text0); } .bl-m-body { overflow-y: auto; flex: 1; padding: 20px; font-family: var(--bl-sans); scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.08) transparent; } .bl-m-body::-webkit-scrollbar { width: 5px; } .bl-m-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 4px; } /* ── Forms ── */ .bl-input, .bl-textarea, .bl-select { background: rgba(255,255,255,.04); border: 1px solid var(--bl-border); border-radius: var(--bl-r-md); color: var(--bl-text0); padding: 8px 12px; font-size: 13px; font-family: var(--bl-sans); outline: none; transition: border-color .15s; box-sizing: border-box; } .bl-input:focus, .bl-textarea:focus, .bl-select:focus { border-color: var(--bl-accent-border); } .bl-input::placeholder, .bl-textarea::placeholder { color: var(--bl-text2); } .bl-input-full, .bl-textarea-full { width: 100%; } .bl-textarea { resize: vertical; min-height: 72px; } .bl-select { cursor: pointer; } .bl-select option { background: #0d0d18; color: var(--bl-text0); } /* ── Toggle ── */ .bl-toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; } .bl-toggle input { position: absolute; opacity: 0; width: 0; height: 0; } .bl-toggle-track { position: absolute; inset: 0; background: var(--bl-bg3); border: 1px solid var(--bl-border); border-radius: 20px; cursor: pointer; transition: background .18s, border-color .18s; } .bl-toggle-track::after { content: ''; position: absolute; width: 14px; height: 14px; left: 2px; top: 2px; background: var(--bl-text2); border-radius: 50%; transition: transform .18s cubic-bezier(.22,1,.36,1), background .18s; } .bl-toggle input:checked ~ .bl-toggle-track { background: var(--bl-accent-dim); border-color: var(--bl-accent-border); } .bl-toggle input:checked ~ .bl-toggle-track::after { transform: translateX(16px); background: var(--bl-accent); } /* ── Buttons ── */ .bl-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 14px; border-radius: var(--bl-r-md); font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: background .14s, transform .1s; font-family: var(--bl-sans); white-space: nowrap; text-decoration: none; line-height: 1.2; } .bl-btn:active { transform: scale(.97); } .bl-btn-primary { background: var(--bl-accent); color: #fff; } .bl-btn-primary:hover { background: #5254cc; } .bl-btn-ghost { background: rgba(255,255,255,.05); color: var(--bl-text1); border: 1px solid var(--bl-border); } .bl-btn-ghost:hover { background: rgba(255,255,255,.09); color: var(--bl-text0); } .bl-btn-danger { background: rgba(239,68,68,.1); color: var(--bl-red); border: 1px solid rgba(239,68,68,.18); } .bl-btn-danger:hover { background: rgba(239,68,68,.18); } .bl-btn-success { background: rgba(34,197,94,.1); color: var(--bl-green); border: 1px solid rgba(34,197,94,.2); } .bl-btn-success:hover { background: rgba(34,197,94,.18); } .bl-btn-sm { padding: 5px 10px; font-size: 12px; } .bl-btn-xs { padding: 3px 8px; font-size: 11px; } .bl-btn-icon { padding: 6px; width: 30px; height: 30px; border-radius: var(--bl-r-sm); } /* ── Tables ── */ .bl-table { width: 100%; border-collapse: collapse; font-size: 13px; font-family: var(--bl-sans); } .bl-table th { padding: 8px 12px; text-align: left; color: var(--bl-text2); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid var(--bl-border); white-space: nowrap; } .bl-table td { padding: 10px 12px; color: var(--bl-text0); border-bottom: 1px solid rgba(255,255,255,.025); vertical-align: middle; } .bl-table tr:last-child td { border-bottom: none; } .bl-table tbody tr:hover td { background: rgba(255,255,255,.02); } .bl-table td.mono { font-family: var(--bl-mono); } .bl-badge-buy { display:inline-block; padding:2px 8px; border-radius:4px; font-size:10px; font-weight:700; background:rgba(34,197,94,.12); color:var(--bl-green); border:1px solid rgba(34,197,94,.25); } .bl-badge-sell { display:inline-block; padding:2px 8px; border-radius:4px; font-size:10px; font-weight:700; background:rgba(239,68,68,.12); color:var(--bl-red); border:1px solid rgba(239,68,68,.25); } /* ── Tag chip ── */ .bl-tag { display: inline-flex; align-items: center; gap: 3px; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; vertical-align: middle; margin-left: 5px; line-height: 1.4; } /* ── Pagination ── */ .bl-pg { display: flex; justify-content: center; align-items: center; gap: 4px; padding: 14px 0 0; flex-wrap: wrap; } .bl-pgbtn { display: inline-flex; align-items: center; justify-content: center; min-width: 32px; height: 30px; padding: 0 8px; border: 1px solid var(--bl-border); border-radius: var(--bl-r-sm); background: none; color: var(--bl-text1); font-size: 12px; cursor: pointer; transition: all .12s; font-family: var(--bl-sans); } .bl-pgbtn:hover:not(.bl-pga):not(.bl-pgd) { background: rgba(255,255,255,.06); color: var(--bl-text0); } .bl-pga { background: var(--bl-accent); color: #fff; border-color: var(--bl-accent); cursor: default; } .bl-pgd { opacity: .3; cursor: default; } .bl-pgdots { color: var(--bl-text2); font-size: 12px; padding: 0 4px; } /* ── Stats grid ── */ .bl-statgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 12px; margin: 0; } .bl-statcard { background: var(--bl-bg2); border: 1px solid var(--bl-border); border-radius: var(--bl-r-lg); padding: 16px; display: flex; flex-direction: column; gap: 4px; } .bl-stat-val { font-size: 22px; font-weight: 800; color: var(--bl-text0); font-family: var(--bl-mono); line-height: 1.1; } .bl-stat-lbl { font-size: 11px; color: var(--bl-text2); text-transform: uppercase; letter-spacing: .5px; font-weight: 600; } /* ── Section row (setting row) ── */ .bl-setting-row { display: flex; align-items: center; justify-content: space-between; padding: 13px 0; border-bottom: 1px solid var(--bl-border); gap: 16px; } .bl-setting-row:last-child { border-bottom: none; } .bl-setting-label { font-size: 13px; font-weight: 500; color: var(--bl-text0); font-family: var(--bl-sans); } .bl-setting-desc { font-size: 11px; color: var(--bl-text2); margin-top: 2px; line-height: 1.4; } /* ── List row ── */ .bl-list-row { display: flex; align-items: center; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--bl-border); gap: 10px; font-size: 13px; color: var(--bl-text0); font-family: var(--bl-sans); } .bl-list-row:last-child { border-bottom: none; } /* ── Risk bar ── */ .bl-risk-wrap { display: flex; align-items: center; gap: 10px; margin: 10px 0 14px; } .bl-risk-label { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border-radius: 6px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; white-space: nowrap; } .bl-risk-track { flex: 1; height: 4px; background: rgba(255,255,255,.06); border-radius: 2px; overflow: hidden; } .bl-risk-fill { height: 100%; border-radius: 2px; transition: width .6s ease; } .bl-risk-score { font-size: 11px; color: var(--bl-text2); white-space: nowrap; font-family: var(--bl-mono); } /* ── Copy button inline ── */ .bl-copy-inline { background: none; border: 1px solid var(--bl-border); border-radius: 4px; color: var(--bl-text2); cursor: pointer; padding: 1px 6px; font-size: 10px; font-family: var(--bl-sans); transition: all .12s; vertical-align: middle; margin-left: 4px; } .bl-copy-inline:hover { background: rgba(255,255,255,.05); color: var(--bl-text0); } /* ── Ad blocker ── */ body.bl-no-ads .GoogleActiveViewElement, body.bl-no-ads [data-google-av-adk], body.bl-no-ads ins.adsbygoogle, body.bl-no-ads iframe[src*="pagead2.googlesyndication.com"], body.bl-no-ads iframe[src*="doubleclick.net"], body.bl-no-ads div[id^="google_ads_iframe"], body.bl-no-ads .ad-container { display: none !important; } /* ── Enhanced dark mode ── */ body.bl-dark .bg-background { background: var(--bl-bg0) !important; } body.bl-dark .bg-card { background: var(--bl-bg1) !important; } body.bl-dark header { background: var(--bl-bg0) !important; } /* ── Compact mode ── */ body.bl-compact .bl-table td { padding: 7px 12px; } body.bl-compact .bl-statcard { padding: 12px; } /* ── Portfolio live flash ── */ @keyframes bl-port-flash { 0%{background:rgba(34,197,94,.22);transform:scale(1.04)}100%{background:transparent;transform:scale(1)} } .bl-port-flash { animation: bl-port-flash .5s ease-out; border-radius: 4px; } /* ── Rugpuller badge ── */ #bl-rug-badge { display: inline-flex; position: relative; margin-left: 8px; padding-bottom: 8px; margin-bottom: -8px; } .bl-rug-chip { padding: 2px 8px; border-radius: 5px; font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; gap: 4px; color: var(--bl-red); background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); cursor: help; } .bl-rug-tip { visibility: hidden; opacity: 0; width: 250px; background: var(--bl-bg1); color: var(--bl-text0); border: 1px solid rgba(239,68,68,.3); border-radius: var(--bl-r-md); padding: 10px 12px; position: absolute; z-index: 10000; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); font-size: 12px; line-height: 1.5; font-weight: 400; font-family: var(--bl-sans); pointer-events: none; white-space: normal; box-shadow: var(--bl-shadow); transition: opacity .15s, visibility .15s; } #bl-rug-badge:hover .bl-rug-tip { visibility: visible; opacity: 1; } /* ── New trade flash ── */ @keyframes bl-row-flash { from { background: rgba(34,197,94,.14); } to { background: transparent; } } .bl-new-row { animation: bl-row-flash 2.5s ease-out; } /* ── Clickable table rows ── */ tr[data-bl-row] { cursor: pointer; } tr[data-bl-row]:hover td { background: rgba(255,255,255,.025) !important; } tr[data-bl-wl] td:first-child { box-shadow: inset 2px 0 0 var(--bl-yellow); } /* ── Profile action buttons ── */ #bl-profile-actions { position: absolute; top: 14px; right: 14px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; z-index: 10; } /* ── Speed bar on coin pages ── */ #bl-speed-bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; padding: 12px 0 0; border-top: 1px solid var(--bl-border); margin-top: 12px; } .bl-quick-buy { padding: 5px 12px; border-radius: var(--bl-r-sm); font-size: 12px; font-weight: 700; cursor: pointer; border: 1px solid rgba(34,197,94,.28); background: rgba(34,197,94,.07); color: var(--bl-green); transition: all .12s; font-family: var(--bl-sans); } .bl-quick-buy:hover { background: rgba(34,197,94,.16); transform: translateY(-1px); } .bl-quick-sell { padding: 5px 11px; border-radius: var(--bl-r-sm); font-size: 12px; font-weight: 700; cursor: pointer; border: 1px solid rgba(239,68,68,.25); background: rgba(239,68,68,.07); color: var(--bl-red); transition: all .12s; font-family: var(--bl-sans); } .bl-quick-sell:hover { background: rgba(239,68,68,.16); } /* ── Holder bar ── */ .bl-holder-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; gap: 1px; margin: 8px 0 10px; } .bl-holder-seg { height: 100%; transition: width .4s ease; } .bl-holder-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,.025); font-size: 12px; } .bl-holder-row:last-child { border-bottom: none; } .bl-holder-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; } .bl-holder-pct { font-weight: 700; font-family: var(--bl-mono); font-size: 11px; color: var(--bl-text2); } /* ── Sidebar branding ── */ [data-bl-brand] { font-weight: 800 !important; background: linear-gradient(90deg, #818cf8, #c4b5fd) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; } /* ════════════════════════════════════════════════════════════════════ LIVE FEED — bottom-right tray ═══════════════════════════════════════════════════════════════════ */ #bl-feed { position: fixed; bottom: 0; right: 24px; width: 300px; background: var(--bl-bg1); border: 1px solid var(--bl-border-hi); border-bottom: none; border-radius: var(--bl-r-xl) var(--bl-r-xl) 0 0; z-index: var(--bl-z-feed); transform: translateY(100%); transition: transform .26s cubic-bezier(.22,1,.36,1); box-shadow: 0 -8px 40px rgba(0,0,0,.5); font-family: var(--bl-sans); } #bl-feed.open { transform: translateY(0); } #bl-feed-header { display: flex; align-items: center; padding: 11px 14px; border-bottom: 1px solid var(--bl-border); cursor: pointer; user-select: none; gap: 8px; } .bl-feed-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bl-green); flex-shrink: 0; animation: bl-pulse 2s infinite; } @keyframes bl-pulse { 0%,100%{opacity:1}50%{opacity:.3} } #bl-feed-label { font-size: 12px; font-weight: 700; color: var(--bl-text0); flex: 1; } #bl-feed-count { font-size: 11px; color: var(--bl-text2); font-family: var(--bl-mono); } #bl-feed-clear { background:none; border:none; color:var(--bl-text2); cursor:pointer; font-size:10px; padding:2px 6px; border-radius:4px; transition:color .12s; } #bl-feed-clear:hover { color:var(--bl-text0); } #bl-feed-chevron { font-size: 10px; color: var(--bl-text2); } #bl-feed-body { max-height: 260px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.06) transparent; } #bl-feed-body::-webkit-scrollbar { width: 3px; } #bl-feed-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.06); } .bl-feed-item { display: flex; align-items: center; gap: 8px; padding: 7px 14px; border-bottom: 1px solid rgba(255,255,255,.025); font-size: 12px; cursor: pointer; transition: background .1s; } .bl-feed-item:hover { background: rgba(255,255,255,.025); } .bl-feed-item:last-child { border-bottom: none; } .bl-feed-sym { font-weight: 700; color: var(--bl-text0); min-width: 52px; } .bl-feed-val { color: var(--bl-text1); } .bl-feed-ts { margin-left: auto; color: var(--bl-text2); font-size: 10px; font-family: var(--bl-mono); } /* ── Status pill (replaces portfolio bar) ── */ #bl-status-pill { position: fixed; bottom: 52px; right: 24px; z-index: var(--bl-z-status); background: var(--bl-bg2); border: 1px solid var(--bl-border-hi); border-radius: 24px; padding: 6px 14px; display: flex; align-items: center; gap: 16px; font-family: var(--bl-sans); font-size: 11px; box-shadow: var(--bl-shadow); transform: translateY(0); transition: opacity .2s, transform .2s; cursor: default; user-select: none; } #bl-status-pill.hidden { opacity: 0; pointer-events: none; transform: translateY(8px); } .bl-pill-item { display: flex; align-items: center; gap: 5px; } .bl-pill-lbl { color: var(--bl-text2); font-size: 10px; text-transform: uppercase; letter-spacing: .4px; } .bl-pill-val { color: var(--bl-text0); font-weight: 700; font-family: var(--bl-mono); } .bl-pill-dot { width: 5px; height: 5px; border-radius: 50%; } .bl-pill-sep { color: var(--bl-border-hi); } /* Feed tab (bottom of feed) */ #bl-feed-tab { position: fixed; bottom: 0; right: 24px; width: 300px; height: 36px; background: var(--bl-bg2); border: 1px solid var(--bl-border-hi); border-bottom: none; border-radius: var(--bl-r-lg) var(--bl-r-lg) 0 0; z-index: calc(var(--bl-z-feed) - 1); display: flex; align-items: center; padding: 0 14px; gap: 8px; cursor: pointer; user-select: none; font-family: var(--bl-sans); transition: background .12s; } #bl-feed-tab:hover { background: var(--bl-bg3); } #bl-feed-tab.hidden { display: none; } .bl-feed-tab-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--bl-green); animation: bl-pulse 2s infinite; } .bl-feed-tab-label { font-size: 12px; font-weight: 600; color: var(--bl-text0); flex: 1; } .bl-feed-tab-cnt { font-size: 11px; color: var(--bl-text2); font-family: var(--bl-mono); } /* ════════════════════════════════════════════════════════════════════ COMMAND PALETTE (search) ═══════════════════════════════════════════════════════════════════ */ #bl-search-wrap { position: fixed; inset: 0; z-index: var(--bl-z-search); display: none; align-items: flex-start; justify-content: center; padding-top: 80px; background: rgba(0,0,0,.6); backdrop-filter: blur(3px); } #bl-search-wrap.open { display: flex; } #bl-search-box { width: 560px; max-width: calc(100vw - 32px); background: var(--bl-bg1); border: 1px solid var(--bl-border-hi); border-radius: var(--bl-r-xl); box-shadow: var(--bl-shadow-lg); overflow: hidden; } #bl-search-input { width: 100%; background: none; border: none; color: var(--bl-text0); font-size: 15px; padding: 16px 18px; outline: none; font-family: var(--bl-sans); box-sizing: border-box; } #bl-search-input::placeholder { color: var(--bl-text2); } #bl-search-divider { height: 1px; background: var(--bl-border); } #bl-search-results { max-height: 340px; overflow-y: auto; } .bl-sr-section { padding: 8px 18px 4px; font-size: 10px; font-weight: 700; color: var(--bl-text2); text-transform: uppercase; letter-spacing: .6px; } .bl-sr-item { display: flex; align-items: center; gap: 10px; padding: 10px 18px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,.02); font-family: var(--bl-sans); transition: background .1s; } .bl-sr-item:hover { background: rgba(255,255,255,.04); } .bl-sr-item:last-child { border-bottom: none; } .bl-sr-icon { font-size: 16px; flex-shrink: 0; } .bl-sr-main { flex: 1; min-width: 0; } .bl-sr-name { font-size: 13px; font-weight: 600; color: var(--bl-text0); } .bl-sr-sub { font-size: 11px; color: var(--bl-text2); margin-top: 1px; } .bl-sr-empty { padding: 24px 18px; text-align: center; font-size: 13px; color: var(--bl-text2); font-family: var(--bl-sans); } /* ════════════════════════════════════════════════════════════════════ CENTER PAGE — full-app shell layout ═══════════════════════════════════════════════════════════════════ */ #bl-app { display: flex; height: 100%; min-height: calc(100vh - 80px); background: var(--bl-bg0); font-family: var(--bl-sans); color: var(--bl-text0); } /* Left nav */ #bl-nav { width: 200px; flex-shrink: 0; background: var(--bl-bg1); border-right: 1px solid var(--bl-border); display: flex; flex-direction: column; padding: 0; position: sticky; top: 0; height: 100vh; overflow-y: auto; } .bl-nav-brand { display: flex; align-items: center; gap: 10px; padding: 20px 18px 16px; border-bottom: 1px solid var(--bl-border); flex-shrink: 0; } .bl-nav-brand-name { font-size: 15px; font-weight: 800; background: linear-gradient(120deg, #818cf8, #c4b5fd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -.3px; } .bl-nav-brand-ver { font-size: 10px; color: var(--bl-text2); font-family: var(--bl-mono); margin-top: 1px; } .bl-nav-section { padding: 16px 12px 4px; font-size: 10px; font-weight: 700; color: var(--bl-text2); text-transform: uppercase; letter-spacing: .6px; } .bl-nav-item { display: flex; align-items: center; gap: 9px; padding: 8px 12px; margin: 1px 6px; border-radius: var(--bl-r-md); font-size: 13px; font-weight: 500; color: var(--bl-text1); cursor: pointer; transition: background .12s, color .12s; user-select: none; border: none; background: none; text-align: left; width: calc(100% - 12px); font-family: var(--bl-sans); } .bl-nav-item:hover { background: rgba(255,255,255,.05); color: var(--bl-text0); } .bl-nav-item.active { background: var(--bl-accent-dim); color: #a5b4fc; border: 1px solid var(--bl-accent-border); } .bl-nav-item-icon { font-size: 15px; flex-shrink: 0; } .bl-nav-badge { margin-left: auto; background: rgba(255,255,255,.08); color: var(--bl-text2); font-size: 10px; font-family: var(--bl-mono); padding: 1px 6px; border-radius: 10px; font-weight: 600; } .bl-nav-footer { margin-top: auto; padding: 12px 12px 16px; border-top: 1px solid var(--bl-border); flex-shrink: 0; } .bl-nav-close-btn { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px; background: rgba(255,255,255,.04); border: 1px solid var(--bl-border); border-radius: var(--bl-r-md); color: var(--bl-text1); font-size: 12px; font-weight: 500; cursor: pointer; font-family: var(--bl-sans); transition: background .12s; } .bl-nav-close-btn:hover { background: rgba(255,255,255,.07); } /* Main content area */ #bl-main { flex: 1; overflow-y: auto; min-height: 100vh; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.06) transparent; } #bl-main::-webkit-scrollbar { width: 5px; } #bl-main::-webkit-scrollbar-thumb { background: rgba(255,255,255,.06); border-radius: 4px; } /* Page header */ .bl-page-header { padding: 28px 28px 20px; border-bottom: 1px solid var(--bl-border); } .bl-page-title { font-size: 22px; font-weight: 800; color: var(--bl-text0); letter-spacing: -.4px; margin: 0 0 4px; } .bl-page-subtitle { font-size: 13px; color: var(--bl-text2); margin: 0; } /* Content panel */ .bl-pane { display: none; } .bl-pane.active { display: block; } .bl-content { padding: 24px 28px; } /* Section header */ .bl-section-title { font-size: 13px; font-weight: 700; color: var(--bl-text0); margin: 0 0 14px; display: flex; align-items: center; gap: 8px; } .bl-section-desc { font-size: 12px; color: var(--bl-text2); margin: -10px 0 16px; line-height: 1.5; } /* Card */ .bl-card { background: var(--bl-bg2); border: 1px solid var(--bl-border); border-radius: var(--bl-r-xl); padding: 20px; margin-bottom: 16px; } .bl-card-title { font-size: 13px; font-weight: 700; color: var(--bl-text0); margin: 0 0 4px; display: flex; align-items: center; gap: 8px; } .bl-card-desc { font-size: 11px; color: var(--bl-text2); margin: 0 0 16px; line-height: 1.4; } /* Two-column grid */ .bl-grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; } .bl-grid-3 { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; } /* Vote buttons */ .bl-vote { display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: var(--bl-r-sm); font-size: 11px; cursor: pointer; border: 1px solid var(--bl-border); background: none; color: var(--bl-text2); font-family: var(--bl-sans); transition: all .12s; } .bl-vote:hover:not(.voted) { background: rgba(255,255,255,.05); color: var(--bl-text0); } .bl-vote.voted { cursor: not-allowed; opacity: .55; } .bl-vote.like.selected { background: rgba(34,197,94,.1); color: var(--bl-green); border-color: rgba(34,197,94,.25); } .bl-vote.dislike.selected { background: rgba(239,68,68,.1); color: var(--bl-red); border-color: rgba(239,68,68,.25); } /* Tooltip */ [data-tip] { position: relative; } [data-tip]:hover::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--bl-bg3); border: 1px solid var(--bl-border-hi); color: var(--bl-text0); font-size: 11px; padding: 5px 9px; border-radius: var(--bl-r-sm); white-space: nowrap; z-index: 9999; pointer-events: none; font-family: var(--bl-sans); font-weight: 400; box-shadow: var(--bl-shadow); } `); // ───────────────────────────────────────────────────────────────────────── // CORE UI SYSTEMS // ───────────────────────────────────────────────────────────────────────── // ─── Toast System ───────────────────────────────────────────────────────── const toaster = { el: null, init() { if (document.getElementById('bl-toasts')) return; this.el = document.createElement('div'); this.el.id = 'bl-toasts'; document.body.appendChild(this.el); }, show({ type = 'info', title = '', body = '', duration } = {}) { if (!this.el) this.init(); const d = duration ?? cfg.get('toastDuration'); const icons = { buy: '▲', sell: '▼', warn: '!', success: '✓', error: '✕', info: '·', alert: '◆', copy: '⎘' }; const t = document.createElement('div'); t.className = `bl-toast bl-t-${type}`; t.innerHTML = `${icons[type] || '·'}
${U.esc(title)}
${body ? `
${U.esc(body)}
` : ''}
`; t.querySelector('.bl-t-close').addEventListener('click', () => this._dismiss(t)); this.el.appendChild(t); requestAnimationFrame(() => t.classList.add('in')); setTimeout(() => this._dismiss(t), d); }, _dismiss(t) { if (!t.parentNode) return; t.classList.remove('in'); t.classList.add('out'); t.addEventListener('transitionend', () => t.remove(), { once: true }); }, }; // ─── Modal System ───────────────────────────────────────────────────────── const modal = { open({ id, title, content, width = '800px', onBind } = {}) { this.close(id); const ov = document.createElement('div'); ov.className = 'bl-overlay'; ov.id = `blm-${id}`; const m = document.createElement('div'); m.className = 'bl-modal'; m.style.maxWidth = width; m.innerHTML = `
${title}
${content}
`; ov.appendChild(m); document.body.appendChild(ov); requestAnimationFrame(() => ov.classList.add('in')); ov.addEventListener('click', e => { if (e.target === ov) this.close(id); }); m.querySelector('.bl-m-close').addEventListener('click', () => this.close(id)); if (onBind) onBind(m); }, close(id) { const ov = document.getElementById(`blm-${id}`); if (!ov) return; ov.classList.remove('in'); ov.classList.add('out'); ov.addEventListener('transitionend', () => ov.remove(), { once: true }); }, }; // ─── WebSocket Hook ─────────────────────────────────────────────────────── const wsHook = { patch() { if (WebSocket.prototype._bl4) return; WebSocket.prototype._bl4 = true; Object.defineProperty(WebSocket.prototype, 'onmessage', { get() { return this.__blCb; }, set(cb) { if (this.url?.startsWith('wss://ws.rugplay.com')) { const h = ev => { try { const d = JSON.parse(ev.data); if ((d.type === 'live-trade' || d.type === 'all-trades') && d.data) { if (tradeDB.add(d.data)) wsHook.onTrade(d.data); } } catch {} cb?.call(this, ev); }; this.__blCb = cb; if (this._blH) this.removeEventListener('message', this._blH); this.addEventListener('message', h); this._blH = h; } else { this.__blCb = cb; if (this._blH) this.removeEventListener('message', this._blH); this.addEventListener('message', cb); this._blH = cb; } }, }); }, onTrade(t) { const s = cfg.all(); const isBuy = t.type?.toUpperCase() === 'BUY'; if (s.liveToasts) { if (isBuy && s.toastBuys) toaster.show({ type:'buy', title:`${t.coinSymbol}`, body:`${t.username} · ${U.fmtUSD(t.totalValue)}` }); if (!isBuy && s.toastSells) toaster.show({ type:'sell', title:`${t.coinSymbol}`, body:`${t.username} · ${U.fmtUSD(t.totalValue)}` }); } if (s.rugAlerts && t.coinSymbol) { const sc = rugScorer.score(t.coinSymbol); if (sc !== null && sc >= s.rugThreshold) { const r = rugScorer.label(sc); toaster.show({ type:'warn', title:`Rug Alert — ${t.coinSymbol}`, body:`Score ${sc}/100 · ${r.text}`, duration: 8000 }); } } if (s.watchlistAlerts && wlDB.has(t.coinSymbol)) { const spike = spikeTracker.check(t.coinSymbol); if (spike && s.volumeSpikeAlerts) { toaster.show({ type:'alert', title:`Volume Spike — ${t.coinSymbol}`, body:`${spike}× normal volume`, duration: 7000 }); } else { toaster.show({ type:'alert', title:`Watchlist — ${t.coinSymbol}`, body:`${t.type} · ${U.fmtUSD(t.totalValue)}` }); } } if (t.username) botDetector.autoTag(t.username); feedPanel.push(t); statusPill.update(); livePortfolio.trigger(); if (U.isCoin() && U.getCoin() === t.coinSymbol?.toUpperCase()) coinEnhancer.pushNewTrade(t); }, }; wsHook.patch(); // ─── URL Watcher ────────────────────────────────────────────────────────── class URLWatcher { constructor(ms = 320) { this._cbs = []; this._cur = location.href; setInterval(() => this._chk(), ms); ['popstate', 'hashchange'].forEach(e => window.addEventListener(e, () => this._chk())); } _chk() { if (location.href !== this._cur) { const old = this._cur; this._cur = location.href; this._cbs.forEach(cb => { try { cb(this._cur, old); } catch {} }); } } on(cb) { this._cbs.push(cb); return this; } } // ─── Status Pill ────────────────────────────────────────────────────────── // Compact bottom-right indicator — replaces the noisy top bar. const statusPill = { el: null, init() { if (document.getElementById('bl-status-pill')) return; const p = document.createElement('div'); p.id = 'bl-status-pill'; p.innerHTML = ` $0 vol · 0 B 0 S · 0 users `; p.title = 'Session stats — click Blackline+ to open dashboard'; document.body.appendChild(p); this.el = p; }, update() { const st = tradeDB.stats(); const s = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; }; s('bl-pill-vol', U.fmtUSD(st.volume)); s('bl-pill-buys', st.buys); s('bl-pill-sells', st.sells); s('bl-pill-users', st.users); }, show(v) { this.el?.classList.toggle('hidden', !v); }, }; // ─── Live Feed ──────────────────────────────────────────────────────────── const feedPanel = { el: null, body: null, open: false, count: 0, tab: null, init() { if (document.getElementById('bl-feed')) return; // Feed tray const f = document.createElement('div'); f.id = 'bl-feed'; f.innerHTML = `
Live Feed
`; document.body.appendChild(f); this.el = f; this.body = document.getElementById('bl-feed-body'); // The persistent tab that's always visible at bottom-right const tab = document.createElement('div'); tab.id = 'bl-feed-tab'; tab.innerHTML = `Live Feed`; document.body.appendChild(tab); this.tab = tab; document.getElementById('bl-feed-header').addEventListener('click', e => { if (e.target.id === 'bl-feed-clear') return; this.toggle(); }); tab.addEventListener('click', () => this.open ? this.toggle() : this.show()); document.getElementById('bl-feed-clear').addEventListener('click', e => { e.stopPropagation(); this.body.innerHTML = ''; this.count = 0; this._updateCount(); }); // Restore session state if (session.get('feedOpen')) this.show(); }, show() { this.open = true; this.el.classList.add('open'); this.tab.classList.add('hidden'); session.set('feedOpen', true); }, hide() { this.open = false; this.el.classList.remove('open'); this.tab.classList.remove('hidden'); session.set('feedOpen', false); }, toggle() { this.open ? this.hide() : this.show(); }, _updateCount() { const cnt = document.getElementById('bl-feed-count'); const tab = document.getElementById('bl-feed-tab-cnt'); const txt = this.count > 0 ? `${this.count}` : ''; if (cnt) cnt.textContent = txt; if (tab) tab.textContent = txt; }, push(t) { if (!this.body) return; this.count++; this._updateCount(); const isBuy = t.type?.toUpperCase() === 'BUY'; const tagData = tagDB.get(t.username); const preset = tagData ? TAG_PRESETS[tagData.tag] : null; const item = document.createElement('div'); item.className = 'bl-feed-item'; item.innerHTML = ` ${isBuy ? 'B' : 'S'} ${U.esc(t.coinSymbol || '')} ${U.fmtUSD(parseFloat(t.totalValue))} ${preset ? `${preset.emoji}` : ''} ${U.timeAgo(t.timestamp)}`; item.addEventListener('click', () => location.href = `/coin/${t.coinSymbol}`); this.body.prepend(item); while (this.body.children.length > 100) this.body.lastChild?.remove(); }, }; setInterval(() => { $$('#bl-feed-body .bl-feed-ts[data-ts]').forEach(el => { el.textContent = U.timeAgo(Number(el.dataset.ts)); }); }, 1000); // ─── Command Palette (Search) ───────────────────────────────────────────── const searchPanel = { el: null, inp: null, res: null, open: false, init() { if (document.getElementById('bl-search-wrap')) return; const w = document.createElement('div'); w.id = 'bl-search-wrap'; w.innerHTML = ` `; document.body.appendChild(w); this.el = w; this.inp = document.getElementById('bl-search-input'); this.res = document.getElementById('bl-search-results'); this.inp.addEventListener('input', () => this._search(this.inp.value.trim())); w.addEventListener('click', e => { if (e.target === w) this.hide(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && this.open) this.hide(); }); }, show() { this.el?.classList.add('open'); this.open = true; setTimeout(() => this.inp?.focus(), 40); }, hide() { this.el?.classList.remove('open'); this.open = false; }, toggle() { this.open ? this.hide() : this.show(); }, _search(q) { if (!q) { this.res.innerHTML = ''; return; } const lq = q.toLowerCase(); const allTrades = tradeDB.all(); const users = [...new Set(allTrades.map(t => t.username).filter(Boolean))].filter(u => u.toLowerCase().includes(lq)).slice(0, 5); const coins = [...new Set(allTrades.map(t => t.coinSymbol))].filter(c => c.toLowerCase().includes(lq)).slice(0, 5); let html = ''; if (users.length) { html += `
Users
`; html += users.map(u => { const d = tagDB.get(u), p = d ? TAG_PRESETS[d.tag] : null; const bot = botDetector.analyze(u); const cnt = tradeDB.forUser(u).length; return `
${p ? p.emoji : '👤'}
@${U.esc(u)}${p ? `${p.label}` : ''}${bot && cfg.get('showBotBadges') ? `🤖` : ''}
${cnt} trades this session
`; }).join(''); } if (coins.length) { html += `
Coins
`; html += coins.map(c => { const sc = rugScorer.score(c), r = sc !== null ? rugScorer.label(sc) : null; const cnt = tradeDB.forCoin(c).length; const vol = tradeDB.forCoin(c).reduce((s,t) => s+t.totalValue, 0); return `
${wlDB.has(c) ? '⭐' : '🪙'}
${U.esc(c)}${r ? `${r.text}` : ''}
${cnt} trades · ${U.fmtUSD(vol)}
`; }).join(''); } if (!users.length && !coins.length) { html = `
No results for "${U.esc(q)}"
`; } this.res.innerHTML = html; $$('.bl-sr-item[data-nav]', this.res).forEach(item => { item.addEventListener('click', e => { if (e.target.closest('button')) return; location.href = item.dataset.nav; this.hide(); }); }); $$('[data-hist]', this.res).forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); histModal.open(btn.dataset.hist); this.hide(); }); }); }, }; // ─── Hotkeys ────────────────────────────────────────────────────────────── const hotkeys = { init() { document.addEventListener('keydown', e => { if (['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) return; const k = e.key.toLowerCase(); if (k === cfg.get('hkCenter').toLowerCase()) { e.preventDefault(); centerPage.visible ? centerPage.hide() : centerPage.show(); } if (k === cfg.get('hkFeed').toLowerCase()) { e.preventDefault(); feedPanel.toggle(); } if (k === cfg.get('hkSearch').toLowerCase()) { e.preventDefault(); searchPanel.toggle(); } if (k === 'escape') { if (centerPage.visible) centerPage.hide(); searchPanel.hide(); } }); }, }; // ─── Sidebar ────────────────────────────────────────────────────────────── const sidebar = { inject() { // Brand rename $$('a[href="/"] span, [data-slot="sidebar-header"] span').forEach(el => { if (!el.children.length && el.textContent.trim() === 'Rugplay' && !el.dataset.blBrand) { el.dataset.blBrand = '1'; el.setAttribute('data-bl-brand', '1'); el.textContent = 'Blackline+'; } }); const home = $('a[href="/"]:has(svg.lucide-house)'); if (!home) return; const li = home.closest('li'); if (!li?.parentNode) return; if (!home.dataset.blPatched) { home.dataset.blPatched = '1'; home.addEventListener('click', e => { e.preventDefault(); if (location.pathname !== '/') location.href = '/#bl-center'; else centerPage.show(); }); } const mkItem = (id, href, svg, label, onClick) => { if (document.getElementById(id)) return; const c = li.cloneNode(true), a = c.querySelector('a'); a.id = id; a.href = href; a.removeAttribute('data-active'); const s = a.querySelector('svg'); if (s) s.outerHTML = svg; const sp = a.querySelector('span'); if (sp) sp.textContent = label; if (onClick) a.addEventListener('click', e => { e.preventDefault(); onClick(); }); li.parentNode.insertBefore(c, li.nextSibling); }; mkItem('bl-sb-main', '/#bl-center', ``, 'Blackline+', () => centerPage.show()); mkItem('bl-sb-tx', '/transactions', ``, 'Transactions', null); const blSb = document.getElementById('bl-sb-main'); if (blSb) { const sp = blSb.querySelector('span'); if (sp && !sp.dataset.blStyled) { sp.dataset.blStyled = '1'; sp.setAttribute('data-bl-brand', '1'); sp.textContent = 'Blackline+'; } } }, }; // ─── Notification Badges ────────────────────────────────────────────────── const notifBadges = { apply() { const enabled = cfg.get('notifBadges'); $$('a[href="/notifications"] > div').forEach(el => { el.style.display = enabled ? '' : 'none'; }); }, }; // ─── Ad Blocker ─────────────────────────────────────────────────────────── const adBlocker = { apply() { document.body.classList.toggle('bl-no-ads', cfg.get('adBlocker')); }, }; // ─── Appear Offline ─────────────────────────────────────────────────────── const appearOffline = { apply(val) { window.dispatchEvent(new CustomEvent('rpp_visibility_changed', { detail: { hidden: val } })); }, }; // ─── Sticky Portfolio ───────────────────────────────────────────────────── const stickyPortfolio = { apply() { const enabled = cfg.get('stickyPortfolio'); const footer = $('[data-sidebar="footer"]'); const content = $('[data-sidebar="content"]') || $('[data-slot="sidebar-content"]'); if (!footer || !content) return; const group = $$('[data-sidebar="group"]').find(g => g.querySelector('[data-sidebar="group-label"]')?.textContent.includes('Portfolio')); if (!group) return; if (enabled && group.parentElement !== footer) { group.style.borderTop = '1px solid rgba(255,255,255,.06)'; footer.insertBefore(group, footer.firstChild); } else if (!enabled && group.parentElement === footer) { group.style.borderTop = ''; content.appendChild(group); } }, }; // ─── Live Portfolio Refresh ─────────────────────────────────────────────── const livePortfolio = { _busy: false, _last: 0, _prev: null, async trigger() { if (!cfg.get('livePortfolioRefresh')) return; const now = Date.now(); if (this._busy || now - this._last < 3500) return; this._last = now; this._busy = true; try { const res = await fetch('/api/portfolio/summary', { headers: { Accept: 'application/json' } }); if (!res.ok) return; const data = await res.json(); this._update(data); } catch {} finally { this._busy = false; } }, _update(data) { const cont = this._findContainer(); if (!cont) return; const spans = cont.querySelectorAll('span.font-mono'); if (spans.length < 3) return; const fmt = v => new Intl.NumberFormat('en-US', { style:'currency', currency:'USD' }).format(v); const nt = data.total_value ?? data.totalValue ?? data.total; const nc = data.cash_value ?? data.cashValue ?? data.cash; const nv = data.coins_value ?? data.coinsValue ?? data.coins; if (nt !== undefined) spans[0].textContent = fmt(nt); if (nc !== undefined) spans[1].textContent = fmt(nc); if (nv !== undefined) spans[2].textContent = fmt(nv); if (this._prev !== null && nt !== undefined && Math.abs(this._prev - nt) > 0.001) { [spans[0], spans[1], spans[2]].forEach(el => { if (!el) return; el.classList.remove('bl-port-flash'); void el.offsetWidth; el.classList.add('bl-port-flash'); el.addEventListener('animationend', () => el.classList.remove('bl-port-flash'), { once: true }); }); } this._prev = nt ?? this._prev; }, _findContainer() { const lbl = $$('span').find(s => s.textContent.trim() === 'Total Value'); return lbl ? lbl.closest('.space-y-2') : null; }, }; // ─── Rugpuller Badge ────────────────────────────────────────────────────── const rugpullerBadge = { _last: null, async run() { if (!cfg.get('showRugpullerBadge') || !U.isCoin()) return; const sym = U.getCoin(); if (!sym || sym === this._last) return; this._last = sym; const findAnchor = () => $$('span').find(s => s.textContent.trim() === 'Created by'); let anchor = findAnchor(); if (!anchor) { await sleep(800); anchor = findAnchor(); } if (!anchor || anchor.parentElement.querySelector('#bl-rug-badge')) return; try { const res = await fetch(`/coin/${sym}/__data.json?x-sveltekit-invalidated=11`); if (!res.ok) return; const d = await res.json(); const dataArr = d?.nodes?.[1]?.data; if (!Array.isArray(dataArr)) return; const coinIdx = dataArr[0]?.coin; if (coinIdx === undefined) return; const coinData = dataArr[coinIdx]; const usernameIdx = coinData?.creatorUsername; const creator = typeof usernameIdx === 'number' ? dataArr[usernameIdx] : null; if (!creator) return; const tagData = tagDB.get(creator); const isBad = tagData && ['RUGPULLER','SCAMMER'].includes(tagData.tag); const repCount = communityReports.all().filter(r => r.username.toLowerCase() === creator.toLowerCase()).length; if (isBad || repCount >= 1) { const reason = isBad ? `You tagged @${creator} as ${TAG_PRESETS[tagData.tag]?.label}` : `${repCount} community report(s) against @${creator}`; const badge = document.createElement('div'); badge.id = 'bl-rug-badge'; badge.innerHTML = `⚠ Flagged
@${U.esc(creator)} has been flagged.

${U.esc(reason)}

Trade with caution.
`; anchor.parentElement.appendChild(badge); } } catch {} }, reset() { this._last = null; document.getElementById('bl-rug-badge')?.remove(); }, }; // ─── User Tagger ────────────────────────────────────────────────────────── const tagger = { html(username) { const d = tagDB.get(username); if (!d) return ''; const p = TAG_PRESETS[d.tag] || TAG_PRESETS.DEV; return `${p.emoji} ${p.label}`; }, inject(el, username, pos = 'beforeend') { if (!el || el.dataset.blTagged) return; el.dataset.blTagged = '1'; const d = tagDB.get(username); if (!d) return; const p = TAG_PRESETS[d.tag] || TAG_PRESETS.DEV; const s = document.createElement('span'); s.className = 'bl-tag'; s.style.background = p.bg; s.style.color = p.text; s.textContent = `${p.emoji} ${p.label}`; el.insertAdjacentElement(pos, s); }, run() { if (U.isUser()) { const u = U.getPageUser(); if (u) { const p = $('p.text-muted-foreground.text-lg'); if (p && p.textContent.includes(`@${u}`)) this.inject(p, u); } } if (U.isCoin()) { $$('.border-b:not([data-bl-tagged])').forEach(el => { const sp = el.querySelector('button span.truncate'); const tm = el.querySelector('span.text-muted-foreground.flex-shrink-0'); if (sp && tm) this.inject(tm, sp.textContent.replace('@','').trim(), 'afterend'); }); } }, }; // ─── Table Enhancer ─────────────────────────────────────────────────────── const tableEnhancer = { run() { $$('main table tbody tr:not([data-bl-row])').forEach(row => { const img = row.querySelector('img[alt]'); if (!img) return; const sym = img.getAttribute('alt'); if (!sym) return; row.setAttribute('data-bl-row', sym); if (wlDB.has(sym)) row.setAttribute('data-bl-wl', '1'); if (hiddenDB.has(sym)) { row.style.display = 'none'; return; } if (cfg.get('clickableRows')) { row.style.cursor = 'pointer'; row.addEventListener('click', e => { if (['A','BUTTON'].includes(e.target.tagName.toUpperCase())) return; location.href = `/coin/${sym}`; }); } }); }, }; // ─── Modal: Tag ─────────────────────────────────────────────────────────── const tagModal = { open(username) { const cur = tagDB.get(username); const opts = Object.entries(TAG_PRESETS).map(([k, v]) => ``).join(''); modal.open({ id: 'bl-tagmod', title: `Tag @${U.esc(username)}`, width: '380px', content: `

Assign a tag to @${U.esc(username)}

`, onBind: m => { m.querySelector('#bl-tm-ok').addEventListener('click', () => { const val = m.querySelector('#bl-tm-sel').value; if (val) tagDB.set(username, val, username); else tagDB.remove(username); modal.close('bl-tagmod'); const p = val ? TAG_PRESETS[val] : null; toaster.show({ type: 'success', title: p ? `Tagged @${username} as ${p.label}` : `Tag removed from @${username}` }); $$('[data-bl-tagged]').forEach(el => delete el.dataset.blTagged); tagger.run(); }); m.querySelector('#bl-tm-x').addEventListener('click', () => modal.close('bl-tagmod')); }, }); }, }; // ─── Modal: Note ────────────────────────────────────────────────────────── const noteModal = { open(sym) { const cur = notesDB.get(sym); modal.open({ id: 'bl-notemod', title: `Note — ${U.esc(sym)}`, width: '440px', content: `

Private note for ${U.esc(sym)}

${cur ? `` : ''}
`, onBind: m => { m.querySelector('#bl-nt-ok').addEventListener('click', () => { const txt = m.querySelector('#bl-nt').value.trim(); if (txt) notesDB.set(sym, txt); else notesDB.remove(sym); modal.close('bl-notemod'); toaster.show({ type: 'success', title: `Note saved for ${sym}` }); }); m.querySelector('#bl-nt-del')?.addEventListener('click', () => { notesDB.remove(sym); modal.close('bl-notemod'); toaster.show({ type: 'info', title: 'Note deleted' }); }); m.querySelector('#bl-nt-x').addEventListener('click', () => modal.close('bl-notemod')); }, }); }, }; // ─── Modal: History ─────────────────────────────────────────────────────── const histModal = { open(username, page = 1) { const trades = tradeDB.forUser(username); const perPage = 12, total = Math.ceil(trades.length / perPage); const slice = trades.slice((page - 1) * perPage, page * perPage); const buys = trades.filter(t => t.type === 'BUY'); const sells = trades.filter(t => t.type === 'SELL'); const tB = buys.reduce((s, t) => s + t.totalValue, 0); const tS = sells.reduce((s, t) => s + t.totalValue, 0); const pnl = tS - tB; const coinMap = {}; trades.forEach(t => { coinMap[t.coinSymbol] = (coinMap[t.coinSymbol] || 0) + 1; }); const topCoins = Object.entries(coinMap).sort((a,b) => b[1]-a[1]).slice(0,5) .map(([s, c]) => `${s}(${c})`).join(' · '); const bot = botDetector.analyze(username); const rows = slice.map(tx => ` ${tx.type} ${U.esc(tx.coinSymbol)} ${U.fmtUSD(tx.totalValue)} ${U.fmtNum(tx.quantity)} ${U.fmtDate(tx.timestamp)} `).join(''); modal.open({ id: 'bl-hist', title: `Trade History — @${U.esc(username)}`, content: ` ${bot ? `
🤖 ${bot.label} — confidence ${bot.score}/100
` : ''}
${trades.length}
Trades
${U.fmtUSD(tB)}
Bought
${U.fmtUSD(tS)}
Sold
${pnl>=0?'+':''}${U.fmtUSD(pnl)}
Est P&L
${topCoins ? `
Top coins: ${topCoins}
` : ''}
${slice.length === 0 ? `

No trades recorded for @${U.esc(username)} yet.

` : `
${rows}
TypeCoinValueQtyDate
`}
`, onBind: m => { U.paginate(document.getElementById('bl-hpg'), { current: page, total }, p => this.open(username, p)); m.querySelector('#bl-ht-tag').addEventListener('click', () => tagModal.open(username)); m.querySelector('#bl-ht-port').addEventListener('click', () => portfolioModal.open(username)); m.querySelector('#bl-ht-copy').addEventListener('click', () => U.copyToast(`https://rugplay.com/user/${username}`, 'Profile link copied')); }, }); }, }; // ─── Modal: Portfolio ───────────────────────────────────────────────────── const portfolioModal = { open(username) { const holdings = portfolio.compute(username); const totalSpent = holdings.reduce((s,h) => s + h.spent, 0); const totalEarned = holdings.reduce((s,h) => s + h.earned, 0); const totalPnl = totalEarned - totalSpent; const active = holdings.filter(h => h.qty > 0.001); const rows = holdings.map(h => ` ${U.esc(h.sym)} ${U.fmtNum(h.qty)} ${U.fmtUSD(h.estValue)} ${U.fmtUSD(h.spent)} ${U.fmtUSD(h.earned)} ${h.pnl>=0?'+':''}${U.fmtUSD(h.pnl)} ${h.buys}B / ${h.sells}S `).join(''); modal.open({ id: 'bl-portmod', title: `Estimated Portfolio — @${U.esc(username)}`, content: `

Estimated from session history. Does not reflect actual on-chain balances.

${holdings.length}
Positions
${active.length}
Active
${U.fmtUSD(totalSpent)}
Total In
${totalPnl>=0?'+':''}${U.fmtUSD(totalPnl)}
Est P&L
${!holdings.length ? `

No holdings in session history.

` : `
${rows}
CoinEst QtyEst ValueSpentEarnedP&LTrades
`}`, }); }, }; // ─── Profile Enhancer ───────────────────────────────────────────────────── const profileEnhancer = { _done: false, async init() { if (!U.isUser() || this._done) return; const header = $('main > div > div > div > div > div > div.bg-card.text-card-foreground.flex.flex-col'); if (!header) return; this._done = true; const [loggedIn, pageUser] = [await U.getLoggedInUser(), U.getPageUser()]; if (!pageUser) return; header.style.position = 'relative'; const wrap = document.createElement('div'); wrap.id = 'bl-profile-actions'; const mkBtn = (label, fn, cls = 'bl-btn-ghost') => { const b = document.createElement('button'); b.className = `bl-btn ${cls} bl-btn-sm`; b.textContent = label; b.addEventListener('click', fn); return b; }; if (loggedIn?.toLowerCase() === pageUser.toLowerCase()) { const a = document.createElement('a'); a.href = '/settings'; a.className = 'bl-btn bl-btn-ghost bl-btn-sm'; a.textContent = 'Edit Profile'; wrap.appendChild(a); } wrap.appendChild(mkBtn('History', () => histModal.open(pageUser))); wrap.appendChild(mkBtn('Portfolio', () => portfolioModal.open(pageUser))); wrap.appendChild(mkBtn('Tag', () => tagModal.open(pageUser))); wrap.appendChild(mkBtn('Copy Link', () => U.copyToast(`https://rugplay.com/user/${pageUser}`, 'Profile link copied'))); header.appendChild(wrap); }, reset() { this._done = false; }, }; // ─── Coin Enhancer ──────────────────────────────────────────────────────── const coinEnhancer = { _tsInt: null, _page: 1, _done: false, init() { if (!U.isCoin() || this._done) return; const sym = U.getCoin(); if (!sym) return; if (document.getElementById('bl-coin-card')) return; const anchor = $$('main div.lg\\:col-span-1 > div.bg-card').find(c => c.textContent.includes('Top Holders')); if (!anchor) return; this._done = true; this._page = 1; // Wallet / Holder Card const wCard = document.createElement('div'); wCard.id = 'bl-wallet-card'; wCard.className = 'bl-card'; wCard.innerHTML = `
Holder Analytics
Loading holders…
`; anchor.insertAdjacentElement('beforebegin', wCard); document.getElementById('bl-wcard-ref').addEventListener('click', () => this.fetchHolders(sym)); this.fetchHolders(sym); // Transaction Card const tCard = document.createElement('div'); tCard.id = 'bl-coin-card'; tCard.className = 'bl-card'; tCard.innerHTML = `
Transactions
Waiting for live trades…
${cfg.get('showPresetBtns') ? `
Buy ${cfg.get('buyPresets').map(amt => ``).join('')} ${cfg.get('showQuickSell') ? ` Sell ${[25,50,75,100].map(p => ``).join('')} ` : ''}
` : ''}`; anchor.insertAdjacentElement('beforebegin', tCard); this._updWl(document.getElementById('bl-cwl'), sym); document.getElementById('bl-cwl').addEventListener('click', () => { const btn = document.getElementById('bl-cwl'); if (wlDB.has(sym)) { wlDB.remove(sym); toaster.show({ type:'info', title:`Removed ${sym} from watchlist` }); } else { wlDB.add(sym); toaster.show({ type:'success', title:`Watching ${sym}` }); } this._updWl(btn, sym); }); document.getElementById('bl-cnb').addEventListener('click', () => { noteModal.open(sym); }); document.getElementById('bl-chide').addEventListener('click', () => { hiddenDB.add(sym); toaster.show({ type:'info', title:`${sym} hidden from tables` }); }); document.getElementById('bl-ccp').addEventListener('click', () => U.copyToast(`https://rugplay.com/coin/${sym}`, 'Coin link copied')); document.getElementById('bl-chs').addEventListener('click', () => this._openSessionModal(sym)); document.getElementById('bl-cr').addEventListener('click', () => { this.renderRisk(sym); this.renderChart(sym); this.fetchPage(sym, 1); }); $$('.bl-quick-buy', tCard).forEach(btn => { btn.addEventListener('click', () => { const inp = $('input[type="number"][min]'); if (inp) { inp.value = btn.dataset.amt; inp.dispatchEvent(new Event('input', { bubbles: true })); } }); }); $$('.bl-quick-sell', tCard).forEach(btn => { btn.addEventListener('click', () => { const slider = $('input[type="range"]'); if (slider) { slider.value = btn.dataset.pct; slider.dispatchEvent(new Event('input', { bubbles: true })); } }); }); this.renderNote(sym); this.renderRisk(sym); this.renderChart(sym); this.fetchPage(sym, 1); this._startTs(); }, async fetchHolders(sym) { const el = document.getElementById('bl-wbody'); if (!el) return; el.innerHTML = 'Loading…'; try { const res = await fetch(`/api/coin/${sym}/holders?limit=10`); if (!res.ok) throw new Error('API fail'); const data = await res.json(); const holders = data?.holders || data || []; if (!holders.length) { el.innerHTML = 'No holder data.'; return; } const total = holders.reduce((s, h) => s + (h.amount || h.balance || 0), 0) || 1; const bar = holders.slice(0,5).map((h,i) => { const pct = ((h.amount || h.balance || 0) / total * 100).toFixed(1); return `
`; }).join(''); const rows = holders.map((h,i) => { const pct = ((h.amount || h.balance || 0) / total * 100).toFixed(1); const u = h.username || h.user?.username || '?'; return `
${U.esc(u)} ${i===0 ? `DEV?` : ''} ${pct}%
`; }).join(''); el.innerHTML = `
${bar}
${rows}`; el.querySelectorAll('[data-copy]').forEach(btn => btn.addEventListener('click', () => U.copyToast(btn.dataset.copy, 'Copied'))); } catch { const trades = tradeDB.forCoin(sym); if (!trades.length) { el.innerHTML = `No data. Watch some trades first.`; return; } const buyQty = {}; trades.filter(t=>t.type==='BUY').forEach(t=>{ buyQty[t.username]=(buyQty[t.username]||0)+t.quantity; }); const sellQty={}; trades.filter(t=>t.type==='SELL').forEach(t=>{ sellQty[t.username]=(sellQty[t.username]||0)+t.quantity; }); const net = Object.fromEntries(Object.entries(buyQty).map(([u,q])=>[u,q-(sellQty[u]||0)])); const sorted = Object.entries(net).filter(([,v])=>v>0).sort((a,b)=>b[1]-a[1]).slice(0,8); const totalQ = sorted.reduce((s,[,v])=>s+v,0)||1; const bar = sorted.slice(0,5).map(([,v],i)=>`
`).join(''); const rows= sorted.map(([u,v],i)=>`
${U.esc(u)}${(v/totalQ*100).toFixed(1)}% est
`).join(''); el.innerHTML = `
Estimated from session history
${bar}
${rows}`; } }, _updWl(btn, sym) { const on = wlDB.has(sym); btn.textContent = on ? 'Watching' : 'Watch'; btn.className = `bl-btn bl-btn-sm ${on ? 'bl-btn-success' : 'bl-btn-ghost'}`; }, renderNote(sym) { const el = document.getElementById('bl-cnote'); if (!el) return; const n = notesDB.get(sym); el.innerHTML = n ? `
📝 ${U.esc(n)}
` : ''; }, renderRisk(sym) { const el = document.getElementById('bl-crisk'); if (!el) return; const sc = rugScorer.score(sym); if (sc === null) { el.innerHTML = `

Accumulating data for risk score…

`; return; } const r = rugScorer.label(sc); el.innerHTML = `
${r.text}
${sc}/100
`; }, renderChart(sym) { const el = document.getElementById('bl-cchart'); if (!el) return; if (!cfg.get('showMiniChart')) { el.innerHTML = ''; return; } const svg = rugScorer.sparkline(sym, 260, 40); el.innerHTML = svg ? `
${svg}
` : ''; }, fetchPage(sym, page = 1) { this._page = page; const trades = tradeDB.forCoin(sym), perPage = 10, total = Math.ceil(trades.length / perPage); this.renderTable(trades.slice((page-1)*perPage, page*perPage)); const pg = document.getElementById('bl-cpg'); if (pg) U.paginate(pg, { current: page, total }, p => this.fetchPage(sym, p)); }, _row(tx) { return ` ${tx.type} ${U.esc(tx.username)}${tagger.html(tx.username)} ${U.fmtUSD(tx.totalValue)} ${U.fmtNum(tx.quantity)} ${U.timeAgo(tx.timestamp)} `; }, renderTable(trades) { const body = document.getElementById('bl-cbody'); if (!body) return; if (!trades.length) { body.innerHTML = `No session transactions yet.`; return; } body.innerHTML = `
${trades.map(t => this._row(t)).join('')}
TypeUserValueQtyTime
`; }, pushNewTrade(t) { if (!cfg.get('autoRefreshCoin')) return; const body = document.getElementById('bl-cbody'); const tbody = body?.querySelector('tbody'); if (!tbody) { this.fetchPage(U.getCoin(), 1); return; } if (tbody.querySelector(`tr[data-id="${U.esc(t.id||'')}"]`)) return; const row = document.createElement('tr'); row.className = 'bl-new-row'; row.dataset.ts = t.timestamp; row.dataset.id = t.id || ''; const b = t.type?.toUpperCase() === 'BUY'; row.innerHTML = `${b?'BUY':'SELL'}${U.esc(t.username||'')}${tagger.html(t.username||'')}${U.fmtUSD(parseFloat(t.totalValue))}${U.fmtNum(t.quantity)}${U.timeAgo(t.timestamp)}`; tbody.prepend(row); while (tbody.children.length > 10) tbody.lastChild.remove(); this.renderRisk(U.getCoin()); this.renderChart(U.getCoin()); }, _openSessionModal(sym) { const trades = tradeDB.forCoin(sym); const buys = trades.filter(t=>t.type==='BUY'), sells = trades.filter(t=>t.type==='SELL'); const tB = buys.reduce((s,t)=>s+t.totalValue,0), tS = sells.reduce((s,t)=>s+t.totalValue,0); const topMap = {}; trades.forEach(t=>{ topMap[t.username]=(topMap[t.username]||0)+1; }); const top3 = Object.entries(topMap).sort((a,b)=>b[1]-a[1]).slice(0,3) .map(([u,c])=>`${u} (${c})`).join(' · '); modal.open({ id: 'bl-sessmod', title: `Session — ${U.esc(sym)}`, content: `
${trades.length}
Trades
${U.fmtUSD(tB)}
Buy Vol
${U.fmtUSD(tS)}
Sell Vol
${top3 ? `
Most active: ${top3}
` : ''}`, }); }, _startTs() { this._stopTs(); this._tsInt = setInterval(() => { $$('#bl-cbody .bl-ts').forEach(el => { const ts = el.closest('tr')?.dataset.ts; if (ts) el.textContent = U.timeAgo(Number(ts)); }); }, 1000); }, _stopTs() { if (this._tsInt) { clearInterval(this._tsInt); this._tsInt = null; } }, reset() { this._done = false; this._stopTs(); document.getElementById('bl-coin-card')?.remove(); document.getElementById('bl-wallet-card')?.remove(); }, }; // ───────────────────────────────────────────────────────────────────────── // CENTER PAGE — App Shell // // Architecture: // #bl-app // ├── #bl-nav (200px fixed left nav) // └── #bl-main (scrollable content) // ├── .bl-page-header // └── .bl-content // └── .bl-pane.active (swapped by nav) // ───────────────────────────────────────────────────────────────────────── const CHANGELOG = [ { v: '4.0.0', date: 'Mar 2025', items: [ 'Complete UI redesign — token-based design system, intentional hierarchy', 'App-shell layout for center page with persistent left navigation', 'Session state persistence — feed open state survives page refresh', 'Replaced noisy top bar with compact status pill', 'Feed tray with always-visible bottom tab (no overlay on hotkeys bar)', 'Command palette search (⌘S) with categorized results', 'Removed hotkey bar clutter — help available in settings', 'Improved toast design with colored left accent stripe', ], }, { v: '3.1.0', date: 'Mar 2025', items: [ 'Ad blocker (CSS-based, toggleable)', 'Live portfolio refresh with flash animation on change', 'Appear offline toggle (hides DM presence)', 'Sticky portfolio sidebar option', 'Rugpuller badge on coin pages using tag DB + community reports', 'Community rugpull reporter — fully local, exportable', 'Changelog tab in center page', ], }, { v: '3.0.0', date: 'Mar 2025', items: [ 'Portfolio estimator from session trade history', 'Coin + user leaderboard', 'Global search panel', 'AI bot detection with auto-tagging', 'Volume spike alerts', 'Wallet holder analytics card', 'Quick buy / sell buttons on coin pages', 'Hotkey system', 'Enhanced dark mode', ], }, ]; const centerPage = { visible: false, _orig: [], _pane: null, init() { window.addEventListener('hashchange', () => { if (location.hash === '#bl-center' && !this.visible) this.show(); else if (location.hash !== '#bl-center' && this.visible) this.hide(); }); }, show() { if (this.visible) return; const main = $('main'); if (!main) return; this._orig = Array.from(main.children); this._orig.forEach(c => c.style.display = 'none'); const wrap = document.createElement('div'); wrap.id = 'bl-ctr-wrap'; wrap.innerHTML = this._render(); main.appendChild(wrap); this.visible = true; if (location.hash !== '#bl-center') location.hash = 'bl-center'; this._bind(); // Restore saved pane this.showPane(session.get('centerTab') || 'overview'); }, hide() { if (!this.visible) return; document.getElementById('bl-ctr-wrap')?.remove(); this._orig.forEach(c => c.style.display = ''); this._orig = []; this.visible = false; if (location.hash === '#bl-center') history.pushState('', document.title, location.pathname + location.search); }, showPane(id) { if (!this.visible) { this.show(); } this._pane = id; session.set('centerTab', id); $$('.bl-nav-item').forEach(el => el.classList.toggle('active', el.dataset.pane === id)); $$('.bl-pane').forEach(p => p.classList.toggle('active', p.id === `bl-pane-${id}`)); }, // Convenience alias for external callers showTab(id) { this.showPane(id); }, _tog(key, label, desc) { const checked = cfg.get(key) ? 'checked' : ''; return `
${label}
${desc}
`; }, _render() { const st = tradeDB.stats(); const tags = tagDB.all(); const wl = wlDB.all(); const lb = tradeDB.coinLeaderboard(20); const ub = tradeDB.userLeaderboard(20); const notes= notesDB.all(); const reps = communityReports.all(); const s = cfg.all(); const nav = (id, icon, label, badge = '') => ` `; // ── Leaderboard rows ── const maxVol = lb[0]?.volume || 1; const lbRows = lb.map((item, i) => ` ${i+1} ${U.esc(item.sym)} ${item.trades} ${U.fmtUSD(item.volume)}
View `).join(''); const ubRows = ub.map((item, i) => { const d = tagDB.get(item.username), p = d ? TAG_PRESETS[d.tag] : null; const bot = botDetector.analyze(item.username); return ` ${i+1} ${U.esc(item.username)}${p?`${p.emoji}`:''}${bot&&s.showBotBadges?`🤖`:''} ${item.trades} ${U.fmtUSD(item.volume)} `; }).join(''); // ── Tag rows ── const tagRows = Object.entries(tags).map(([u, d]) => { const p = TAG_PRESETS[d.tag] || TAG_PRESETS.DEV; return `
@${U.esc(d.orig||u)} ${p.emoji} ${p.label}
`; }).join('') || `

No tags yet.

`; // ── Watchlist rows ── const wlRows = wl.map(sym => { const sc = rugScorer.score(sym), r = sc !== null ? rugScorer.label(sc) : null; const note = notesDB.get(sym); return `
${U.esc(sym)} ${r ? `${r.text}` : ''} ${note ? `📝` : ''}
View
`; }).join('') || `

No coins watched yet.

`; // ── Notes rows ── const noteRows = Object.entries(notes).map(([sym, txt]) => `
${U.esc(sym)} ${U.esc(txt.slice(0,60))}${txt.length>60?'…':''}
` ).join('') || `

No notes yet.

`; // ── Report rows ── const repRows = reps.slice(0,10).map(r => { const myVote = communityReports.getVote(r.id); return `
@${U.esc(r.username)} · *${U.esc(r.coinSymbol||'?')}
${U.esc(r.description)}
${U.fmtDate(r.timestamp)}
`; }).join('') || `

No reports yet.

`; // ── Changelog ── const clHtml = CHANGELOG.map(entry => `
v${entry.v} ${entry.date}
`).join(''); return `

Overview

Live session intelligence — ${st.total} trades captured

${U.fmtUSD(st.volume)}
Volume Seen
${st.buys}
Buys
${st.sells}
Sells
${st.coins}
Unique Coins
${st.users}
Unique Users
${tagDB.count()}
Tagged
${wl.length}
Watching
${Object.keys(notes).length}
Notes
Top Coins This Session
Coins ranked by session volume
${lb.length ? `
${lbRows}
#CoinTradesVolumeActivity
` : `

No data yet — trades appear as you browse Rugplay.

`}
Most Active Users
Traders ranked by session activity
${ub.length ? `
${ubRows}
#UserTradesVolume
` : `

No users seen yet.

`}

Leaderboard

Session activity rankings

Coin Leaderboard
Coins by session volume
${lb.length ? `
${lbRows}
#CoinTradesVolumeActivity
` : `

No data yet.

`}
User Leaderboard
Traders by session activity
${ub.length ? `
${ubRows}
#UserTradesVolume
` : `

No data yet.

`}

Portfolio

Estimated holdings from session trade history

Estimate Portfolio
Enter a username to compute estimated holdings based on session-captured trades. Not financial data — session only.

Settings

Customize Blackline+ behavior

Live Feed
${this._tog('liveToasts', 'Live Toasts', 'Show notifications on every trade')} ${this._tog('toastBuys', 'Notify Buys', 'Include buy trades in notifications')} ${this._tog('toastSells', 'Notify Sells', 'Include sell trades in notifications')} ${this._tog('autoRefreshCoin', 'Auto-Refresh Coin Page', 'Push new trades to transaction card live')}
Toast Duration
${(s.toastDuration/1000).toFixed(1)}s
Alerts
${this._tog('rugAlerts', 'Rugpull Alerts', 'Alert when AI risk score exceeds threshold')} ${this._tog('watchlistAlerts', 'Watchlist Alerts', 'Alert on trades for watched coins')} ${this._tog('volumeSpikeAlerts', 'Volume Spikes', 'Alert on sudden volume increases')}
Risk Threshold
Alert when score ≥ ${s.rugThreshold}
Interface
${this._tog('notifBadges', 'Notification Badges', 'Show unread count on sidebar icon')} ${this._tog('clickableRows', 'Clickable Rows', 'Click table rows to navigate to coin')} ${this._tog('highlightWatched','Watchlist Highlight', 'Mark watched coins in tables')} ${this._tog('showMiniChart', 'Sparkline Chart', 'Trade activity chart on coin pages')} ${this._tog('darkEnhance', 'Enhanced Dark Mode', 'Deeper background colors')} ${this._tog('compactMode', 'Compact Mode', 'Reduce padding throughout')} ${this._tog('showBotBadges', 'Bot Badges', 'Show bot detection labels on users')}
Privacy & QoL
${this._tog('adBlocker', 'Ad Blocker', 'Hide ads across Rugplay')} ${this._tog('livePortfolioRefresh', 'Live Portfolio Refresh', 'Flash sidebar portfolio values on trades')} ${this._tog('appearOffline', 'Appear Offline', 'Hide DM online presence status')} ${this._tog('stickyPortfolio', 'Sticky Portfolio Sidebar', 'Pin portfolio to sidebar footer')} ${this._tog('showRugpullerBadge', 'Rugpuller Badge', 'Flag creators on coins you have reported')}
Trading Tools
${this._tog('showPresetBtns', 'Quick Buy Buttons', 'Preset buy amounts on coin pages')} ${this._tog('showQuickSell', 'Quick Sell Buttons', 'Quick sell percentages on coin pages')}
Buy Presets ($)
Comma-separated amounts
Hotkeys
Keys active when not typing in an input
Open Blackline+${s.hkCenter}
Toggle Live Feed${s.hkFeed}
Open Search${s.hkSearch}
Close / DismissEsc

Tags

Label and categorize users

Add Tag
Tagged Users ${tagDB.count()}
${tagRows}

Watchlist

Monitor specific coins for activity

Add Coin
Watching ${wl.length}
${wlRows}

Notes

Private notes attached to coins

Notes ${Object.keys(notes).length}
${noteRows}

Reports

Community rugpull reports — stored locally, exportable

File a Report
Reports are stored locally and can be exported to share with others.
Community Reports ${reps.length}
${repRows}

Changelog

What's new in each version

${clHtml}

Data

Manage stored session data

Trade History
Stored locally, max ${MAX_HISTORY.toLocaleString()} records.
${st.total}
Records
Export
Download your data as JSON.
Import
Restore a Blackline+ export file.
Danger Zone
Wipe all Blackline+ data. Cannot be undone.
`; }, _bind() { const w = document.getElementById('bl-ctr-wrap'); if (!w) return; // Nav $$('.bl-nav-item[data-pane]', w).forEach(btn => { btn.addEventListener('click', () => this.showPane(btn.dataset.pane)); }); document.getElementById('bl-nav-close')?.addEventListener('click', () => this.hide()); // Close on Esc handled by hotkeys // Settings — toggles $$('[data-cfg]', w).forEach(inp => { inp.addEventListener('change', () => { cfg.set(inp.dataset.cfg, inp.checked); notifBadges.apply(); document.body.classList.toggle('bl-dark', cfg.get('darkEnhance')); document.body.classList.toggle('bl-compact', cfg.get('compactMode')); document.body.classList.toggle('bl-no-ads', cfg.get('adBlocker')); if (inp.dataset.cfg === 'appearOffline') appearOffline.apply(inp.checked); if (inp.dataset.cfg === 'stickyPortfolio') stickyPortfolio.apply(); }); }); // Settings — sliders const tdSlider = document.getElementById('bl-td'); const tdLabel = document.getElementById('bl-td-lbl'); if (tdSlider && tdLabel) { tdSlider.addEventListener('input', () => { cfg.set('toastDuration', parseInt(tdSlider.value)); tdLabel.textContent = `${(tdSlider.value/1000).toFixed(1)}s`; }); } const rtSlider = document.getElementById('bl-rt'); const rtLabel = document.getElementById('bl-rt-lbl'); if (rtSlider && rtLabel) { rtSlider.addEventListener('input', () => { cfg.set('rugThreshold', parseInt(rtSlider.value)); rtLabel.textContent = `Alert when score ≥ ${rtSlider.value}`; }); } // Settings — presets document.getElementById('bl-presets-save')?.addEventListener('click', () => { const raw = document.getElementById('bl-presets-in')?.value; const presets = raw?.split(',').map(v => parseFloat(v.trim())).filter(v => !isNaN(v) && v > 0); if (presets?.length) { cfg.set('buyPresets', presets); toaster.show({ type:'success', title:'Buy presets saved' }); } }); // Overview — user search const doUserSearch = () => { const q = document.getElementById('bl-usrch')?.value.trim(); if (q) histModal.open(q); }; document.getElementById('bl-usrch-go')?.addEventListener('click', doUserSearch); document.getElementById('bl-usrch')?.addEventListener('keydown', e => { if (e.key==='Enter') doUserSearch(); }); // Overview / Leaderboard — history buttons $$('[data-hist]', w).forEach(btn => btn.addEventListener('click', () => histModal.open(btn.dataset.hist))); // Portfolio document.getElementById('bl-port-go')?.addEventListener('click', () => { const u = document.getElementById('bl-port-user')?.value.trim(); if (!u) return; const h = portfolio.compute(u); const el = document.getElementById('bl-port-result'); if (!el) return; if (!h.length) { el.innerHTML = `

No holdings found for @${U.esc(u)} in session history.

`; return; } const totalSpent = h.reduce((s,x)=>s+x.spent,0), totalPnl = h.reduce((s,x)=>s+x.earned,0)-totalSpent; const rows = h.map(p => ` ${U.esc(p.sym)} ${U.fmtNum(p.qty)} ${U.fmtUSD(p.estValue)} ${U.fmtUSD(p.spent)} ${U.fmtUSD(p.earned)} ${p.pnl>=0?'+':''}${U.fmtUSD(p.pnl)} `).join(''); el.innerHTML = `
${h.length}
Positions
${U.fmtUSD(totalSpent)}
Total In
${totalPnl>=0?'+':''}${U.fmtUSD(totalPnl)}
Est P&L
${rows}
CoinEst QtyEst ValueSpentEarnedP&L
`; }); document.getElementById('bl-port-user')?.addEventListener('keydown', e => { if (e.key==='Enter') document.getElementById('bl-port-go')?.click(); }); // Tags document.getElementById('bl-ta')?.addEventListener('click', () => { const u = document.getElementById('bl-ti')?.value.trim().replace('@',''); const t = document.getElementById('bl-ts')?.value; if (!u || !t) { toaster.show({ type:'warn', title:'Enter a username and select a tag' }); return; } tagDB.set(u, t, u); const p = TAG_PRESETS[t]; const list = document.getElementById('bl-tlist'); if (list) { const row = document.createElement('div'); row.className = 'bl-list-row'; row.dataset.tr = u; row.innerHTML = `@${U.esc(u)} ${p.emoji} ${p.label}
`; row.querySelector('[data-vh]').addEventListener('click', () => histModal.open(u)); row.querySelector('[data-untag]').addEventListener('click', () => { tagDB.remove(u); row.remove(); }); list.prepend(row); } document.getElementById('bl-ti').value = ''; toaster.show({ type:'success', title:`Tagged @${u} as ${p.label}` }); }); document.getElementById('bl-ti')?.addEventListener('keydown', e => { if (e.key==='Enter') document.getElementById('bl-ta')?.click(); }); document.getElementById('bl-tlist')?.addEventListener('click', e => { const vh = e.target.closest('[data-vh]'), ut = e.target.closest('[data-untag]'); if (vh) histModal.open(vh.dataset.vh); if (ut) { tagDB.remove(ut.dataset.untag); document.querySelector(`[data-tr="${ut.dataset.untag}"]`)?.remove(); } }); // Watchlist document.getElementById('bl-wa')?.addEventListener('click', () => { const sym = document.getElementById('bl-wi')?.value.trim().toUpperCase(); if (!sym) return; wlDB.add(sym); const list = document.getElementById('bl-wlist'); if (list) { const row = document.createElement('div'); row.className = 'bl-list-row'; row.dataset.wr = sym; row.innerHTML = `
${U.esc(sym)}
View
`; list.prepend(row); } document.getElementById('bl-wi').value = ''; toaster.show({ type:'success', title:`Watching ${sym}` }); }); document.getElementById('bl-wi')?.addEventListener('keydown', e => { if (e.key==='Enter') document.getElementById('bl-wa')?.click(); }); document.getElementById('bl-wlist')?.addEventListener('click', e => { const unwl = e.target.closest('[data-unwl]'), wln = e.target.closest('[data-wlnote]'); if (unwl) { wlDB.remove(unwl.dataset.unwl); document.querySelector(`[data-wr="${unwl.dataset.unwl}"]`)?.remove(); } if (wln) noteModal.open(wln.dataset.wlnote); }); // Notes document.getElementById('bl-nlist')?.addEventListener('click', e => { const en = e.target.closest('[data-en]'), dn = e.target.closest('[data-dn]'); if (en) noteModal.open(en.dataset.en); if (dn) { notesDB.remove(dn.dataset.dn); document.querySelector(`[data-nr="${dn.dataset.dn}"]`)?.remove(); toaster.show({ type:'info', title:'Note deleted' }); } }); // Reports const renderReportList = (page = 1) => { const all = communityReports.all(); const perPage = 8, total = Math.ceil(all.length / perPage); const slice = all.slice((page-1)*perPage, page*perPage); const cnt = document.getElementById('bl-rp-cnt'); if (cnt) cnt.textContent = all.length; const list = document.getElementById('bl-rp-list'); if (!list) return; if (!slice.length) { list.innerHTML = `

No reports yet.

`; return; } list.innerHTML = slice.map(r => { const mv = communityReports.getVote(r.id); return `
@${U.esc(r.username)} · *${U.esc(r.coinSymbol||'?')}
${U.esc(r.description)}
${U.fmtDate(r.timestamp)}
`; }).join(''); list.querySelectorAll('.bl-vote').forEach(btn => { btn.addEventListener('click', () => { if (btn.classList.contains('voted')) return; const rid = btn.closest('[data-rid]')?.dataset.rid; if (communityReports.vote(rid, btn.dataset.vote)) { const span = btn.querySelector('span'); span.textContent = parseInt(span.textContent) + 1; btn.closest('[data-rid]').querySelectorAll('.bl-vote').forEach(b => b.classList.add('voted')); btn.classList.add('selected'); } }); }); const pg = document.getElementById('bl-rp-pg'); if (pg) U.paginate(pg, { current: page, total }, renderReportList); }; renderReportList(); document.getElementById('bl-rp-sub')?.addEventListener('click', () => { const u = document.getElementById('bl-rp-user')?.value.trim(); const sym = document.getElementById('bl-rp-sym')?.value.trim(); const desc = document.getElementById('bl-rp-desc')?.value.trim(); const msg = document.getElementById('bl-rp-msg'); if (!u || !sym || !desc) { if (msg) { msg.textContent = 'All fields are required.'; msg.style.color = 'var(--bl-red)'; } return; } communityReports.add({ username: u, coinSymbol: sym, description: desc }); ['bl-rp-user','bl-rp-sym','bl-rp-desc'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); if (msg) { msg.textContent = 'Report submitted.'; msg.style.color = 'var(--bl-green)'; setTimeout(() => { msg.textContent=''; }, 3000); } renderReportList(); toaster.show({ type:'success', title:`Report filed for @${u}` }); }); document.getElementById('bl-rp-exp')?.addEventListener('click', () => this._dl('bl4_reports.json', communityReports.all())); // Data document.getElementById('bl-ch')?.addEventListener('click', () => { if (confirm('Clear all trade history? Cannot be undone.')) { tradeDB.clear(); toaster.show({ type:'info', title:'History cleared' }); } }); document.getElementById('bl-eh')?.addEventListener('click', () => this._dl('bl4_history.json', tradeDB.all())); document.getElementById('bl-et')?.addEventListener('click', () => this._dl('bl4_tags_notes.json', { tags: tagDB.all(), notes: notesDB.all() })); document.getElementById('bl-ew')?.addEventListener('click', () => this._dl('bl4_watchlist.json', wlDB.all())); document.getElementById('bl-ea')?.addEventListener('click', () => this._dl('bl4_export.json', { history: tradeDB.all(), tags: tagDB.all(), notes: notesDB.all(), watchlist: wlDB.all(), settings: cfg.all(), reports: communityReports.all(), v: BL_VERSION, at: new Date().toISOString(), })); document.getElementById('bl-ib')?.addEventListener('click', () => { const f = document.getElementById('bl-if')?.files[0]; if (!f) return; const r = new FileReader(); r.onload = e => { try { const d = JSON.parse(e.target.result); if (d.history) store.set(K.history, d.history); if (d.tags) store.set(K.tags, d.tags); if (d.notes) store.set(K.notes, d.notes); if (d.watchlist)store.set(K.watchlist,d.watchlist); if (d.settings) store.set(K.settings, d.settings); if (d.reports) store.set(K.reports, d.reports); tradeDB._c = null; cfg._c = null; toaster.show({ type:'success', title:'Imported. Reload to apply all changes.' }); } catch { toaster.show({ type:'error', title:'Import failed — invalid file.' }); } }; r.readAsText(f); }); document.getElementById('bl-ra')?.addEventListener('click', () => { if (confirm('Reset ALL Blackline+ data? This cannot be undone.')) { Object.values(K).forEach(k => store.del(k)); tradeDB._c = null; cfg._c = null; toaster.show({ type:'info', title:'Reset complete. Reloading…' }); setTimeout(() => location.reload(), 1200); } }); }, _dl(name, data) { const b = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const u = URL.createObjectURL(b); const a = document.createElement('a'); a.href = u; a.download = name; a.click(); URL.revokeObjectURL(u); toaster.show({ type:'copy', title:`Exported ${name}` }); }, }; // ─── URL Shortcuts ──────────────────────────────────────────────────────── (function () { const p = location.pathname; const um = p.match(/^\/@([a-zA-Z0-9_.\-]+)$/); if (um) { location.replace(`/user/${um[1]}`); return; } const cm = p.match(/^\/\*([A-Za-z0-9]+)$/); if (cm) location.replace(`/coin/${cm[1].toUpperCase()}`); })(); // ───────────────────────────────────────────────────────────────────────── // BOOT // ───────────────────────────────────────────────────────────────────────── async function boot() { if (document.readyState === 'loading') await new Promise(r => document.addEventListener('DOMContentLoaded', r)); // Core UI toaster.init(); feedPanel.init(); searchPanel.init(); hotkeys.init(); centerPage.init(); statusPill.init(); // Apply persistent settings if (cfg.get('darkEnhance')) document.body.classList.add('bl-dark'); if (cfg.get('compactMode')) document.body.classList.add('bl-compact'); if (cfg.get('adBlocker')) document.body.classList.add('bl-no-ads'); if (cfg.get('appearOffline')) appearOffline.apply(true); // Reactive enhancer — runs on DOM mutations and URL changes const run = U.debounce(async () => { sidebar.inject(); notifBadges.apply(); tagger.run(); tableEnhancer.run(); stickyPortfolio.apply(); rugpullerBadge.run(); await profileEnhancer.init(); coinEnhancer.init(); statusPill.update(); // Handle hash routing if (location.hash === '#bl-center' && !centerPage.visible) centerPage.show(); else if (location.hash !== '#bl-center' && centerPage.visible) centerPage.hide(); }, 300); new MutationObserver(run).observe(document.body, { childList: true, subtree: true }); new URLWatcher().on(() => { profileEnhancer.reset(); coinEnhancer.reset(); rugpullerBadge.reset(); run(); }); run(); } boot(); })();