Flickr: Commenters Summary

Displays a panel showing commenters sorted by the number of comments made

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Flickr: Commenters Summary
// @namespace    http://tampermonkey.net/
// @version      0.5
// @author       Isidro Vila Verde
// @description  Displays a panel showing commenters sorted by the number of comments made
// @match        https://www.flickr.com/*
// @match        https://flickr.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // Configurações globais
    const STORAGE = {
        apiKey: 'flickr_api_key',
        darkMode: 'flickr_dark_mode',
        panelPos: 'flickr_panel_pos',
        sortMode: 'flickr_sort_mode'
    };

    // Elementos globais
    let btn = null;
    let panel = null;
    let isRunning = false;
    const validPathRegex = /^\/photos\/[^/]+(?:\/(?:with\/.+)?)?$/;

    // Verificador de URL
    function isValidPage() {
        return validPathRegex.test(window.location.pathname);
    }

    // Limpeza dos elementos
    function cleanUp() {
        if (btn) {
            btn.remove();
            btn = null;
        }
        if (panel) {
            panel.remove();
            panel = null;
        }
        isRunning = false;
    }

    // Cria o botão inicial
    function createStartButton() {
        if (btn) return;

        btn = document.createElement('button');
        btn.textContent = '📊 Comentadores';
        btn.style.position = 'fixed';
        btn.style.top = '5px';
        btn.style.left = '50%';
        btn.style.transform = 'translateX(-50%)';
        btn.style.zIndex = '9999';
        btn.style.padding = '2px';
        btn.style.background = '#0063dc';
        btn.style.color = '#fff';
        btn.style.border = 'none';
        btn.style.borderRadius = '5px';
        btn.style.cursor = 'pointer';
        btn.style.maxWidth = '10vw';
        btn.style.whiteSpace = 'nowrap';
        btn.style.overflow = 'hidden';
        btn.style.textOverflow = 'ellipsis';

        btn.addEventListener('click', run);
        document.body.appendChild(btn);
    }

    // Observador de mudanças de URL
    function setupUrlObserver() {
        let lastUrl = location.href;

        // Observa mudanças a cada 500ms
        setInterval(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                handleUrlChange();
            }
        }, 500);

        // Captura navegações via History API
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        history.pushState = function() {
            originalPushState.apply(this, arguments);
            handleUrlChange();
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            handleUrlChange();
        };

        // Captura eventos de popstate (back/forward)
        window.addEventListener('popstate', handleUrlChange);
    }

    // Manipulador de mudança de URL
    function handleUrlChange() {
        if (isValidPage()) {
            console.log('isValidPage');
            if (!btn) {
                console.log('createButton');
                createStartButton();
            }
        } else {
            console.log('isNotValidPage=>CleanButton');
            cleanUp();
        }
    }

    // Funções auxiliares
    const log = (...args) => console.log('[FlickrResumo]', ...args);

    function getStored(key, fallback = null) {
        return JSON.parse(localStorage.getItem(key)) ?? fallback;
    }

    function setStored(key, value) {
        localStorage.setItem(key, JSON.stringify(value));
    }

    function getApiKey() {
        let key = getStored(STORAGE.apiKey);
        if (!key) {
            key = prompt("🔑 Introduz a tua API key do Flickr:");
            if (key) setStored(STORAGE.apiKey, key.trim());
            else return null;
        }
        return key;
    }

    async function resolveUserId(apiKey) {
        const path = window.location.pathname;
        const match = path.match(/^\/photos\/([^/]+)(?:\/(?:with\/.+)?)?$/);
        if (!match) return null;

        const identifier = match[1];
        if (/^\d+@N\d+$/.test(identifier)) {
            return identifier;
        }

        const fullUrl = `https://www.flickr.com/photos/${identifier}/`;
        const url = `https://www.flickr.com/services/rest/?method=flickr.urls.lookupUser&api_key=${apiKey}&url=${encodeURIComponent(fullUrl)}&format=json&nojsoncallback=1`;

        try {
            const data = await fetchJSON(url);
            return data.user?.id || null;
        } catch (e) {
            console.error("Erro ao resolver user_id via lookupUser:", e);
            return null;
        }
    }

    async function fetchJSON(url) {
        const res = await fetch(url);
        return res.json();
    }

    async function getPhotos(userId, apiKey, perPage = 100, maxPages = 2) {
        let photos = [];
        for (let page = 1; page <= maxPages; page++) {
            const url = `https://www.flickr.com/services/rest/?method=flickr.people.getPublicPhotos&api_key=${apiKey}&user_id=${userId}&format=json&nojsoncallback=1&per_page=${perPage}&page=${page}`;
            log(`📷 A obter fotos da página ${page}...`);
            const data = await fetchJSON(url);
            if (!data.photos?.photo?.length) break;
            photos = photos.concat(data.photos.photo);
            if (page >= data.photos.pages) break;
        }
        log(`✅ Total de fotos obtidas: ${photos.length}`);
        return photos;
    }

    async function getComments(photoId, apiKey) {
        const url = `https://www.flickr.com/services/rest/?method=flickr.photos.comments.getList&api_key=${apiKey}&photo_id=${photoId}&format=json&nojsoncallback=1`;
        const data = await fetchJSON(url);
        return (data.comments?.comment || []).map(c => ({
            user: c.authorname,
            username: c.realname || c.authorname,
            nsid: c.author,
            date: new Date(parseInt(c.datecreate, 10) * 1000)
        }));
    }

    function formatDate(date) {
        return date.toISOString().split("T")[0];
    }

    function createPanel(dataMap, totalPhotos) {
        let sortBy = getStored(STORAGE.sortMode, 'count');

        const sorted = () => {
            return Object.entries(dataMap).sort((a, b) => {
                if (sortBy === 'count') return b[1].count - a[1].count;
                return b[1].last - a[1].last;
            });
        };

        panel = document.createElement("div");
        panel.style.position = "fixed";
        panel.style.width = "600px";
        panel.style.height = "400px";
        panel.style.overflow = "auto hidden";
        panel.style.resize = "both";
        panel.style.zIndex = "10000";
        panel.style.border = "2px solid #0063dc";
        panel.style.borderRadius = "8px";
        panel.style.boxShadow = "0 0 10px rgba(0,0,0,0.3)";
        panel.style.fontFamily = "sans-serif";

        const savedPos = getStored(STORAGE.panelPos, { top: 100, left: 100 });
        panel.style.top = savedPos.top + 'px';
        panel.style.left = savedPos.left + 'px';

        let dark = getStored(STORAGE.darkMode, false);

        // Cabeçalho
        const header = document.createElement("div");
        header.style.background = "#0063dc";
        header.style.color = "#fff";
        header.style.padding = "6px 10px";
        header.style.cursor = "move";
        header.style.display = "flex";
        header.style.flexDirection = "column";
        header.style.gap = "4px";

        const titleRow = document.createElement("div");
        titleRow.style.display = "flex";
        titleRow.style.justifyContent = "space-between";
        titleRow.style.alignItems = "center";

        const titleSpan = document.createElement("span");
        titleSpan.textContent = "Resumo de Comentadores";
        titleRow.appendChild(titleSpan);

        const controls = document.createElement("div");

        const makeBtn = (text, title, onclick) => {
            const btn = document.createElement("button");
            btn.textContent = text;
            btn.title = title;
            btn.style.marginLeft = "6px";
            btn.style.cursor = "pointer";
            btn.onclick = onclick;
            return btn;
        };

        const closeBtn = makeBtn("✖", "Fechar", () => {
            cleanUp();
            if (btn) btn.disabled = false;
        });
        const darkBtn = makeBtn("🌙", "Alternar tema", () => {
            dark = !dark;
            setStored(STORAGE.darkMode, dark);
            applyTheme();
        });
        const sortBtn = makeBtn("↕️", "Alternar ordenação", () => {
            sortBy = sortBy === 'count' ? 'date' : 'count';
            setStored(STORAGE.sortMode, sortBy);
            updateContent();
        });

        [sortBtn, darkBtn, closeBtn].forEach(btn => controls.appendChild(btn));
        titleRow.appendChild(controls);
        header.appendChild(titleRow);

        // Progresso no header
        const progressContainer = document.createElement("div");
        progressContainer.style.display = "flex";
        progressContainer.style.alignItems = "center";
        progressContainer.style.gap = "8px";
        progressContainer.style.fontSize = "0.85em";
        progressContainer.style.opacity = "0.9";

        const smallSpinner = document.createElement("div");
        smallSpinner.style.width = "14px";
        smallSpinner.style.height = "14px";
        smallSpinner.style.border = "2px solid rgba(255,255,255,0.3)";
        smallSpinner.style.borderRadius = "50%";
        smallSpinner.style.borderTop = "2px solid #fff";
        smallSpinner.style.animation = "spin 1s linear infinite";
        smallSpinner.style.display = "none";

        const progressText = document.createElement("span");
        progressContainer.appendChild(smallSpinner);
        progressContainer.appendChild(progressText);
        header.appendChild(progressContainer);

        panel.appendChild(header);

        // Container principal
        const mainContainer = document.createElement("div");
        mainContainer.style.position = "relative";
        mainContainer.style.height = "calc(100% - 60px)";
        mainContainer.style.overflow = "auto";

        // Spinner grande central
        const bigSpinner = document.createElement("div");
        bigSpinner.style.position = "absolute";
        bigSpinner.style.top = "50%";
        bigSpinner.style.left = "50%";
        bigSpinner.style.transform = "translate(-50%, -50%)";
        bigSpinner.style.width = "60px";
        bigSpinner.style.height = "60px";
        bigSpinner.style.border = "6px solid rgba(0,99,220,0.2)";
        bigSpinner.style.borderRadius = "50%";
        bigSpinner.style.borderTop = "6px solid #0063dc";
        bigSpinner.style.animation = "spin 1s linear infinite";
        bigSpinner.style.display = "none";

        // Conteúdo
        const content = document.createElement("div");
        content.style.padding = "10px";
        content.style.display = "grid";
        content.style.gridTemplateColumns = "1fr auto auto";
        content.style.gap = "8px";
        content.style.alignItems = "center";
        content.style.fontSize = "14px";
        content.style.minHeight = "100%";

        // Adicionar animação
        const style = document.createElement("style");
        style.textContent = `
            @keyframes spin {
                0% { transform: translate(-50%, -50%) rotate(0deg); }
                100% { transform: translate(-50%, -50%) rotate(360deg); }
            }
        `;
        document.head.appendChild(style);

        mainContainer.appendChild(bigSpinner);
        mainContainer.appendChild(content);
        panel.appendChild(mainContainer);
        document.body.appendChild(panel);

        function applyTheme() {
            panel.style.background = dark ? "#1e1e1e" : "#fff";
            panel.style.color = dark ? "#ccc" : "#000";
            bigSpinner.style.border = dark ? "6px solid rgba(170,170,221,0.2)" : "6px solid rgba(0,99,220,0.2)";
            bigSpinner.style.borderTop = dark ? "6px solid #aad" : "6px solid #0063dc";
            smallSpinner.style.border = dark ? "2px solid rgba(170,170,221,0.3)" : "2px solid rgba(255,255,255,0.3)";
            smallSpinner.style.borderTop = dark ? "2px solid #aad" : "2px solid #fff";
        }

        function updateContent(processed = 0, total = totalPhotos) {
            if (processed === 0 && Object.keys(dataMap).length === 0) {
                bigSpinner.style.display = "block";
                content.style.display = "none";
            } else {
                bigSpinner.style.display = "none";
                content.style.display = "grid";
            }

            if (processed > 0 && processed < total) {
                smallSpinner.style.display = "block";
                progressText.textContent = `A processar: ${processed} / ${total} fotos`;
            } else if (processed > 0) {
                smallSpinner.style.display = "none";
                progressContainer.style.display = "none";
            } else {
                smallSpinner.style.display = "none";
                progressText.textContent = "";
            }

            content.innerHTML = "";

            ['Utilizador', 'Comentários', 'Último comentário'].forEach(h => {
                const el = document.createElement("div");
                el.textContent = h;
                el.style.fontWeight = "bold";
                el.style.position = "sticky";
                el.style.top = "0";
                el.style.background = dark ? "#1e1e1e" : "#fff";
                el.style.zIndex = "1";
                content.appendChild(el);
            });

            sorted().forEach(([user, info]) => {
                content.appendChild(userLink(info.username, info.nsid));
                content.appendChild(el(info.count));
                content.appendChild(el(formatDate(info.last)));
            });

            function el(text) {
                const d = document.createElement("div");
                d.textContent = text;
                return d;
            }

            function userLink(name, nsid) {
                const d = document.createElement("div");
                const a = document.createElement("a");
                a.href = `https://www.flickr.com/photos/${nsid}/`;
                a.textContent = name;
                a.target = "_blank";
                a.style.color = dark ? "#aad" : "#06c";
                a.style.textDecoration = "none";
                d.appendChild(a);
                return d;
            }
        }

        applyTheme();
        updateContent();

        // Função de arrastar
        let dragging = false, offsetX = 0, offsetY = 0;

        titleRow.onmousedown = e => {
            if (e.target.tagName === 'BUTTON') return;

            dragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
            e.preventDefault();
        };

        document.onmousemove = e => {
            if (dragging) {
                panel.style.left = (e.clientX - offsetX) + 'px';
                panel.style.top = (e.clientY - offsetY) + 'px';
                setStored(STORAGE.panelPos, {
                    top: parseInt(panel.style.top),
                    left: parseInt(panel.style.left)
                });
            }
        };

        document.onmouseup = () => dragging = false;

        return { updateContent };
    }

    async function run() {
        if (!isValidPage()) return;
        if (isRunning) return;

        isRunning = true;
        if (btn) btn.disabled = true;

        try {
            const apiKey = getApiKey();
            if (!apiKey) {
                cleanUp();
                return;
            }

            const nsid = await resolveUserId(apiKey);
            if (!nsid) {
                alert("❌ Não foi possível obter o ID do utilizador.");
                cleanUp();
                return;
            }

            const photos = await getPhotos(nsid, apiKey, 100, 2);
            if (!photos.length) {
                alert("⚠️ Sem fotos públicas.");
                cleanUp();
                return;
            }

            const commenters = {};
            const { updateContent } = createPanel(commenters, photos.length);
            let updateCounter = 0;

            for (let i = 0; i < photos.length; i++) {
                if (!isValidPage()) {
                    cleanUp();
                    return;
                }

                const photo = photos[i];
                log(`💬 Comentários da foto ${i + 1}/${photos.length} (ID ${photo.id})...`);
                const comments = await getComments(photo.id, apiKey);

                for (const { user, username, nsid, date } of comments) {
                    if (!commenters[user]) {
                        commenters[user] = { count: 1, last: date, nsid, username };
                    } else {
                        commenters[user].count++;
                        if (date > commenters[user].last) {
                            commenters[user].last = date;
                        }
                    }
                }

                updateCounter++;
                if (updateCounter >= 10 || i === photos.length - 1) {
                    updateContent(i + 1, photos.length);
                    updateCounter = 0;
                }

                await new Promise(r => setTimeout(r, 500));
            }

            log("📊 Resultado final:", commenters);
        } catch (error) {
            console.error("Erro durante execução:", error);
            cleanUp();
        }
    }

    // Inicialização
    setupUrlObserver();
    if (isValidPage()) {
        createStartButton();
    }
})();