Beyond Dark

A Dark Theme for the DnDBeyond Character Sheet! (WIP)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Beyond Dark
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  A Dark Theme for the DnDBeyond Character Sheet! (WIP)
// @author       You
// @match        https://www.dndbeyond.com/*characters/*
// @grant        GM_addStyle
// ==/UserScript==

//pSBC Color Shade/Blend/Convert tool (V4). Source: https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)
const pSBC = (p, c0, c1, l) => {
    let r, g, b, P, f, t, h, i = parseInt, m = Math.round, a = typeof (c1) == "string";
    if (typeof (p) != "number" || p < -1 || p > 1 || typeof (c0) != "string" || (c0[0] != 'r' && c0[0] != '#') || (c1 && !a)) return null;
    if (!this.pSBCr) this.pSBCr = (d) => {
        let n = d.length, x = {};
        if (n > 9) {
            [r, g, b, a] = d = d.split(","), n = d.length;
            if (n < 3 || n > 4) return null;
            x.r = i(r[3] == "a" ? r.slice(5) : r.slice(4)), x.g = i(g), x.b = i(b), x.a = a ? parseFloat(a) : -1
        } else {
            if (n == 8 || n == 6 || n < 4) return null;
            if (n < 6) d = "#" + d[1] + d[1] + d[2] + d[2] + d[3] + d[3] + (n > 4 ? d[4] + d[4] : "");
            d = i(d.slice(1), 16);
            if (n == 9 || n == 5) x.r = d >> 24 & 255, x.g = d >> 16 & 255, x.b = d >> 8 & 255, x.a = m((d & 255) / 0.255) / 1000;
            else x.r = d >> 16, x.g = d >> 8 & 255, x.b = d & 255, x.a = -1
        } return x
    };
    h = c0.length > 9, h = a ? c1.length > 9 ? true : c1 == "c" ? !h : false : h, f = this.pSBCr(c0), P = p < 0, t = c1 && c1 != "c" ? this.pSBCr(c1) : P ? { r: 0, g: 0, b: 0, a: -1 } : { r: 255, g: 255, b: 255, a: -1 }, p = P ? p * -1 : p, P = 1 - p;
    if (!f || !t) return null;
    if (l) r = m(P * f.r + p * t.r), g = m(P * f.g + p * t.g), b = m(P * f.b + p * t.b);
    else r = m((P * f.r ** 2 + p * t.r ** 2) ** 0.5), g = m((P * f.g ** 2 + p * t.g ** 2) ** 0.5), b = m((P * f.b ** 2 + p * t.b ** 2) ** 0.5);
    a = f.a, t = t.a, f = a >= 0 || t >= 0, a = f ? a < 0 ? t : t < 0 ? a : a * P + t * p : 0;
    if (h) return "rgb" + (f ? "a(" : "(") + r + "," + g + "," + b + (f ? "," + m(a * 1000) / 1000 : "") + ")";
    else return "#" + (4294967296 + r * 16777216 + g * 65536 + b * 256 + (f ? m(a * 255) : 0)).toString(16).slice(1, f ? undefined : -2)
}
(function () {
    'use strict';
    // Userscript Globals
    var accentColor;
    var accentColorMuted;
    /**
     * @type {CSSStyleSheet}
     */
    var sheet;

    // // Change BgColor
    // var bodyElems = document.getElementsByTagName('body');
    // for (let i = 0; i < bodyElems.length; i++) {
    //     bodyElems[i].style.background = "#222222";
    //     console.log("Changing BG Color. " + i);
    // }

    const colorReplacements = {
        "#ffffff": "#353535",
        "#ededed": "#282828",
        "#e0e3e3": "#323232",
        "#838383": "#ababab",
        "#faf8f7": "#3a3d3e",
    }
    initStyleSheet();
    // Called ONCE after DOMContentLoaded
    function onLoad() {
        var startTime = performance.now();
        console.info("Starting Userscript.");

        accentColor = getCharColor();
        accentColorMuted = pSBC(-0.8, accentColor, "#383838");

        restyleSvgs();

        sheet.insertRule(
            `.ddbc-tab-options--layout-pill div.ddbc-tab-options__header-heading--is-active {
                background-color: ${accentColor} !important;
            }`);


        restyleQuickRollAreas();

        fixSlotManagerColors();

        // restyleImgIcons();

        console.log(`DarkMode main exec time: ${performance.now() - startTime} ms`);
    }

    function getCharColor() {
        return window.getComputedStyle(document.querySelectorAll(".body-rpgcharacter-sheet .site-bar")[0]).getPropertyValue('border-color');;
    }

    function initStyleSheet() {
        // Create Stylesheet
        sheet = createStyleSheet('dark-mode-userscript');

        // Background
        sheet.insertRule(`body {
            background: #222222 !important;
        }`);

        // Text Color
        const content_container = document.getElementById('content');
        content_container.style.color = '#d4d5d7';//'#e4e5e7';

        // Initiative
        // document.getElementsByClassName('ct-combat__summary-label--outline')[0].style.textShadow = 'none';
        // Inspiration
        // document.getElementsByClassName('ct-inspiration__label')[0].style.textShadow = 'none';
        sheet.insertRule(`div.ct-combat__summary-label--outline, div.ct-inspiration__label {
            text-shadow: none;
        }`)
        // Sidebar Recolor
        sheet.insertRule(`div.ct-sidebar__pane-gap, div.ct-sidebar__pane-content {
            background-color: #303030;
        }`);
        // Ability Scores Recolor
        sheet.insertRule(
            `.ddbc-ability-summary__primary,
        .ct-proficiency-bonus-box__value .ddbc-signed-number.ddbc-signed-number--large,
        .ct-proficiency-bonus-box .ct-proficiency-bonus-box__heading,
        .ddbc-item-name--rarity-common,
        .ddbc-tab-list__nav-item:hover,
        .ddbc-tab-list__nav-item--is-active,
        .ddbc-spell-name,
        .ddbc-checkbox__label,
        .ct-spell-manager__filter-heading,
        .ct-class-spell-manager__heading,
        .ct-inventory-filter__input,
        .ct-spells-filter__input,
        .ct-feats-manage-pane__feat-heading-name,
        .ct-sidebar__heading,
        .ct-health-summary__adjuster-field-input,
        .collapsible-heading
        {
            color: #e4e5e7 !important;
        }`);
        sheet.insertRule(
            `.ct-class-spell-manager__info,
        .ddbc-html-content p,
        .ct-sidebar__pane-content > *,
        .ct-campaign-pane a {
            color: #d4d5d7 !important;
        }`);

        // Restyling Accent/Element bg colors
        // #f1f1f1 -> #434343
        sheet.insertRule(
            `div.ddbc-collapsible__header {
            background-color: #434343;
        }`);
        // #f2f2f2 -> #424242;
        sheet.insertRule(
            `.ddbc-tab-options--layout-pill div.ddbc-tab-options__header-heading,
        div.ct-spell-detail__tag,
        div.ct-item-detail__tag {
            background-color: #424242;
        }`);

        // Attacks per Action text: convert to secondary color
        sheet.insertRule(
            `span.ct-actions__attacks-per-action {
            color: #838383;
        }`
        );
        // BG #ffffff -> #353535
        sheet.insertRule(
            `.site button.ct-health-summary__adjuster-button,
        input.ct-health-summary__adjuster-field-input {
            background-color: ${colorReplacements["#ffffff"]} !important;
        }`);

        // Tables
        //#fff -> #353535
        sheet.insertRule(
            `.ct-sidebar td {
            background: ${colorReplacements["#ffffff"]} !important;
        }`)
        //#faf8f7 ->
        sheet.insertRule(
            `.ct-sidebar tr:nth-child(odd) td {
            background: ${colorReplacements["#faf8f7"]} !important;
        }`)

        // Collapsible Content Bg
        sheet.insertRule(
            `.ddbc-collapsible__content {
            background-color: #383838 !important;
        }`);
        // SVG Backgrounds
        sheet.insertRule(`
        svg > path,
        .ct-combat__statuses polygon,
        .ct-initiative-box polygon,
        #InspirationBoxSvg-Fill-1,
        .ddbc-svg.ddbc-ability-score-box-svg > path:nth-child(2),
        .ct-proficiency-groups-box polygon,
        .ct-sidebar__pane-top path,
        .ct-sidebar__pane-bottom path,
        svg.ddbc-svg.ddbc-manage-level-svg.ddbc-svg--themed path
        {
            fill: #303030 !important;
        }`);
        sheet.insertRule(`
        .ddbc-svg.ddbc-proficiency-svg.ddbc-proficiency-icon.ddbc-proficiency-level-icon circle
        {
            fill: #cbcbcb !important;
        }`);
        // const no_prof_circles = document.getElementsByClassName('ddbc-no-proficiency-icon');
        // for (let circle of no_prof_circles) {
        //     circle.style.backgroundColor = "inherit";
        // }
        sheet.insertRule(`span.ddbc-no-proficiency-icon {
            background-color: inherit;
        }`)

        sheet.insertRule(`.ct-skills__list {
            overflow-y: auto;
            max-height: min-content !important;
        }`);
    }

    // function recolorElements(elems) {
    //     let matchCount = 0;
    //     for (let i = 0; i < elems.length; i++) {
    //         //console.log("Userscript: test");
    //         let el = elems[i];
    //         let bgColorRGB = getRGBValues(window.getComputedStyle(el, null).getPropertyValue("background-color"));
    //         let bgColor = rgbToHex(bgColorRGB[0], bgColorRGB[1], bgColorRGB[2]);

    //         if (!el.classList.contains('ddbc-no-proficiency-icon')) {
    //             const colorMatch = colorReplacements[bgColor];
    //             if (colorMatch) {
    //                 el.style.backgroundColor = colorMatch;
    //                 matchCount++;
    //                 console.log(el);
    //             }
    //         }

    //     }
    //     console.log(`matchCount: ${matchCount}`);
    // }

    function fixSlotManagerColors() {
        document.querySelectorAll('.ct-slot-manager__slot.ct-slot-manager__slot--interactive').forEach(e => e.style.backgroundColor = "");
    }

    function restyleQuickRollAreas() {
        sheet.insertRule(`.beyond20-quick-roll-area {
            border: 1px solid ${accentColor.toString()};
            border-radius: 6px;
            box-sizing: border-box;
            margin-right: 5px;
        }`);
        sheet.insertRule(`.ct-spells-spell__action.beyond20-quick-roll-area {
            padding-right: 0 !important;
        }`)

        sheet.insertRule(//.ct-skills__col--modifier
            `.beyond20-quick-roll-area:hover {
            background-color: ${accentColorMuted};
        }
        `);
    }

    function restyleSvgs() {
        // const svgs = document.getElementsByClassName('ddbc-svg');
        //console.log(svgs);
        //console.log("I'm Confused");
        // for (let svg of svgs) {

        //     let paths = svg.getElementsByTagName("*");
        //     //paths[0].setAttribute("fill", "#383838");
        //     for (let j = 0; j < paths.length; j++) {
        //         //console.log("elems test" +j);
        //         let c_path_fill = paths[j].getAttribute("fill")
        //         if (c_path_fill === "#FEFEFE") {
        //             paths[j].setAttribute("fill", "#373737");
        //         } else if (c_path_fill === "#383838") {
        //             paths[j].setAttribute("fill", "#cbcbcb");
        //         }
        //     }
        // }

        // Some css selectors grabbed from https://github.com/Azmoria/dndbeyonddark
        // Borders
        sheet.insertRule(`
        svg > path + path,
        .ct-combat__statuses path,
        .ct-initiative-box path,
        .ct-inspiration path,
        .ct-proficiency-groups-box path,
        .ddbc-attunement-slot.ddbc-attunement-slot--empty path
        {
            fill: ${accentColor} !important;
        }`);

    }

    function restyleImgIcons() {
        sheet.insertRule(`.i-hamburger, .i-concentration, .i-homebrew, .i-spell-melee, .i-spell-ranged, .i-favorite, .i-strength-save, .i-dexterity-save, .i-constitution-save, .i-charisma-save, .i-wisdom-save, .i-intelligence-save, .i-type-acid, .i-type-bludgeoning, .i-type-cold, .i-type-fire, .i-type-force, .i-type-lightning, .i-type-necrotic, .i-type-piercing, .i-type-poison, .i-type-psychic, .i-type-radiant, .i-type-slashing, .i-type-thunder, .i-condition-blinded, .i-condition-charmed, .i-condition-deafened, .i-condition-exhaustion, .i-condition-exhausted, .i-condition-frightened, .i-condition-grappled, .i-condition-incapacitated, .i-condition-invisible, .i-condition-paralyzed, .i-condition-petrified, .i-condition-poisoned, .i-condition-prone, .i-condition-restrained, .i-condition-stunned, .i-condition-unconscious, .i-condition-white-blinded, .i-condition-white-charmed, .i-condition-white-deafened, .i-condition-white-exhaustion, .i-condition-white-exhausted, .i-condition-white-frightened, .i-condition-white-grappled, .i-condition-white-incapacitated, .i-condition-white-invisible, .i-condition-white-paralyzed, .i-condition-white-petrified, .i-condition-white-poisoned, .i-condition-white-prone, .i-condition-white-restrained, .i-condition-white-stunned, .i-condition-white-unconscious, .i-aoe-cone, .i-aoe-cube, .i-aoe-cylinder, .i-aoe-line, .i-aoe-sphere, .i-aoe-square, .i-artificer, .i-bard, .i-cleric, .i-druid, .i-paladin, .i-ranger, .i-sorcerer, .i-warlock, .i-wizard, .i-barbarian, .i-fighter, .i-monk, .i-rogue, .i-aberration, .i-beast, .i-celestial, .i-construct, .i-dragon, .i-elemental, .i-fey, .i-fiend, .i-giant, .i-humanoid, .i-monstrosity, .i-ooze, .i-plant, .i-undead, .i-ritual, .i-req-attunement, .i-legendary-monster, .i-checkmark, .i-partnered-content-small, .i-partnered-content-medium, .i-partnered-content-large, .i-copy, .i-download, .i-gift, .i-radio-checked, .i-radio-empty, .i-radio-checked-light
        , .ddbc-healing-icon__icon, .ddbc-combat-attack__icon-img, .ddbc-combat-attack__icon-img--action-attack-weapon-melee
        {
            filter: invert(100%);
        }`)
    }

    function restyleSidebar() {
        let sidebarCapSvgs = document.getElementsByClassName('ddbc-sidebar-cap-svg');
        // let sidebarGap = document.getElementsByClassName('ct-sidebar__pane-gap');
        // let sidebarContent = document.getElementsByClassName('ct-sidebar__pane-content');
        for (let svg of sidebarCapSvgs) {
            svg.children[0].setAttribute("fill", "#303030");
        }

        // for (let el of sidebarGap) {
        //     el.style.backgroundColor = "#383838";
        // }
        // for (let el of sidebarContent) {
        //     el.style.backgroundColor = "#383838";
        // }

    }

    // Bind Event Listeners
    window.addEventListener('load', () => setTimeout(onLoad, 500), false);

    window.addEventListener('resize', () => setTimeout(() => {
        restyleSidebar();
        restyleSvgs();
    }, 0));

    document.body.addEventListener('click', () => setTimeout(() => {
        restyleSidebar();
        // restyleQuickRollAreas();
    }, 0));


    //=========
    // Utility
    //=========
    function componentToHex(c) {
        var hex = c.toString(16);
        return hex.length == 1 ? "0" + hex : hex;
    }

    function rgbToHex(r, g, b) {
        return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    }

    function getRGBValues(rgbString) {
        rgbString = rgbString.substring(rgbString.indexOf('(') + 1, rgbString.indexOf(')'));
        // Notice here that we don't know how many digits are in each value,
        // but we know that every value is separated by a comma.
        // So split the three values using comma as the separator.
        // The split function returns an object.
        let rgbColors = rgbString.split(',', 3);

        // Convert redValue to integer
        rgbColors[0] = parseInt(rgbColors[0]);
        // Convert greenValue to integer
        rgbColors[1] = parseInt(rgbColors[1]);
        // Convert blueValue to integer
        rgbColors[2] = parseInt(rgbColors[2]);

        return rgbColors;
    }

    function createStyleSheet(id, media) {
        var el = document.createElement('style');
        // WebKit hack
        el.appendChild(document.createTextNode(''));
        // el.type  = 'text/css';
        el.rel = 'stylesheet';
        el.media = media || 'screen';
        el.id = id;
        document.head.appendChild(el);
        return el.sheet;
    }

})();