AO3: [Wrangling] Fandom Resources Quicklinks

adds a bar with fandom-specific links at the top of the bin

04.10.2024 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         AO3: [Wrangling] Fandom Resources Quicklinks
// @namespace    https://greatest.deepsurf.us/en/users/906106-escctrl
// @description  adds a bar with fandom-specific links at the top of the bin
// @author       escctrl
// @version      0.2
// @match        *://*.archiveofourown.org/tags/*/wrangle?*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
// @require      https://update.greatest.deepsurf.us/scripts/491888/1355841/Light%20or%20Dark.js
// @license      MIT
// ==/UserScript==

/* global jQuery, lightOrDark */

(function($) {
    'use strict';

    // --- THE USUAL INIT STUFF AT THE BEGINNING -------------------------------------------------------------------------------

    $("head").append(`<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">`);

    let cfg = 'wrangleResources'; // name of dialog and localstorage used throughout
    let dlg = '#'+cfg;

    /* *** EXAMPLE STORAGE: all Witcher subfandoms point to the AMT which has some resources configured
        resources = {
            "Wiedźmin | The Witcher - All Media Types": [ ["wikia", "https://witcher.fandom.com/wiki/Witcher_Wiki"],
                                                          ["IMDB Cast", "https://www.imdb.com/title/tt5180504/fullcredits/"] ],
            "Wiedźmin | The Witcher (Video Game)":               "Wiedźmin | The Witcher - All Media Types",
            "Wiedźmin | The Witcher Series - Andrzej Sapkowski": "Wiedźmin | The Witcher - All Media Types",
            "The Witcher (TV)":                                  "Wiedźmin | The Witcher - All Media Types"
        };
    */
    let resources = loadConfig();

    // --- CONFIGURATION DIALOG HANDLING -------------------------------------------------------------------------------

    createDialog();

    function createDialog() {

        // if the background is dark, use the dark UI theme to match
        let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "dark-hive" : "base";

        // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling
        $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
        .append(`<style tyle="text/css">${dlg}, .ui-dialog .ui-dialog-buttonpane button {font-size: revert; line-height: 1.286;}
        ${dlg} form {box-shadow: revert; cursor:auto;}
        ${dlg} fieldset {background: revert; box-shadow: revert;}
        ${dlg} fieldset p { padding-left: 0; padding-right: 0; }
        ${dlg} legend {font-size: inherit; height: auto; width: auto; opacity: inherit;}
        ${dlg} fieldset input::placeholder { font-style: italic; opacity: 0.2; }
        ${dlg} fieldset input[type="text"] { padding: 0.2em 0.5em; width: 20em; }
        ${dlg} fieldset input[type="text"][id^="display"] { width: 10em; }
        ${dlg} fieldset p.indented { padding: 0.2em 0 0.2em 2em; }
        ${dlg} fieldset div.fandom, ${dlg} fieldset div.fandom-new { margin: 1em 0 0 0; }
        ${dlg} fieldset button { xfont-size: 80%; margin: 0.1em; }
        ${dlg} fieldset ul.autocomplete { display: inline-block; }
        ${dlg} ul.autocomplete li { margin: 0; }
        </style>`);

        // wrapper div for the dialog
        $("#main").append(`<div id="${cfg}"></div>`);

        // these create the necessary blank HTML fields that get dynamically added on button-clicks
        let newresource = templateResource();
        let newlinked = templateLink();
        let newfandom = templateFandom();

        let prevStoredHTML = "";
        for (let [f, r] of Object.entries(resources)) {
            prevStoredHTML += templateFandom(f, r);
        }

        $(dlg).html(`<form>
            <fieldset><legend>Instructions</legend>
                <div class="userstuff">
                <p style="margin-top: 0;">Select the "Add Fandom" button for input fields to add another fandom to the list.</p>
                <p>The Fandom fields offer autocomplete, so you can easily choose the canonical fandom tag.</p>
                <p style="margin-bottom: 0;">For each fandom, you can choose if it</p>
                <ul style="margin-top: 0;">
                    <li>gets its own set of resources (display text + URL)</li>
                    <li>should be linked to another fandom (e.g. its metatag) and show those same resources</li>
                </ul></div>
            </fieldset>
            <fieldset><legend>Fandom Resources</legend>
                ${prevStoredHTML}
                <div class="fandom-new"><button name="add-fandom" type="button"><i class="fa fa-plus-square" aria-hidden="true"></i> Add Fandom</button></div>
            </fieldset>
        </form>`);

        // optimizing the size of the GUI in case it's a mobile device
        let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px)
        if (dialogwidth < 1000) $("head").append(`<style tyle="text/css"> ${dlg} label { display: none; } </style>`); // saving some space on narrow screens
        dialogwidth = dialogwidth > 500 ? dialogwidth * 0.7 : dialogwidth * 0.9;
        let dialogheight = parseFloat(getComputedStyle($(dlg)[0]).fontSize) * 50;

        $(dlg).dialog({
            appendTo: "#main",
            modal: true,
            title: 'Quicklinks to Fandom Resources Config',
            draggable: true,
            resizable: false,
            autoOpen: false,
            width: dialogwidth,
            maxHeight: dialogheight,
            position: {my:"center", at: "center top"},
            buttons: {
                Reset: deleteConfig,
                Save: storeConfig,
                Cancel: function() { $( dlg ).dialog( "close" ); }
            }
        });

        // if no other script has created it yet, write out a "Userscripts" option to the main navigation
        if ($('#scriptconfig').length == 0) {
            $('#header ul.primary.navigation li.dropdown').last()
                .after(`<li class="dropdown" id="scriptconfig">
                    <a class="dropdown-toggle" href="/" data-toggle="dropdown" data-target="#">Userscripts</a>
                    <ul class="menu dropdown-menu"></ul></li>`);
        }
        // then add this script's config option to navigation dropdown
        $('#scriptconfig .dropdown-menu').append(`<li><a href="javascript:void(0);" id="opencfg_${cfg}">Fandom Resources</a></li>`);

        // on click, open the configuration dialog
        $("#opencfg_"+cfg).on("click", function(e) {
            $( dlg ).dialog('open');
        });

        // delegated event handler for reactive GUI: adding/removing fandoms/resources/links
        $(dlg).on("click", "fieldset button", function(e) {
            e.preventDefault();

            let parent = $(e.target).parent();

            // depending on the button that was clicked, we add/remove different rows of data or hide buttons
            switch (e.target.name) {
                case "set-link":
                    $(parent).before(newlinked).find('[name="set-link"], [name="add-resource"]').hide();
                    e.target.scrollIntoView(false);
                    break;
                case "add-resource":
                    $(parent).before(newresource).find('[name="set-link"]').hide();
                    e.target.scrollIntoView(false);
                    break;
                case "delete-resource":
                    if ($(parent).parent().find('p.resource').length === 1) $(parent).parent().find('[name="set-link"], [name="add-resource"]').show();
                    $(parent).remove();
                    break;
                case "delete-link":
                    $(parent).parent().find('[name="set-link"], [name="add-resource"]').show();
                    $(parent).remove();
                    break;
                case "delete-fandom":
                    $(parent).parent().remove();
                    break;
                case "add-fandom":
                    $(parent).before(newfandom);
                    e.target.scrollIntoView(false);
                    break;
            }
        });
    }

    // --- HELPER FUNCTIONS TO CREATE GUI HTML -------------------------------------------------------------------------------

    function templateFandom(f, r) {
        f = f ?? ""; // avoids that we print "undefined" if this is called for the blank-for-adding-fandom instance
        let resourcesHTML = ""; // holds HTML of the resource/fandom link configuration that was stored for the given fandom
        let visibleButtons = [true, true]; // initialize visible buttons when stored fandom config is shown in GUI

        // when a bunch of resource links were configured for this fandom, we build those as HTML & hide the fandom-link button
        if (r instanceof Array) {
            for (let entry of r) { resourcesHTML += templateResource(entry); }
            visibleButtons[1] = false;
        }
        // when fandom was linked to another, we build that HTML & hide both resource and fandom-link button
        else if (typeof r === "string") {
            resourcesHTML = templateLink(r);
            visibleButtons = [false, false];
        }

        return `
                <div class="fandom">
                    <label for="fandom[]">Fandom:</label>
                    <input type="text" id="fandom[]" name="fandom[]" class="fandom autocomplete" data-autocomplete-method="/autocomplete/fandom"
                    data-autocomplete-hint-text="Start typing for Fandom suggestions!" data-autocomplete-no-results-text="(No suggestions found)"
                    data-autocomplete-min-chars="1" data-autocomplete-searching-text="Searching..." data-autocomplete-token-limit="1" value="${f}"/>
                    ${resourcesHTML}
                    <p class="indented add-new">
                        <button name="add-resource" type="button" ${ visibleButtons[0] ? '' : 'style="display: none;"' }><i class="fa fa-plus-square-o" aria-hidden="true"></i> Add Resource</button>
                        <button name="set-link" type="button" ${ visibleButtons[1] ? '' : 'style="display: none;"' }><i class="fa fa-hand-o-right" aria-hidden="true"></i> Link to Another Fandom</button>
                        <button name="delete-fandom" type="button"><i class="fa fa-trash" aria-hidden="true"></i> Delete Fandom Config</button>
                    </p>
                </div>`;
    }

    function templateResource(r = ["", ""]) {
        return `
                    <p class="indented resource">
                        <i class="fa fa-external-link" aria-hidden="true"></i>
                        <label for="display[]">Resource:</label> <input type="text" id="display[]" name="display[]" placeholder="e.g. IMDB" value="${r[0]}" />
                        <label for="url[]">URL:</label> <input type="text" id="url[]" name="url[]" placeholder="e.g. http://www.imdb.com" value="${r[1]}" />
                        <button name="delete-resource" type="button"><i class="fa fa-minus-square-o" aria-hidden="true"></i> Remove Resource</button>
                    </p>`;
    }

    function templateLink(l="") {
        return `
                    <p class="indented linked">
                        <i class="fa fa-hand-o-right" aria-hidden="true"></i> <label for="linked[]">uses same resources as fandom:</label>
                        <input type="text" id="linked[]" name="linked[]" class="fandom autocomplete" data-autocomplete-method="/autocomplete/fandom"
                        data-autocomplete-hint-text="Start typing for Fandom suggestions!" data-autocomplete-no-results-text="(No suggestions found)"
                        data-autocomplete-min-chars="1" data-autocomplete-searching-text="Searching..." data-autocomplete-token-limit="1" value="${l}"/>
                        <button name="delete-link" type="button"><i class="fa fa-chain-broken" aria-hidden="true"></i> Unlink Fandoms</button>
                    </p>`;
    }

    // --- LOCALSTORAGE MANIPULATION -------------------------------------------------------------------------------

    function deleteConfig() {
        if (confirm('Are you sure you want to delete all Fandom Resource quicklinks?')) {
            localStorage.removeItem(cfg);
            $(dlg).dialog('close');
            // currently this is creating a "n.slice is not a function" exception. not a clue why. none of the other ways to close the dialog have issues.
        }
    }

    function storeConfig() {
        // object to start collecting our storage data
        let fandom_resources = {};

        // grab all the elements and fields
        $(dlg).find('div.fandom').each(function(ix) {
            let fandom = $(this).find('> ul.autocomplete li.added.tag');

            if ($(fandom).length === 1) {
                fandom = $(fandom).contents().eq(0).text().trim();

                let linkedfandom = $(this).find('p.linked > ul.autocomplete li.added.tag');
                let resources = $(this).find('p.resource');

                if ($(linkedfandom).length === 1 && $(resources).length > 0) {
                    console.log('resources for fandom '+fandom+' could not be stored, both link and resources given - only one of these is allowed');
                }
                else if ($(resources).length > 0) {
                    let resource_list = []; // to keep this simple, we'll use an Array because JSON.stringify(Map) ends up with an empty {}
                    $(resources).each(function() {
                        let display = $(this).find('input[id="display[]"]').prop('value') || "";
                        let url = $(this).find('input[id="url[]"]').prop('value') || "";
                        resource_list.push([display, url]);
                    });

                    fandom_resources[fandom] = resource_list;
                }
                else if ($(linkedfandom).length === 1) {
                    linkedfandom = $(linkedfandom).contents().eq(0).text().trim();
                    fandom_resources[fandom] = linkedfandom;
                }
                else console.log('resources for fandom '+fandom+' could not be stored, no link nor resources given');
            }
            else console.log('resources for entry #'+ix+' could not be stored, no fandom given');
        });

        // by the end of this, we've filled up fandom_resources with all data and are ready to store
        localStorage.setItem(cfg, JSON.stringify(fandom_resources));
        $(dlg).dialog('close');
    }

    function loadConfig() {
        return JSON.parse(localStorage.getItem(cfg) ?? "{}");
    }

    // --- WRITING THE TOP BAR WITH THE WRANGLING RESOURCE LINKS -------------------------------------------------------------------------------

    // if this isn't a fandom bin, quit because we don't know the fandom to show resources for
    // also quit if there are no tags to display: mostly because we rely on #wrangulator to provide the styling
    if ($('#inner').find('ul.navigation.actions').eq(1).find('li').length != 5 || $('#wrangulator').length < 1) return;

    // grab the currently viewed fandom name
    let fandom = $('#main > .heading a.tag').text();

    // if this fandom points to another fandom (or just some nickname), we'll continue looking for links under that other fandom name
    if (typeof resources[fandom] === "string") fandom = resources[fandom];

    // if there are links configured, we'll display them. or it's an empty bar.
    if (resources[fandom] instanceof Array) {
        let links = [];
        resources[fandom].forEach((val) => links.push(`<a href="${val[1]}" target="_blank">${val[0]} <i class="fa fa-external-link" aria-hidden="true"></i></a>`));

        let bgcolor = $('#wrangulator fieldset').css('background-color');
        let fontcolor = $('#wrangulator fieldset').css('color');
        let boxshadow = $('#wrangulator fieldset').css('box-shadow');

        $('#header').append(`<div style="background-color: ${bgcolor}; color: ${fontcolor};
          padding: 0.5em 0.5em 0.5em 1em;
          xmargin-top: -1em;
          box-shadow: ${boxshadow};
          text-align: center;
          font-size: 90%;">Fandom Resources: ${links.join(", ")}</div>`);
    }
})(jQuery);