Board Game Geek - One page auction geeklists

Loads all geeklist items into a single view simmilar to Peyo61's external tool.

当前为 2022-08-30 提交的版本,查看 最新版本

// ==UserScript==
// @name         Board Game Geek - One page auction geeklists
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  Loads all geeklist items into a single view simmilar to Peyo61's external tool.
// @author       Kempeth @ boardgamegeek
// @match        https://boardgamegeek.com/geeklist/*
// @icon         https://cf.geekdo-static.com/icons/favicon2.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

const FETCHLIMIT = 20000;

var nodeBtnConvert;
var nodeProgressFull;
var nodeProgressEmpty;
var nodeItemList;
var nodeFirstItem;

var geeklistid;
var taggedItems = []; // list of ids
var items = []; // data from api

(function() {
    'use strict';

    //console.log('root function');

    init();
})();

function retry(f)
{
    window.setTimeout(f, 500);
}

function init()
{
    var els = document.getElementsByTagName('gg-geeklist-page-ui');
    if (els.length == 0) { retry(init); return; }

    els = els[0].getElementsByTagName('header');
    if (els.length == 0) { retry(init); return; }

    els = els[0].getElementsByTagName('nav');
    if (els.length == 0) { retry(init); return; }

    var el = els[0];
    nodeBtnConvert = document.createElement('button');
    nodeBtnConvert.innerHTML = "One Page Auction";
    nodeBtnConvert.className = "btn btn-warning";
    nodeBtnConvert.onclick = convert;
    el.append(nodeBtnConvert);
}

function setProgress(done, total)
{
    var percentage = total > 0 ? (done * 100.0 / total) : 0;
    nodeProgressFull.style.width = percentage + '%';
    nodeProgressEmpty.style.width = (100 - percentage) + '%';
    nodeProgressFull.setAttribute('aria-valuemax', total);
    nodeProgressFull.setAttribute('aria-valuenow', done);
    if (percentage > 50)
    {
        nodeProgressFull.innerHTML = done + ' / ' + total;
        nodeProgressEmpty.innerHTML = '';
    }
    else
    {
        nodeProgressEmpty.innerHTML = done + ' / ' + total;
        nodeProgressFull.innerHTML = '';
    }
}

function setProgressAsync(done, total)
{
    return new Promise(res => {
        setProgress(done, total);
        window.setTimeout(() => {
            res();
        }, 0);
    });
}

