Tier Placeholder Tile

Create editable placeholder tiles and auto-insert/upload them into TierMaker templates

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Tier Placeholder Tile
// @namespace    http://tampermonkey.net/
// @version      2.1.4
// @description  Create editable placeholder tiles and auto-insert/upload them into TierMaker templates
// @author       Liminality Dreams
// @match        https://tiermaker.com/*
// @icon         https://i.pinimg.com/736x/ae/d8/49/aed849cca00ffd2a756b107ae91075f9.jpg
// @grant        none
// @license GNU 3.0
// ==/UserScript==

(function() {
    'use strict';

    function $(s, ctx=document){return ctx.querySelector(s)}
    function $all(s, ctx=document){return Array.from((ctx||document).querySelectorAll(s))}

    function ensureUi() {
        if (document.getElementById('tm-placeholder-ui')) return;
        const ui = document.createElement('div');
        ui.id = 'tm-placeholder-ui';
        ui.style.position = 'fixed';
        ui.style.right = '12px';
        ui.style.top = '12px';
        ui.style.zIndex = 2147483647;
        ui.style.width = '300px';
        ui.style.fontFamily = 'Inter, Arial, sans-serif';
        ui.style.boxShadow = '0 8px 24px rgba(0,0,0,0.4)';
        ui.style.borderRadius = '12px';
        ui.style.background = '#0f0f10';
        ui.style.color = '#eee';
        ui.style.padding = '8px';
        ui.style.backdropFilter = 'blur(4px)';
        ui.innerHTML = `
            <div id="tm-header" style="display:flex;align-items:center;gap:8px">
                <img id="tm-icon" src="https://i.pinimg.com/736x/ae/d8/49/aed849cca00ffd2a756b107ae91075f9.jpg" style="width:34px;height:34px;border-radius:6px;flex:0 0 34px;object-fit:cover" />
                <div style="flex:1">
                    <div style="font-weight:600">Tier Placeholder Tile</div>
                    <div style="font-size:11px;opacity:.7">Create, drag, upload</div>
                </div>
                <button id="tm-min" style="background:#111;border:1px solid #222;color:#ddd;padding:6px;border-radius:6px;cursor:pointer">—</button>
            </div>
            <div id="tm-body" style="margin-top:8px">
                <label style="font-size:12px;opacity:.8">Text</label>
                <input id="tm-text" type="text" maxlength="40" placeholder="placeholder text" style="width:100%;padding:6px;margin-top:4px;border-radius:8px;border:1px solid #222;background:#09090a;color:#eee"/>
                <div style="display:flex;gap:8px;margin-top:8px;align-items:center">
                    <div style="flex:1">
                        <label style="font-size:12px;opacity:.8">Background</label>
                        <input id="tm-color" type="color" value="#b76bff" style="width:100%;height:36px;border-radius:8px;border:0;padding:0;margin-top:4px"/>
                    </div>
                    <div style="width:84px">
                        <label style="font-size:12px;opacity:.8">Shape</label>
                        <select id="tm-shape" style="width:100%;padding:6px;margin-top:4px;border-radius:8px;background:#09090a;color:#eee;border:1px solid #222">
                            <option value="square">Square</option>
                            <option value="round">Round</option>
                        </select>
                    </div>
                </div>
                <div style="display:flex;gap:8px;margin-top:8px;align-items:center">
                    <div style="flex:1">
                        <label style="font-size:12px;opacity:.8">Size</label>
                        <select id="tm-size" style="width:100%;padding:6px;margin-top:4px;border-radius:8px;background:#09090a;color:#eee;border:1px solid #222">
                            <option value="256">256×256</option>
                            <option value="400" selected>400×400</option>
                            <option value="600">600×600</option>
                        </select>
                    </div>
                    <div style="width:84px">
                        <label style="font-size:12px;opacity:.8">Font</label>
                        <select id="tm-fontsize" style="width:100%;padding:6px;margin-top:4px;border-radius:8px;background:#09090a;color:#eee;border:1px solid #222">
                            <option value="18">18px</option>
                            <option value="22" selected>22px</option>
                            <option value="30">30px</option>
                        </select>
                    </div>
                </div>
                <div style="display:flex;gap:8px;margin-top:10px">
                    <button id="tm-create" style="flex:1;padding:8px;border-radius:8px;background:linear-gradient(180deg,#2a2a2a,#111);border:1px solid #333;color:#fff;cursor:pointer">Create Placeholder</button>
                    <button id="tm-insert" style="flex:1;padding:8px;border-radius:8px;background:#122;border:1px solid #233;color:#dff;cursor:pointer">Create + Insert</button>
                </div>
                <div id="tm-preview-wrap" style="margin-top:10px;display:flex;gap:8px;align-items:center">
                    <div id="tm-preview" style="width:64px;height:64px;border-radius:6px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);display:flex;align-items:center;justify-content:center;overflow:hidden;background:#b76bff;"></div>
                    <div style="flex:1;font-size:12px;opacity:.8">Drag the preview into the tier area or press Create + Insert to auto-add to the upload input.</div>
                </div>
                <div id="tm-thumbs" style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap"></div>
            </div>
        `;
        document.body.appendChild(ui);

        const header = $('#tm-header', ui);
        const body = $('#tm-body', ui);
        const minBtn = $('#tm-min', ui);
        let minimized = false;
        minBtn.addEventListener('click', () => {
            minimized = !minimized;
            body.style.display = minimized ? 'none' : 'block';
            minBtn.textContent = minimized ? '+' : '—';
            ui.style.width = minimized ? '48px' : '300px';
        });

        const inputs = {
            text: $('#tm-text', ui),
            color: $('#tm-color', ui),
            shape: $('#tm-shape', ui),
            size: $('#tm-size', ui),
            fontsize: $('#tm-fontsize', ui),
            preview: $('#tm-preview', ui),
            thumbs: $('#tm-thumbs', ui),
            create: $('#tm-create', ui),
            insert: $('#tm-insert', ui)
        };

        function renderPreview() {
            const size = parseInt(inputs.size.value,10);
            const canvas = document.createElement('canvas');
            canvas.width = 200;
            canvas.height = 200;
            const ctx = canvas.getContext('2d');
            const bg = inputs.color.value;
            ctx.fillStyle = bg;
            if (inputs.shape.value === 'round') {
                ctx.beginPath();
                ctx.arc(100,100,100,0,Math.PI*2);
                ctx.fill();
            } else {
                ctx.fillRect(0,0,200,200);
            }
            const txt = inputs.text.value || 'Placeholder';
            ctx.fillStyle = '#0b0b0b';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.font = `${parseInt(inputs.fontsize.value,10)}px Arial`;
            wrapTextCenter(ctx, txt, 100, 100, 170, parseInt(inputs.fontsize.value,10)+6);
            inputs.preview.style.backgroundImage = `url(${canvas.toDataURL()})`;
            inputs.preview.style.backgroundSize = 'cover';
            inputs.preview.draggable = true;
            inputs.preview.dataset.dataurl = canvas.toDataURL();
        }

        function wrapTextCenter(ctx, text, x, y, maxWidth, lineHeight) {
            const words = text.split(' ');
            let line = '';
            const lines = [];
            for (let n = 0; n < words.length; n++) {
                const testLine = line + words[n] + ' ';
                const metrics = ctx.measureText(testLine);
                const testWidth = metrics.width;
                if (testWidth > maxWidth && n > 0) {
                    lines.push(line.trim());
                    line = words[n] + ' ';
                } else {
                    line = testLine;
                }
            }
            lines.push(line.trim());
            const startY = y - (lines.length-1) * (lineHeight/2);
            for (let i = 0; i < lines.length; i++) {
                ctx.fillText(lines[i], x, startY + i * lineHeight);
            }
        }

        inputs.text.addEventListener('input', renderPreview);
        inputs.color.addEventListener('input', renderPreview);
        inputs.shape.addEventListener('change', renderPreview);
        inputs.size.addEventListener('change', renderPreview);
        inputs.fontsize.addEventListener('change', renderPreview);

        async function createCanvasDataURL() {
            const size = parseInt(inputs.size.value,10);
            const canvas = document.createElement('canvas');
            canvas.width = size;
            canvas.height = size;
            const ctx = canvas.getContext('2d');
            const bg = inputs.color.value;
            ctx.fillStyle = bg;
            if (inputs.shape.value === 'round') {
                ctx.beginPath();
                ctx.arc(size/2, size/2, size/2, 0, Math.PI*2);
                ctx.fill();
            } else {
                ctx.fillRect(0,0,size,size);
            }
            const txt = inputs.text.value || 'Placeholder';
            ctx.fillStyle = '#000000';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.font = `${parseInt(inputs.fontsize.value,10)}px Arial`;
            wrapTextCenter(ctx, txt, size/2, size/2, size - 24, parseInt(inputs.fontsize.value,10)+8);
            return canvas.toDataURL('image/png');
        }

        function makeThumb(dataUrl, filename) {
            const el = document.createElement('div');
            el.style.width = '64px';
            el.style.height = '64px';
            el.style.borderRadius = '8px';
            el.style.overflow = 'hidden';
            el.style.backgroundImage = `url(${dataUrl})`;
            el.style.backgroundSize = 'cover';
            el.style.cursor = 'grab';
            el.draggable = true;
            el.dataset.url = dataUrl;
            el.dataset.filename = filename;
            el.addEventListener('dragstart', (e) => {
                try {
                    e.dataTransfer.setData('text/uri-list', dataUrl);
                    e.dataTransfer.setData('text/plain', filename);
                } catch (err) {}
                if (e.dataTransfer.items && e.dataTransfer.items.add) {
                    fetch(dataUrl).then(r=>r.blob()).then(blob=>{
                        const file = new File([blob], filename, {type: 'image/png'});
                        const dt = new DataTransfer();
                        dt.items.add(file);
                        try {
                            e.dataTransfer.items.clear();
                        } catch(e){}
                        try {
                            e.dataTransfer.items.add(file);
                        } catch(e){}
                    });
                }
            });
            el.addEventListener('click', async () => {
                const dt = new DataTransfer();
                const blob = await (await fetch(dataUrl)).blob();
                dt.items.add(new File([blob], filename, {type:'image/png'}));
                autoInsertFiles(dt.files);
            });
            inputs.thumbs.appendChild(el);
        }

        async function autoInsertFiles(fileList) {
            const fileInput = document.querySelector('input[type=file]');
            if (fileInput) {
                const dt = new DataTransfer();
                if (fileList instanceof FileList) {
                    for (let f of fileList) dt.items.add(f);
                } else if (Array.isArray(fileList)) {
                    for (let f of fileList) dt.items.add(f);
                } else {
                    dt.items.add(fileList);
                }
                fileInput.files = dt.files;
                const ev = new Event('change', {bubbles:true});
                fileInput.dispatchEvent(ev);
                return true;
            } else {
                const imagesContainer = findImagesContainer();
                if (imagesContainer) {
                    for (let f of fileList) {
                        const url = URL.createObjectURL(f);
                        insertLocalThumbnail(imagesContainer, url, f.name);
                    }
                    return true;
                }
            }
            return false;
        }

        function insertLocalThumbnail(container, url, name) {
            const thumbWrap = document.createElement('div');
            thumbWrap.style.width = '80px';
            thumbWrap.style.height = '80px';
            thumbWrap.style.margin = '6px';
            thumbWrap.style.borderRadius = '8px';
            thumbWrap.style.overflow = 'hidden';
            thumbWrap.style.backgroundImage = `url(${url})`;
            thumbWrap.style.backgroundSize = 'cover';
            container.appendChild(thumbWrap);
        }

        function findImagesContainer() {
            const candidates = $all('div');
            for (let c of candidates) {
                if (/No images have been uploaded|Upload images to be used in your tier list/i.test(c.innerText)) continue;
                if (c.querySelector && c.querySelector('img')) return c;
            }
            const alt = document.querySelector('.uploaded-images, .images, .tiles, .tm-image-list');
            return alt;
        }

        async function createAndOptionallyInsert(doInsert=false) {
            const dataUrl = await createCanvasDataURL();
            const filename = `placeholder-${Date.now()}.png`;
            makeThumb(dataUrl, filename);
            const blob = await (await fetch(dataUrl)).blob();
            const file = new File([blob], filename, {type:'image/png'});
            const dt = new DataTransfer();
            dt.items.add(file);
            const success = await autoInsertFiles(dt.files);
            if (!success) {
                // fallback: create floating drag image
                const floatImg = document.createElement('img');
                floatImg.src = dataUrl;
                floatImg.style.position = 'fixed';
                floatImg.style.right = '12px';
                floatImg.style.top = '80px';
                floatImg.style.width = '120px';
                floatImg.style.height = '120px';
                floatImg.style.zIndex = 2147483646;
                floatImg.style.border = '2px solid rgba(255,255,255,0.06)';
                floatImg.style.borderRadius = '10px';
                document.body.appendChild(floatImg);
                setTimeout(()=>{ floatImg.remove(); }, 4000);
            }
        }

        inputs.create.addEventListener('click', ()=>createAndOptionallyInsert(false));
        inputs.insert.addEventListener('click', ()=>createAndOptionallyInsert(true));

        inputs.preview.addEventListener('dragstart', (e)=>{
            const dataUrl = e.target.dataset.dataurl;
            const filename = `placeholder-${Date.now()}.png`;
            try {
                e.dataTransfer.setData('text/uri-list', dataUrl);
                e.dataTransfer.setData('text/plain', filename);
            } catch(err){}
            fetch(dataUrl).then(r=>r.blob()).then(blob=>{
                const file = new File([blob], filename, {type:'image/png'});
                try {
                    e.dataTransfer.items.clear();
                } catch(e){}
                try {
                    e.dataTransfer.items.add(file);
                } catch(e){}
            });
        });

        renderPreview();
        setupTierDropTargets();
    }

    function setupTierDropTargets() {
        const observer = new MutationObserver(()=>attachDropToTargets());
        observer.observe(document.body, {childList:true, subtree:true});
        attachDropToTargets();
    }

    function attachDropToTargets() {
        const targets = $all('.tier.sort, .tier.sortable, .tier, .sort');
        targets.forEach(t => {
            if (t.dataset.tmPlaceholderAttached) return;
            t.dataset.tmPlaceholderAttached = '1';
            t.addEventListener('dragover', (e)=>{ e.preventDefault(); });
            t.addEventListener('drop', async (e)=>{
                e.preventDefault();
                const items = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length ? e.dataTransfer.files : null;
                if (items) {
                    const dt = new DataTransfer();
                    for (let f of items) dt.items.add(f);
                    autoInsertFilesToTier(t, dt.files);
                    return;
                }
                const uri = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
                if (uri && uri.startsWith('data:')) {
                    const blob = await (await fetch(uri)).blob();
                    const file = new File([blob], `placeholder-${Date.now()}.png`, {type:'image/png'});
                    const dt = new DataTransfer();
                    dt.items.add(file);
                    autoInsertFilesToTier(t, dt.files);
                }
            }, {passive:false});
        });
    }

    function autoInsertFilesToTier(tierEl, fileList) {
        const fileInput = document.querySelector('input[type=file]');
        if (fileInput) {
            const dt = new DataTransfer();
            for (let f of fileList) dt.items.add(f);
            fileInput.files = dt.files;
            fileInput.dispatchEvent(new Event('change', {bubbles:true}));
            return;
        }
        const img = document.createElement('img');
        img.style.width = '80px';
        img.style.height = '80px';
        img.style.objectFit = 'cover';
        img.style.margin = '4px';
        img.style.borderRadius = '8px';
        const reader = new FileReader();
        reader.onload = ()=> {
            img.src = reader.result;
            tierEl.appendChild(img);
        };
        reader.readAsDataURL(fileList[0]);
    }

    ensureUi();

    function autoInsertFiles(files) {
        const fileInput = document.querySelector('input[type=file]');
        if (fileInput) {
            const dt = new DataTransfer();
            for (let f of files) dt.items.add(f);
            fileInput.files = dt.files;
            fileInput.dispatchEvent(new Event('change',{bubbles:true}));
            return true;
        }
        const imagesContainer = document.querySelector('.uploaded-images, .images, .tm-image-list');
        if (imagesContainer) {
            for (let f of files) {
                const url = URL.createObjectURL(f);
                const d = document.createElement('div');
                d.style.width='80px';d.style.height='80px';d.style.margin='6px';d.style.borderRadius='8px';d.style.backgroundImage=`url(${url})`;d.style.backgroundSize='cover';
                imagesContainer.appendChild(d);
            }
            return true;
        }
        return false;
    }

})();