Dasgar YT

Skin uploader (URL/file), 512x512, KB cap, local mod-library + inject+save to agar.io account, editor upload button placed inside site editor modal (best-effort). No cheats.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Dasgar YT
// @namespace    https://violentmonkey.github.io/
// @version      1.1
// @description  Skin uploader (URL/file), 512x512, KB cap, local mod-library + inject+save to agar.io account, editor upload button placed inside site editor modal (best-effort). No cheats.
// @match        *://agar.io/*
// @match        *://*.agar.io/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  /* ---------- Config ---------- */
  const MAX_SIZE_BYTES = 512 * 1024; // 512 KB upload UI cap
  const FINAL_SIZE = 512;            // final canvas size 512x512
  const LIB_KEY = 'dasgar_mod_skins_v1';
  const CACHED_KEY = 'dasgar_cached_skin_v1';

  /* ---------- Styles ---------- */
  GM_addStyle(`
    #dasgar_menu {
      position: fixed;
      top: 18px;
      right: 18px;
      width: 360px;
      z-index: 2147483647;
      background: rgba(12,12,14,0.94);
      color: #eaf8ff;
      padding: 12px;
      border-radius: 10px;
      font-family: Arial, sans-serif;
      font-size: 13px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.6)
    }
    #dasgar_menu h3 { margin:0 0 8px 0; color:#7ef1c7; }
    #dasgar_preview { width:120px; height:120px; border-radius:50%; display:block; margin:8px auto; background:#222; object-fit:cover; border:6px solid #ff7700; }
    #dasgar_menu input[type="text"], #dasgar_menu input[type="number"], #dasgar_menu select {
      width:100%; padding:7px; border-radius:6px; border:1px solid rgba(255,255,255,0.06); background:rgba(255,255,255,0.02); color:#fff;
    }
    #dasgar_menu input[type="file"]{ width:100%; margin-top:6px; }
    #dasgar_menu button { width:100%; padding:8px; border-radius:8px; border:none; background:#2b8cff; color:#fff; font-weight:700; cursor:pointer; margin-top:8px; }
    .das-row { display:flex; gap:8px; align-items:center; margin-top:8px; }
    .das-small { padding:6px 8px; border-radius:8px; border:none; background:#00aaff; color:#fff; cursor:pointer; }
    .skin-entry { display:flex; gap:8px; align-items:center; padding:6px; margin-top:6px; background: rgba(255,255,255,0.03); border-radius:8px; }
    .skin-entry img { width:40px; height:40px; border-radius:50%; object-fit:cover; border:2px solid rgba(0,0,0,0.18); }
    #dasgar_editor_inject { position:absolute; right:18px; bottom:18px; z-index:2147483648; }
    #dasgar_status { margin-top:6px; color:#cfefff; font-size:12px; text-align:center; }
  `);

  /* ---------- UI Build ---------- */
  const menu = document.createElement('div');
  menu.id = 'dasgar_menu';
  menu.innerHTML = `
    <h3>Dasgar YT — Skins</h3>

    <label>Skin URL (direct image)</label>
    <input type="text" id="dasgar_url" placeholder="https://.../image.png">

    <label>Upload image (max 512 KB)</label>
    <input type="file" id="dasgar_file" accept="image/*">

    <canvas id="dasgar_preview" width="120" height="120"></canvas>

    <div class="das-row">
      <input type="color" id="dasgar_bg" value="#ffcc00" style="width:58px; height:36px; border-radius:6px; border:none;">
      <input type="color" id="dasgar_border" value="#ff7700" style="width:58px; height:36px; border-radius:6px; border:none;">
      <input type="number" id="dasgar_border_w" value="6" min="0" max="40" style="width:70px; padding:6px; border-radius:6px;">
      <select id="dasgar_size" style="flex:1; padding:6px; border-radius:6px;">
        <option value="512">512 px (final)</option>
        <option value="256">256 px</option>
        <option value="128">128 px</option>
      </select>
    </div>

    <button id="dasgar_convert">Convert & Add to Library</button>
    <button id="dasgar_apply_site" style="background:#32cc6b">Upload → Save to agar.io</button>

    <h4 style="margin-top:10px">Saved (mod library)</h4>
    <div id="dasgar_library"></div>

    <div id="dasgar_status">Load via URL or Upload → Convert → Save to library or save into agar.io.</div>
  `;
  document.body.appendChild(menu);

  // preview canvas
  const preview = document.getElementById('dasgar_preview');
  const pctx = preview.getContext('2d');

  const urlInput = document.getElementById('dasgar_url');
  const fileInput = document.getElementById('dasgar_file');
  const bgInput = document.getElementById('dasgar_bg');
  const borderInput = document.getElementById('dasgar_border');
  const borderWInput = document.getElementById('dasgar_border_w');
  const sizeSelect = document.getElementById('dasgar_size');
  const convertBtn = document.getElementById('dasgar_convert');
  const applySiteBtn = document.getElementById('dasgar_apply_site');
  const libDiv = document.getElementById('dasgar_library');
  const statusEl = document.getElementById('dasgar_status');

  /* ---------- State ---------- */
  let loadedImage = null;   // HTMLImageElement or HTMLCanvasElement used for preview
  let cachedFinal = null;   // canvas 512/256/... final result stored
  // helper: write status
  function setStatus(t){ statusEl.innerText = t; console.log('[DasgarSkin]', t); }

  /* ---------- Small helpers ---------- */
  function clearPreview(){ pctx.clearRect(0,0,preview.width, preview.height); pctx.fillStyle = '#333'; pctx.fillRect(0,0,preview.width, preview.height); }
  clearPreview();
  function drawPreviewFromImage(img){
    const s = preview.width;
    pctx.clearRect(0,0,s,s);
    pctx.save();
    pctx.beginPath(); pctx.arc(s/2,s/2,s/2,0,Math.PI*2); pctx.closePath(); pctx.clip();
    const scale = Math.max(s/img.width, s/img.height);
    const iw = img.width * scale, ih = img.height * scale;
    pctx.drawImage(img, s/2 - iw/2, s/2 - ih/2, iw, ih);
    pctx.restore();
    // border
    const bw = Math.max(0, Math.min(40, parseInt(borderWInput.value) || 0));
    if (bw > 0){
      pctx.beginPath(); pctx.arc(s/2,s/2, s/2 - bw/2,0,Math.PI*2); pctx.lineWidth = bw; pctx.strokeStyle = borderInput.value || '#ff7700'; pctx.stroke();
    }
    preview.style.background = bgInput.value;
  }

  /* ---------- load image from URL helper (tries multiple ways) ---------- */
  function loadImageFromURL(url){
    return new Promise((resolve,reject) => {
      if (!url) return reject(new Error('No URL'));
      // 1) direct load
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => {
        // 2) try with crossOrigin anonymous
        const img2 = new Image();
        img2.crossOrigin = 'anonymous';
        img2.onload = () => resolve(img2);
        img2.onerror = () => {
          // 3) try fetch → blob (some servers permit)
          fetch(url).then(r => {
            if (!r.ok) throw new Error('fetch failed');
            return r.blob();
          }).then(blob => {
            const obj = URL.createObjectURL(blob);
            const img3 = new Image();
            img3.onload = () => { URL.revokeObjectURL(obj); resolve(img3); };
            img3.onerror = () => { URL.revokeObjectURL(obj); reject(new Error('blob load failed')); };
            img3.src = obj;
          }).catch(err => {
            reject(err);
          });
        };
        img2.src = url;
      };
      img.src = url;
    });
  }

  /* ---------- file input handling ---------- */
  fileInput.addEventListener('change', (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    if (f.size > MAX_SIZE_BYTES){
      setStatus('File too large (max ' + Math.round(MAX_SIZE_BYTES/1024) + ' KB).');
      return;
    }
    setStatus('Loading file for preview...');
    const obj = URL.createObjectURL(f);
    const img = new Image();
    img.onload = () => {
      loadedImage = img;
      drawPreviewFromImage(img);
      setStatus('File ready for conversion.');
      URL.revokeObjectURL(obj);
    };
    img.onerror = () => {
      setStatus('File could not be loaded.');
      URL.revokeObjectURL(obj);
    };
    img.src = obj;
  });

  /* ---------- URL input handling ---------- */
  urlInput.addEventListener('change', async () => {
    const u = urlInput.value.trim();
    if (!u) return;
    setStatus('Loading image from URL...');
    try {
      const img = await loadImageFromURL(u);
      loadedImage = img;
      drawPreviewFromImage(img);
      setStatus('URL loaded for preview.');
    } catch (err) {
      console.warn(err);
      setStatus('Failed to load URL (CORS or blocked). Try download & upload file instead.');
    }
  });

  /* ---------- convert to final canvas (512/256/128) ---------- */
  function makeFinalCanvas(srcImg, size, borderW, borderColor, bgColor){
    const c = document.createElement('canvas'); c.width = c.height = size;
    const ctx = c.getContext('2d');
    // fill bg
    ctx.fillStyle = bgColor || '#fff'; ctx.fillRect(0,0,size,size);
    // circular clip and draw
    ctx.save();
    ctx.beginPath(); ctx.arc(size/2, size/2, size/2 - 1, 0, Math.PI*2); ctx.closePath(); ctx.clip();
    if (srcImg instanceof HTMLCanvasElement) ctx.drawImage(srcImg, 0, 0, size, size);
    else {
      const scale = Math.max(size / srcImg.width, size / srcImg.height);
      const iw = srcImg.width * scale, ih = srcImg.height * scale;
      ctx.drawImage(srcImg, size/2 - iw/2, size/2 - ih/2, iw, ih);
    }
    ctx.restore();
    // stroke border inside circle
    const bw = Math.max(0, Math.min(40, borderW || 0));
    if (bw > 0) {
      ctx.beginPath(); ctx.arc(size/2, size/2, size/2 - bw/2 - 1, 0, Math.PI*2);
      ctx.lineWidth = bw; ctx.strokeStyle = borderColor || '#ff7700'; ctx.stroke();
    }
    return c;
  }

  /* ---------- library handling ---------- */
  function saveToLibrary(name, dataURL){
    const lib = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
    lib.push({ name: name || ('Skin ' + (lib.length+1)), data: dataURL, ts: Date.now() });
    try { localStorage.setItem(LIB_KEY, JSON.stringify(lib)); setStatus('Saved to mod library.'); renderLibrary(); } catch (e) { setStatus('Save failed (storage).'); }
  }

  function renderLibrary(){
    libDiv.innerHTML = '';
    const lib = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
    if (!lib.length) { libDiv.innerHTML = '<div style="color:#bcd7ee;font-size:12px">No saved skins yet.</div>'; return; }
    lib.forEach((s, idx) => {
      const row = document.createElement('div'); row.className = 'skin-entry';
      row.innerHTML = `<div style="display:flex;align-items:center;gap:8px"><img src="${s.data}"><div style="font-size:12px">${s.name}</div></div>
        <div style="display:flex;gap:6px">
          <button data-idx="${idx}" class="das-row use">Use</button>
          <button data-idx="${idx}" class="das-row inject" style="background:#32cc6b">Upload→Agar</button>
          <button data-idx="${idx}" class="das-row del" style="background:#ff4d4d">Del</button>
        </div>`;
      libDiv.appendChild(row);
    });

    // attach events
    libDiv.querySelectorAll('button.use').forEach(b => b.addEventListener('click', e => {
      const i = +e.currentTarget.dataset.idx;
      const lib = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
      const item = lib[i];
      if (item) {
        // set preview & cachedFinal
        loadedImage = new Image();
        loadedImage.onload = () => { drawPreviewFromImage(loadedImage); setStatus('Loaded saved skin in preview.'); };
        loadedImage.src = item.data;
        cachedFinal = null;
      }
    }));

    libDiv.querySelectorAll('button.del').forEach(b => b.addEventListener('click', e => {
      const i = +e.currentTarget.dataset.idx;
      const lib = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
      lib.splice(i,1);
      localStorage.setItem(LIB_KEY, JSON.stringify(lib));
      renderLibrary();
      setStatus('Deleted saved skin.');
    }));

    libDiv.querySelectorAll('button.inject').forEach(b => b.addEventListener('click', async (e) => {
      const i = +e.currentTarget.dataset.idx;
      const lib = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
      const item = lib[i];
      if (!item) return;
      // load image element
      try {
        const img = new Image();
        img.onload = async () => {
          // create final canvas from saved data (should already be final, but ensure proper size)
          const finalSize = parseInt(sizeSelect.value,10) || FINAL_SIZE;
          const canvas = makeFinalCanvas(img, finalSize, parseInt(borderWInput.value,10) || 0, borderInput.value, bgInput.value);
          cachedFinal = canvas;
          setStatus('Prepared to inject into site editor. Attempting injection & Save...');
          const ok = await injectIntoEditorAndSave(canvas);
          setStatus(ok ? 'Attempt to save on site done (check site).' : 'Failed to find site editor or Save button.');
        };
        img.src = item.data;
      } catch (err) { console.warn(err); setStatus('Error preparing injection.'); }
    }));
  }

  /* ---------- Convert button => convert current preview/loaded image and save to library ---------- */
  convertBtn.addEventListener('click', () => {
    if (!loadedImage) { setStatus('No image to convert — upload or URL first.'); return; }
    const size = parseInt(sizeSelect.value,10) || FINAL_SIZE;
    const bw = parseInt(borderWInput.value,10) || 0;
    const bc = borderInput.value;
    const bgc = bgInput.value;
    const finalCanvas = makeFinalCanvas(loadedImage, size, bw, bc, bgc);
    cachedFinal = finalCanvas;
    // show final in preview (scaled)
    const tmp = new Image();
    tmp.onload = () => { pctx.clearRect(0,0,preview.width, preview.height); pctx.drawImage(tmp,0,0,preview.width,preview.height); setStatus('Converted final skin (ready).'); };
    tmp.src = finalCanvas.toDataURL('image/png');
    // Save to library
    saveToLibrary('Skin ' + (new Date()).toLocaleTimeString(), finalCanvas.toDataURL('image/png'));
    // cache small saved (<=256) into local quick key
    try { if (size <= 256) localStorage.setItem(CACHED_KEY, finalCanvas.toDataURL('image/png')); } catch(e){}
  });

  /* ---------- initial render library ---------- */
  renderLibrary();

  /* ---------- Editor injection: find the editor canvas and container ---------- */
  function findEditorCanvas() {
    const canvases = Array.from(document.querySelectorAll('canvas')).filter(c => {
      try {
        const r = c.getBoundingClientRect();
        return r.width > 160 && r.height > 160 && r.top > 0 && r.left > 0 && getComputedStyle(c).visibility !== 'hidden';
      } catch (e) { return false; }
    });
    if (!canvases.length) return null;
    // pick canvas closest to center
    const cx = innerWidth/2, cy = innerHeight/2;
    canvases.sort((a,b) => {
      const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
      const da = Math.hypot((ra.left+ra.width/2)-cx, (ra.top+ra.height/2)-cy);
      const db = Math.hypot((rb.left+rb.width/2)-cx, (rb.top+rb.height/2)-cy);
      return da - db;
    });
    return canvases[0];
  }

  function findEditorContainer(candidateCanvas){
    if (!candidateCanvas) return null;
    let anc = candidateCanvas.parentElement;
    for (let i=0;i<10 && anc; i++){
      const style = getComputedStyle(anc);
      // heuristics for modal: white-ish background, large width and visible
      if ((/rgba?\(255,\s*255,\s*255/.test(style.backgroundColor) || style.backgroundColor === 'white' || style.backgroundColor.indexOf('rgb') >= 0) &&
          anc.clientWidth > 300) {
        return anc;
      }
      // also check if ancestor contains a "Save" button text
      if (anc.querySelector && Array.from(anc.querySelectorAll('button,a,input')).some(el=>/save|upload|create/i.test((el.innerText||el.value||el.title||'').trim()))) {
        return anc;
      }
      anc = anc.parentElement;
    }
    // fallback: return the canvas parent
    return candidateCanvas.parentElement || document.body;
  }

  /* ---------- injecting our Upload UI into site editor (bottom-right of editor) ---------- */
  let injectedEditorPanel = null;
  function injectUploadPanelIntoEditor(){
    try {
      const canvas = findEditorCanvas();
      if (!canvas) return null;
      const container = findEditorContainer(canvas);
      if (!container) return null;
      // if panel already exists and is inside same container, do nothing
      if (injectedEditorPanel && injectedEditorPanel.parentElement === container) return injectedEditorPanel;
      // remove previous if in different place
      if (injectedEditorPanel && injectedEditorPanel.parentElement) injectedEditorPanel.parentElement.removeChild(injectedEditorPanel);
      // create panel
      const panel = document.createElement('div');
      panel.id = 'dasgar_editor_inject';
      panel.style.position = 'absolute';
      panel.style.right = '18px';
      panel.style.bottom = '18px';
      panel.style.background = 'rgba(0,0,0,0.45)';
      panel.style.padding = '8px';
      panel.style.borderRadius = '8px';
      panel.style.color = '#fff';
      panel.style.zIndex = 2147483648;
      panel.innerHTML = `
        <input type="file" id="dasgar_editor_file" accept="image/*" style="width:140px"><br>
        <button id="dasgar_editor_convert" class="das-small" style="margin-top:6px;padding:6px 10px;background:#2b8cff">Convert & Inject</button>
      `;
      // append
      container.style.position = container.style.position || 'relative';
      container.appendChild(panel);
      injectedEditorPanel = panel;

      // hookup file input and button
      const editorFile = panel.querySelector('#dasgar_editor_file');
      const editorBtn = panel.querySelector('#dasgar_editor_convert');

      editorFile.addEventListener('change', (ev) => {
        const f = ev.target.files && ev.target.files[0];
        if (!f) return;
        if (f.size > MAX_SIZE_BYTES) { setStatus('Editor upload too large (max ' + Math.round(MAX_SIZE_BYTES/1024) + ' KB).'); return; }
        const obj = URL.createObjectURL(f);
        const img = new Image();
        img.onload = () => { loadedImage = img; drawPreviewFromImage(img); setStatus('Editor file loaded. Click Convert & Inject to place into editor.'); URL.revokeObjectURL(obj); };
        img.onerror = () => { setStatus('Editor file load failed.'); URL.revokeObjectURL(obj); };
        img.src = obj;
      });

      editorBtn.addEventListener('click', async () => {
        if (!loadedImage && !cachedFinal) { setStatus('No image to inject — upload or select from library.'); return; }
        // make final canvas from loadedImage or cachedFinal
        const src = loadedImage || (cachedFinal ? (()=>{ const tmp = document.createElement('canvas'); tmp.width = cachedFinal.width; tmp.height = cachedFinal.height; tmp.getContext('2d').drawImage(cachedFinal,0,0); return tmp; })() : null);
        if (!src) { setStatus('Nothing to inject.'); return; }
        const size = parseInt(sizeSelect.value,10) || FINAL_SIZE;
        const finalCanvas = makeFinalCanvas(src, size, parseInt(borderWInput.value,10)||0, borderInput.value, bgInput.value);
        cachedFinal = finalCanvas;
        // try injecting to editor canvas
        const ok = await injectIntoEditorAndSave(finalCanvas);
        setStatus(ok ? 'Injected & attempted Save on site (check site).' : 'Could not inject / find Save; try opening skin editor.');
      });

      return panel;
    } catch (err) {
      console.warn('injectEditor error', err);
      return null;
    }
  }

  // observe DOM for canvas/modal changes and inject panel when editor opens
  const domObserver = new MutationObserver(() => {
    try { injectUploadPanelIntoEditor(); } catch(e){}
  });
  domObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });

  /* ---------- attempt to inject a final canvas into the editor canvas & click Save ---------- */
  async function injectIntoEditorAndSave(finalCanvas){
    try {
      const editorCanvas = findEditorCanvas();
      if (!editorCanvas) { console.warn('No editor canvas'); return false; }
      // try to draw into editor canvas context
      try {
        const ectx = editorCanvas.getContext('2d');
        ectx.save();
        // compute destination square for draw
        const dst = Math.min(editorCanvas.width, editorCanvas.height);
        const dx = (editorCanvas.width - dst) / 2, dy = (editorCanvas.height - dst) / 2;
        // clip circle area if possible
        try { ectx.beginPath(); ectx.arc(editorCanvas.width/2, editorCanvas.height/2, dst/2 - 1, 0, Math.PI*2); ectx.closePath(); ectx.clip(); } catch(e){}
        ectx.drawImage(finalCanvas, dx, dy, dst, dst);
        ectx.restore();
        setStatus('Injected into the editor canvas.');
      } catch (err) {
        console.warn('draw into editor failed', err);
        return false;
      }

      // find a visible Save button (heuristic)
      const buttons = Array.from(document.querySelectorAll('button, a, input[type="button"], input[type="submit"]')).filter(el => {
        try {
          if (el.offsetParent === null) return false; // not visible
          const txt = (el.innerText || el.value || el.title || '').trim().toLowerCase();
          return /save|upload|create|apply|convert/i.test(txt);
        } catch(e){ return false; }
      });
      // pick the Save-looking one closest to editorCanvas
      let saveBtn = null;
      if (buttons.length) {
        // score by proximity to editor canvas
        const rc = editorCanvas.getBoundingClientRect();
        let best = Infinity;
        buttons.forEach(b => {
          const rb = b.getBoundingClientRect();
          const dist = Math.hypot((rb.left+rb.width/2)-(rc.left+rc.width/2), (rb.top+rb.height/2)-(rc.top+rc.height/2));
          if (dist < best) { best = dist; saveBtn = b; }
        });
      }

      if (saveBtn) {
        // click it programmatically (safest: dispatch MouseEvents)
        setStatus('Found Save button — clicking to save to account (you must be logged in).');
        saveBtn.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
        await new Promise(r => setTimeout(r, 80));
        saveBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
        await new Promise(r => setTimeout(r, 40));
        saveBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
        saveBtn.click();
        return true;
      } else {
        setStatus('No Save button found near editor — either site changed UI or editor is not open.');
        return false;
      }
    } catch (e) {
      console.warn('inject+save error', e); setStatus('Injection failed: ' + (e.message||e)); return false;
    }
  }

  /* ---------- top-level Apply button tries to inject cachedFinal or ask user steps ---------- */
  applySiteBtn.addEventListener('click', async () => {
    if (!cachedFinal) {
      if (!loadedImage) { setStatus('No skin prepared — use Upload or URL then Convert first.'); return; }
      // auto convert to final using chosen size & border
      const size = parseInt(sizeSelect.value,10) || FINAL_SIZE;
      cachedFinal = makeFinalCanvas(loadedImage, size, parseInt(borderWInput.value,10) || 0, borderInput.value, bgInput.value);
    }
    setStatus('Attempting to inject & save to agar.io (site editor must be open).');
    const ok = await injectIntoEditorAndSave(cachedFinal);
    setStatus(ok ? 'Attempt done — check site/editor and Save dialog.' : 'Could not inject — open the site skin editor and try again.');
  });

  /* ---------- try restore last cached small skin for preview ---------- */
  (function restoreCached(){
    try {
      const s = localStorage.getItem(CACHED_KEY);
      if (s) {
        const img = new Image(); img.onload = ()=>{ loadedImage = img; drawPreviewFromImage(img); setStatus('Restored cached small skin.'); }; img.src = s;
      }
    } catch(e){}
  })();

  /* ---------- wire color/border live preview (if loadedImage) ---------- */
  bgInput.addEventListener('input', ()=>{ if (loadedImage) drawPreviewFromImage(loadedImage); });
  borderInput.addEventListener('input', ()=>{ if (loadedImage) drawPreviewFromImage(loadedImage); });
  borderWInput.addEventListener('input', ()=>{ if (loadedImage) drawPreviewFromImage(loadedImage); });

  /* ---------- initial attempt to inject panel if editor already open ---------- */
  setTimeout(injectUploadPanelIntoEditor, 900);
  // and periodically try (so when user opens editor UI we inject)
  setInterval(injectUploadPanelIntoEditor, 1500);

  /* ---------- final console message ---------- */
  console.log('[Dasgar YT Skins v1.3] loaded — use URL/file → Convert & Add to Library → Upload→Save to agar.io (editor must be open & you must be signed in).');

})();