function convert()
{
    var els = document.getElementsByTagName('gg-geeklist-page-ui');
    if (els.length == 0) { retry(convert); return; }

    els = els[0].getElementsByTagName('div');
    if (els.length == 0) { retry(convert); return; }

    var footerNav = els[0].getElementsByTagName('nav');
    if (els.length == 0) { retry(convert); return; }
    footerNav = footerNav[footerNav.length - 1];

    els = els[0].getElementsByTagName('header');
    if (els.length == 0) { retry(convert); return; }

    els = els[0].getElementsByTagName('nav');
    if (els.length == 0) { retry(convert); return; }
    var headerNav = els[0];

    els = document.getElementsByTagName('gg-geeklist-items-ui');
    if (els.length == 0) { retry(convert); return; }

    els = els[0].getElementsByTagName('div');
    if (els.length == 0) { retry(convert); return; }
    nodeItemList = els[0];

    // remove header items
    for (var child of headerNav.childNodes)
    {
        if (child.tagName != 'GG-THUMBS-GEEKGOLD-GIVEN' && child.tagName != 'GG-SUBSCRIPTION-BUTTON')
        {
            headerNav.removeChild(child);
        }
    }

    // add header items
    var progress = document.createElement('div');
    progress.className = "progress";
    progress.style.height = "32px";
    progress.style.width = "256px";
    headerNav.appendChild(progress);

    nodeProgressFull = document.createElement('div');
    nodeProgressFull.className = "progress-bar";
    nodeProgressFull.setAttribute('role', 'progressbar');
    nodeProgressFull.setAttribute('aria-valuemin', 0);
    nodeProgressFull.setAttribute('aria-valuemax', 0);
    nodeProgressFull.setAttribute('aria-valuenow', 0);
    nodeProgressFull.style.width = '0%';
    nodeProgressFull.innerHTML = '';
    progress.appendChild(nodeProgressFull);

    nodeProgressEmpty = document.createElement('div');
    nodeProgressEmpty.className = "progress-bar bg-secondary";
    nodeProgressEmpty.setAttribute('role', 'progressbar');
    nodeProgressEmpty.style.width = '100%';
    nodeProgressEmpty.innerHTML = '0 / ???';
    progress.appendChild(nodeProgressEmpty);

    var btn = document.createElement('button');
    btn.innerHTML = "Refresh";
    btn.className = "btn btn-primary";
    headerNav.appendChild(btn);

    // Sort Dropdown
    var ddSort = document.createElement('div');
    ddSort.className = "dropdown";
    headerNav.appendChild(ddSort);

    var ddSortBtn = document.createElement('button');
    ddSortBtn.innerText = "Sort ";
    ddSortBtn.className = "btn btn-secondary dropdown-toggle";
    ddSort.appendChild(ddSortBtn);

    var ddSortList = document.createElement('ul');
    ddSortList.className = "dropdown-menu";
    ddSort.appendChild(ddSortList);

    ddSortBtn.onclick = () => {
        ddSortList.style.display = (ddSortList.style.display == "block") ? "none" : "block";
    };
    ddSortBtn.onblur = () => {
        window.setTimeout(() => {
            ddSortList.style.display = "none";
        }, 200);
    };

    var ddSortPosted = document.createElement('li');
    ddSortPosted.innerHTML = "<button type=\"button\" class=\"dropdown-item\">Posted</button>";
    ddSortPosted.onclick = () => {
        console.log('sorting by default', new Date());
        sortBy('');
    };
    ddSortList.appendChild(ddSortPosted);

    var ddSortName = document.createElement('li');
    ddSortName.innerHTML = "<button type=\"button\" class=\"dropdown-item\">Name</button>";
    ddSortName.onclick = () => {
        console.log('sorting by name', new Date());
        sortBy('name');
    };
    ddSortList.appendChild(ddSortName);

    // Filter Dropdown
    var ddFilter = document.createElement('div');
    ddFilter.className = "dropdown";
    headerNav.appendChild(ddFilter);

    var ddFilterBtn = document.createElement('button');
    ddFilterBtn.innerText = "Filter ";
    ddFilterBtn.className = "btn btn-secondary dropdown-toggle";
    ddFilter.appendChild(ddFilterBtn);

    var ddFilterList = document.createElement('ul');
    ddFilterList.className = "dropdown-menu";
    ddFilter.appendChild(ddFilterList);

    ddFilterBtn.onclick = () => {
        ddFilterList.style.display = (ddFilterList.style.display == "block") ? "none" : "block";
    };
    ddFilterBtn.onblur = () => {
        window.setTimeout(() => {
            ddFilterList.style.display = "none";
        }, 200);
    };

    var ddFilterBoardgames = document.createElement('li');
    ddFilterBoardgames.className = "dropdown-item";
    ddFilterBoardgames.innerHTML = "<input class=\"form-check-input\" type=\"checkbox\" value=\"\" checked id=\"filterBoardgames\"> " +
      "<label class=\"form-check-label\" for=\"filterBoardgames\">Boardgames</label>";
    ddFilterList.appendChild(ddFilterBoardgames);
    document.getElementById('filterBoardgames').onclick = () => {
        window.setTimeout(() => {
            filterList();
        }, 400);
    };

    var ddFilterExpansions = document.createElement('li');
    ddFilterExpansions.className = "dropdown-item";
    ddFilterExpansions.innerHTML = "<input class=\"form-check-input\" type=\"checkbox\" value=\"\" checked id=\"filterExpansions\"> " +
      "<label class=\"form-check-label\" for=\"filterExpansions\">Expansions</label>";
    ddFilterList.appendChild(ddFilterExpansions);
    document.getElementById('filterExpansions').onclick = () => {
        window.setTimeout(() => {
            filterList();
        }, 400);
    };

    var ddFilterDivider = document.createElement('li');
    ddFilterDivider.innerHTML = "<hr class=\"dropdown-divider\">";
    ddFilterList.appendChild(ddFilterDivider);

    var ddFilterUntagged = document.createElement('li');
    ddFilterUntagged.className = "dropdown-item";
    ddFilterUntagged.innerHTML = "<input class=\"form-check-input\" type=\"checkbox\" value=\"\" checked id=\"filterUntagged\"> " +
      "<label class=\"form-check-label\" for=\"filterUntagged\">Untagged</label>";
    ddFilterList.appendChild(ddFilterUntagged);
    document.getElementById('filterUntagged').onclick = () => {
        window.setTimeout(() => {
            filterList();
        }, 400);
    };

    var ddFilterTagged = document.createElement('li');
    ddFilterTagged.className = "dropdown-item";
    ddFilterTagged.innerHTML = "<input class=\"form-check-input\" type=\"checkbox\" value=\"\" checked id=\"filterTagged\"> " +
      "<label class=\"form-check-label\" for=\"filterTagged\">Tagged</label>";
    ddFilterList.appendChild(ddFilterTagged);
    document.getElementById('filterTagged').onclick = () => {
        window.setTimeout(() => {
            filterList();
        }, 400);
    };

    // remove convert button
    nodeBtnConvert.parentNode.removeChild(nodeBtnConvert);

    // remove geeklist items
    nodeItemList.innerHTML = '';

    nodeFirstItem = nodeItemList.childNodes[0];
    nodeItemList.className = "tw-grid tw-gap-2";

    // remove footer nav
    //footerNav.parentNode.removeChild(footerNav);
    footerNav.innerHTML = '';

    geeklistid = window.location.pathname.split('/')[2];

    getTaggedItems();
    items = [];
    fetchPage(1);
}

