Tier Placeholder Tile

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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;
    }

})();