function fetchPage(page)
{
    // https://api.geekdo.com/api/listitems?page=1&listid=301669
    var url = "https://api.geekdo.com/api/listitems?page=" + page + "&listid=" + geeklistid;

    fetch(url)
        .then(response => response.json())
        .then(
        data => {
            var nr = (data.pagination.pageid - 1) * data.pagination.perPage;
            for (var item of data.data)
            {
                items[nr] = item;
                nr++;
                makeRow(nr, item);
            }
            setProgress(nr, data.pagination.total);

            if (data.data.length < data.pagination.perPage)
            {
                //console.log("last page");
            }
            else if (page < FETCHLIMIT)
            {
                //console.log("more pages");
                fetchPage(page + 1);
            }
        }
    );
}

function makeRow(nr, item)
{
    var row = document.createElement('span');
    row.className = "tw-relative tw-flex tw-flex-wrap tw-items-center tw-gap-x-1.5 tw-gap-y-2 tw-rounded-md tw-border tw-border-gray-400 tw-bg-gray-100 tw-p-2";
    const thingtype = item.item.href.split('/')[1];
    const bodylower = item.body.toLowerCase();
    const notsold = bodylower.includes("not sold");
    const soldto = bodylower.includes("sold to");
    const sold = bodylower.includes("sold");
    const auctionclosed = bodylower.includes("auction closed");

    var struckout = 0;
    var struckoutstart = bodylower.indexOf("[-]");
    var struckoutend = bodylower.indexOf("[/-]");
    while (struckoutstart >= 0 && struckoutend >= 0)
    {
        struckout += struckoutend - struckoutstart;
        struckoutstart = bodylower.indexOf("[-]", struckoutstart + 1);
        struckoutend = bodylower.indexOf("[/-]", struckoutend + 1);
    }


    var tagButton = document.createElement("button");
    if (taggedItems.includes(item.id))
    {
        tagButton.className = "btn btn-sm btn-primary";
        tagButton.innerText = "Tagged";
    }
    else
    {
        tagButton.className = "btn btn-sm btn-outline-primary";
        tagButton.innerText = "Tag";
    }
    tagButton.onclick = () => {
        toggleTaggedItem(tagButton, item.id);
    };
    row.appendChild(tagButton);

    var tagGeeklistLink = document.createElement("a");
    tagGeeklistLink.href = "/geeklist/" + geeklistid + "/test?itemid=" + item.id + "#" + item.id;
    tagGeeklistLink.target = "_blank";
    tagGeeklistLink.innerText = nr + ".";
    row.appendChild(tagGeeklistLink);

    var txtThingType = document.createTextNode(thingtype == "boardgame" ? "Boardgame" : thingtype == "boardgameexpansion" ? "Expansion" : "Other");
    row.appendChild(txtThingType);

    var tagGeeklistItemLink = document.createElement("a");
    tagGeeklistItemLink.href = item.item.href;
    tagGeeklistItemLink.target = "_blank";
    tagGeeklistItemLink.style.maxWidth = "50%";
    tagGeeklistItemLink.style.textOverflow = "ellipsis";
    tagGeeklistItemLink.style.overflow = "hidden";
    tagGeeklistItemLink.style.whiteSpace = "nowrap";
    tagGeeklistItemLink.innerText = item.item.name;
    row.appendChild(tagGeeklistItemLink);

    var tagState = document.createElement("span");
    if (notsold)
    {
        tagState.innerText = "not sold";
        tagState.className = "badge bg-danger";
    }
    else if (soldto || sold)
    {
        tagState.innerText = "sold";
        tagState.className = "badge bg-danger";
    }
    else if (auctionclosed || struckout > 100)
    {
        tagState.innerText = "closed";
        tagState.className = "badge bg-secondary";
    }
    else
    {
        tagState.innerText = "open";
        tagState.className = "badge bg-success";
    }
    row.appendChild(tagState);

    var spanRight = document.createElement('span');
    spanRight.className = "tw-ml-auto tw-hidden tw-pr-0.5 tw-text-sm tw-text-muted md:tw-inline";
    row.appendChild(spanRight);

    var btnPreview = document.createElement('button');
    btnPreview.className = "btn btn-sm text-bg-secondary";
    btnPreview.innerText = "Preview";
    btnPreview.onclick = () => {
        if (btnPreview.data != null)
        {
            btnPreview.data.parentNode.removeChild(btnPreview.data);
            btnPreview.data = null;
        }
        else
        {
            makePreview(item, btnPreview);
        }
    };
    spanRight.appendChild(btnPreview);

    nodeItemList.insertBefore(row, nodeFirstItem);
    item.node = row;
}

function makePreview(item, btn)
{
    var gli = document.createElement('div');
    gli.className = "geeklist-item tw-grid tw-items-start tw-gap-2.5 sm:tw-grid-cols-[minmax(auto,_185px)_minmax(65%,_1fr)] lg:tw-gap-3 lg:tw-gap-x-4";
    item.node.parentNode.insertBefore(gli, item.node.nextSibling);
    btn.data = gli;

    const gliImg = makeItemImage(item);
    gli.appendChild(gliImg);

    var gliDiv = document.createElement('div');
    gliDiv.className = "tw-min-w-0 sm:tw-mt-0";
    gli.appendChild(gliDiv);

    const gliDivBody = makeItemBody(item);
    gliDiv.appendChild(gliDivBody);
}

function makeItemUser(item)
{
}

function makeItemBody(item)
{
    var body = document.createElement('div');

    var parser = new DOMParser();
    var xmlDoc = parser.parseFromString(item.bodyXml, "text/xml");

    for (var child of xmlDoc.firstChild.childNodes)
    {
        console.log(child);
        if (child.nodeName == 'safehtml')
        {
            var safehtml = document.createElement('div');
            safehtml.innerHTML = child.firstChild.data;
            body.appendChild(safehtml);
        }
        else
        {
            var other = document.createElement('div');
            other.className = "alert alert-warning";
            other.innerText = "displaying " + child.nodeName + " is not yet supported by one page auction userscript.";
            body.appendChild(other);
        }
    }

    return body;
}

function makeItemImage(item)
{
    var gliImg = document.createElement('a');
    gliImg.className = "geeklist-item__img-link tw-relative tw-block tw-overflow-hidden tw-rounded-md tw-bg-gray-100";
    gliImg.href = item.linkedImage.href;

    var gliImgDiv = document.createElement('div');
    gliImgDiv.className = "tw-h-44 tw-w-full sm:tw-aspect-w-1 sm:tw-aspect-h-1";
    gliImg.appendChild(gliImgDiv);

    var gliImgDivImg = document.createElement('img');
    gliImgDivImg.className = "img-fluid geeklist-item__img tw-absolute tw-w-full tw-h-full tw-p-2.5 md:tw-p-3.5 tw-object-contain tw-z-10";
    gliImgDivImg.loading = "lazy";
    gliImgDivImg.style.display = "inherit";
    gliImgDivImg.src = item.item.imageSets.square100.src;
    gliImgDivImg.srcset = item.item.imageSets.square100.src + " 1x," + item.item.imageSets.square100["src@2x"] + " 2x";
    gliImgDivImg.alt = item.linkedImage.alt;
    gliImgDivImg.sizes = "";
    gliImgDiv.appendChild(gliImgDivImg);

    gliImgDivImg = document.createElement('img');
    gliImgDivImg.className = "img-fluid tw-absolute tw-w-full tw-h-full tw-object-cover tw-scale-125 tw-opacity-30 tw-blur-xl tw-bg-white";
    gliImgDivImg.loading = "lazy";
    gliImgDivImg.style.display = "inherit";
    gliImgDivImg.src = item.item.imageSets.square100.src;
    gliImgDivImg.srcset = item.item.imageSets.square100.src + " 1x," + item.item.imageSets.square100["src@2x"] + " 2x";
    gliImgDivImg.sizes = "";
    gliImgDiv.appendChild(gliImgDivImg);

    gliImgDiv = document.createElement('div');
    gliImgDiv.className = "tw-absolute tw-right-2 tw-bottom-2 tw-z-20";
    gliImgDiv.innerHTML = "<gg-rating-indicator tooltip=\"Avg. Rating\" shape=\"hex\" size=\"md\">" +
        "<span container=\"body\" class=\"hex rating--" + Math.floor(item.stats.average) + " tw-flex tw-font-semibold tw-h-[var(--hex-h)] tw-items-center tw-justify-center tw-text-white tw-text-xs tw-w-[var(--hex-w)]\"" +
        " style=\"background:" + getColorFromRating(item.stats.average) + "; --hex-w: 1.625rem; --hex-h: 1.875rem; -webkit-clip-path: polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%); clip-path: polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);\">" +
        "<span class=\"tw-pb-px\"> " + (Math.round(item.stats.average * 10) / 10) + " </span>" +
        "</span>" +
        "</gg-rating-indicator>";
    gliImg.appendChild(gliImgDiv);

    return gliImg;
}

function getColorFromRating(rating)
{
    const i = Math.floor(rating);
    switch (i)
    {
        case 1:
        case 2: return "#b2151f";
        case 3:
        case 4: return "#d71925";
        case 5:
        case 6: return "#5369a2";
        case 7: return "#1978b3";
        case 8: return "#1d804c";
        case 9:
        case 10: return "#186b40";
        default: return "#666e75";
    }
}

function encodeWithTextNode(htmlstring) {
	let textarea = document.createElement('textarea');
	let text = document.createTextNode(htmlstring);
	textarea.appendChild(text);
	return textarea.innerHTML;
}

function getTaggedItems()
{
    taggedItems = JSON.parse(GM_getValue("TaggedItems_" + geeklistid, "[]"));
}

function toggleTaggedItem(btn, itemid)
{
    if (taggedItems.includes(itemid))
    {
        taggedItems = taggedItems.filter(i => i != itemid);
        btn.className = "btn btn-sm btn-outline-primary";
        btn.innerText = "Tag";
    }
    else
    {
        taggedItems[taggedItems.length] = itemid;
        btn.className = "btn btn-sm btn-primary";
        btn.innerText = "Tagged";
    }
    GM_setValue("TaggedItems_" + geeklistid, JSON.stringify(taggedItems));
}

async function sortBy(type)
{
    await setProgressAsync(0, items.length);

    var sorted;
    if (type == null || type == '' || type == 'default')
    {
        sorted = items;
    }
    else if (type == 'name')
    {
        sorted = [...items];
        sorted = sorted.sort((a,b) => {
            var r = a.item.name > b.item.name ? 1 : -1;
            return r;
        });
    }

    sortChunks(sorted, 0);
}

function sortChunks(sorted, start)
{
    sortAChunk(sorted, start)
        .then(async data => {
			//console.log(data.next, data.sorted.length);
            await setProgressAsync(data.next, data.sorted.length);

            if (data.next >= data.sorted.length)
            {
                console.log("sorting finished", new Date());
            }
            else
            {
                sortChunks(data.sorted, data.next);
            }
        }
    );
}

function sortAChunk(sorted, start)
{
    const chunk = 250;

	return new Promise(res => {
		window.setTimeout(() => {
			//console.log(start);
			for (var i = 0; i < chunk && start + i < sorted.length; i++)
			{
				var item = sorted[start + i];

				nodeItemList.removeChild(item.node);
                nodeItemList.insertBefore(item.node, nodeFirstItem);
			}
			res({
				sorted: sorted,
				next: start + i
			});
		}, 0);
	});
}

function filterList()
{
    console.log("filtering list");
    const fBg = document.getElementById('filterBoardgames').checked;
    const fEx = document.getElementById('filterExpansions').checked;
    const fUT = document.getElementById('filterUntagged').checked;
    const fTg = document.getElementById('filterTagged').checked;

    for (var i = 0; i < items.length; i++)
    {
        var show = true;
        var item = items[i];
        const thingtype = item.item.href.split('/')[1];
        const tagged = taggedItems.includes(item.id);
        if (!fBg && thingtype == 'boardgame') show = false;
        if (!fEx && thingtype == 'boardgameexpansion') show = false;
        if (!fUT && !tagged) show = false;
        if (!fTg && tagged) show = false;

        var cls = (show ? "tw-relative tw-flex tw-flex-wrap tw-items-center tw-gap-x-1.5 tw-gap-y-2 tw-rounded-md tw-border tw-border-gray-400 tw-bg-gray-100 tw-p-2" : "tw-relative d-none tw-flex-wrap tw-items-center tw-gap-x-1.5 tw-gap-y-2 tw-rounded-md tw-border tw-border-gray-400 tw-bg-gray-100 tw-p-2");
        item.node.className = cls;

        /*if ((i + 1) % 100 == 0 || i == items.length - 1)
        {
            setProgress(i+1, items.length);
        }*/
    }
}





QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址